To write effective Java code, we need to understand how Java actually stores different types of data in memory. This explains why sometimes changing a value in a method affects the original variable, and other times it doesn’t.
// PRIMITIVES: Direct values
int x = 42; // x stores 42 directly
int y = x; // y gets its own copy of 42
y = 100; // changing y doesn't affect x
System.out.println(x); // still prints 42
// OBJECTS: References to data
Player p1 = new Player(100); // p1 stores a reference to the Player
Player p2 = p1; // p2 gets a copy of the reference
p2.reduceHealth(50); // affects the one shared Player
System.out.println(p1.getHealth()); // prints 50 - both see the change
// Initial score
int score = 100; // Write "100" on first sticky note
// Try to create a backup
int backupScore = score; // Copy "100" to a new sticky note
// Change the backup
backupScore = 50; // Change only the backup note to "50"
// Check both scores
System.out.println("Original: " + score); // Shows 100
System.out.println("Backup: " + backupScore); // Shows 50
int lives = 3;
int extraLives = lives; // Creates a copy, not a connection!
extraLives--; // Only extraLives becomes 2
System.out.println(lives); // Still 3!
Remember: With primitives, each variable gets its own independent sticky note. Changes to one never affect the other!
public class Player {
private int health;
public Player(int h) { health = h; }
public void damage(int amt) { health -= amt; }
public int getHealth() { return health; }
}
When you create and share a Player object, here’s what happens:
// Step 1: Create new Player (like getting a new locker)
Player hero = new Player(100); // Get locker #123 with 100 health inside
// Step 2: Share the locker number
Player sidekick = hero; // Give sidekick the same locker number (#123)
// Step 3: Either person can affect the shared Player
sidekick.damage(25); // Sidekick opens locker #123 and reduces health
System.out.println(hero.getHealth()); // Hero checks same locker, sees 75 health
Before damage: After damage:
hero ────┐ hero ────┐
└─> [100] └─> [75]
sidekick ────┘ sidekick ────┘
public class Player {
private int health;
private String name;
public Player(String name, int health) {
this.name = name;
this.health = health;
}
public void damage(int amt) {
health = Math.max(0, health - amt);
}
public int getHealth() { return health; }
}
// Two approaches to area damage:
public class Game {
// ❌ WRONG WAY: Tries to work with copies of numbers
public static void wrongAreaDamage(int health1, int health2, int damage) {
health1 -= damage; // Only changes local copies
health2 -= damage; // Original players unaffected
}
// ✅ RIGHT WAY: Works with the actual players
public static void rightAreaDamage(Player p1, Player p2, int damage) {
p1.damage(damage); // Changes real player health
p2.damage(damage); // Changes real player health
}
}
Let’s see what happens in memory when we use these:
Player hero = new Player("Hero", 100);
Player ally = new Player("Ally", 80);
// Wrong way - nothing changes:
wrongAreaDamage(hero.getHealth(), ally.getHealth(), 30);
System.out.println(hero.getHealth()); // Still 100
System.out.println(ally.getHealth()); // Still 80
// Right way - both players take damage:
rightAreaDamage(hero, ally, 30);
System.out.println(hero.getHealth()); // Now 70
System.out.println(ally.getHealth()); // Now 50
// Mistake 1: Passing number instead of Player
healPlayer(hero.getHealth(), 50); // Wrong! Passes copy of health
healPlayer(hero, 50); // Right! Passes player reference
// Mistake 2: Thinking new variables mean new objects
Player backup = hero; // Same player, two references
Player clone = new Player(hero.getHealth()); // Different player!
public static void bossFight(Player hero, Player boss, int damage) {
// Inside method: we have copies of both locker numbers
hero.damage(damage); // Open hero's locker, reduce health
boss.damage(damage*2); // Open boss's locker, reduce health more
}
// Create two separate lockers
Player hero = new Player("Hero", 100); // Locker #1: 100 health
Player boss = new Player("Boss", 200); // Locker #2: 200 health
// Share both locker numbers with bossFight
bossFight(hero, boss, 30);
Before battle: After battle:
hero ────┐ hero ────┐
└─> [H:100] └─> [H:70]
boss ────┐ boss ────┐
└─> [H:200] └─> [H:140]
Inside bossFight method:
hero & p1 ────┐
└─> [Same Hero object]
boss & p2 ────┐
└─> [Same Boss object]
Key Point: A method can receive multiple locker numbers (references) and modify all those objects. Each parameter is a copy of the reference, but they all point to the real objects.
// Wrong: Passing the Number Instead of the Locker
// ❌ WRONG - passes copy of health number
void healPlayer(int health, int amount) {
health += amount; // Changes thrown away
}
// ✅ RIGHT - passes locker number (reference)
void healPlayer(Player player, int amount) {
player.heal(amount); // Changes real player
}
// Wrong: Thinking New Variables Mean New Objects
// ❌ WRONG assumption: these are different players
Player backup = hero; // Just sharing same locker!
backup.damage(10); // Hero also loses health!
// ✅ RIGHT way to make a separate player
Player clone = new Player(hero.getHealth()); // New locker
// Wrong: Misunderstanding Pass-by-Value
// Java always passes copies, but:
primitiveMethod(score); // Copies the number
objectMethod(player); // Copies the locker number
Primitives are allocated on the heap, while references are allocated on the stack.
The real distinction is that primitives store actual values, while references store addresses. This statement is an oversimplification (and not always accurate about stack vs. heap).
Primitives store their actual value in each variable; reference variables store an address pointing to shared data elsewhere.
Correct! Primitive variables each hold an independent value (like a sticky note), while references point to the same underlying object (like a locker number).
Primitives can never be copied, but references can be freely copied.
Actually, primitives are copied (by value). You can also copy a reference variable (resulting in two variables pointing to the same object).
You must call new to create primitives, but objects are created automatically.
It’s the opposite: primitives don’t use new; objects typically do.
Sample Solution: When you assign p2 = p1, you give p2 the same "locker number" that p1 already had. So, p1 and p2 are both opening the same locker (the same Player object). If p2.damage(10) changes what’s in that locker, p1 sees the change as well.
Checkpoint3.6.6.Parsons: Wrong vs Right Method for Updating Health.
Rearrange the lines below to form a short Java class with two methods: wrongHeal (using an int parameter) and rightHeal (using a Player parameter). One method won’t actually change the player’s health, and the other will. Put the lines in a valid order with proper braces.
public class HealingTest {
---
public static void wrongHeal(int health, int amount) {
---
health += amount; // Only changes the copy
---
}
---
public static void rightHeal(Player p, int amount) {
---
p.heal(amount); // Actually changes Player's health
---
}
---
}
Checkpoint3.6.7.Reflection: Pass-by-Value for Primitives vs. References.
In 2–3 sentences, reflect on why Java is said to be “pass-by-value” even when calling a method with an object reference. Consider what’s being copied and how that leads to changes in the original object.
Sample Solution: Java always copies the variable’s value. For primitives, that value is the actual number. For objects, the value is the reference (like a locker number). Copying the same “locker number” means both copies point to the same object, so changes in the method remain visible to the caller.