Skip to main content

Section 3.6 Primitive & Reference Types

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.

Subsection 3.6.1 Two Ways Java Stores Data

Java has two fundamentally different ways of handling data:
  1. Primitive Types (int, double, boolean, etc.)
    • Stored directly in memory as their actual value
    • When copied, you get a completely independent copy
    • Examples: numbers, true/false values
    • The name of type starts with lowercase letter
  2. Reference Types (Objects)
    • Stored as a reference (like an address) that points to data elsewhere in memory
    • When copied, you copy the reference but both still point to the same data
    • Examples: String, ArrayList, your own classes
    • The name of type starts with uppercase letter
Think of it like this:
Real World Analogy: School Lockers
  • Primitives are like writing on a sticky note:
    • The actual number is right there on the note
    • If you copy the note, each person has their own independent copy
    • Changing one copy doesn’t affect the other
  • Objects are like using a school locker:
    • Instead of carrying everything, you just carry the locker number
    • If you share that locker number, you’re both using the same locker
    • When one person changes something in the locker, everyone with that number sees the change
Let’s see both perspectives in code:
// 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
In memory, it looks like this:
Primitives:           Objects:
x: [42]              p1 ────┐
y: [100]                     └──> [Player data]
                     p2 ────┘
This fundamental difference affects everything from variable assignment to method parameters, which we’ll explore next.

Subsection 3.6.2 How Primitives Work: Independent Copies

Remember our sticky note analogy? Let’s see exactly how primitives work with a game score example:
// 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
Let’s watch how memory changes at each step:
Step 1: Create score
┌─────────────┐
│score: [100]   │ 
└─────────────┘

Step 2: Create backup
┌─────────────┐
│score: [100]   │
│backup:[100]   │
└─────────────┘

Step 3: Change backup
┌─────────────┐
│score: [100]   │
│backup:[50]    │
└─────────────┘
This works the same for all primitive types:
double price = 9.99;              // One sticky note
double salePrize = price;         // New independent sticky note
salePrize = 7.99;                 // Only changes salePrize

boolean gameOver = false;         // One sticky note
boolean lastState = gameOver;     // New independent sticky note
lastState = true;                 // Only changes lastState
🚫 Common Mistake: Thinking variables are connected
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!

Subsection 3.6.3 How Objects Work: Sharing Access Through References

Remember our locker analogy? Let’s see how it works with a simple game character:
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 ────┘

Subsection 3.6.4 Methods with References: The Right and Wrong Ways

Let’s look at a real game scenario: damaging multiple players in an area effect spell:
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
Memory during rightAreaDamage:

Outside method:          Inside rightAreaDamage:
hero ────┐               p1 ────┐
          └─> [H:100]            └─> [H:100] -> [H:70]
ally ────┐               p2 ────┐
          └─> [H:80]             └─> [H:80]  -> [H:50]
🚫 Common Mistakes:
// 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!

Subsection 3.6.5 What Really Happens Inside Methods

Let’s watch exactly what happens in both the wrong and right approaches:

Subsubsection 3.6.5.1 Wrong Way: Passing Health Value

Step 1: Starting state
┌─────────────────┐
│Hero's Locker      │
│health = 100       │
└─────────────────┘

Step 2: Call wrongReduce(hero.getHealth(), 5)
┌─────────────────┐    ┌─────────────┐
│Hero's Locker       │    │Method's Copy  │
│health = 100        │    │health = 100   │
└─────────────────┘    └─────────────┘
                          ↓
                       health = 95
                          ↓
Hero unchanged!        Copy discarded

Result: Hero still at 100 health

Subsubsection 3.6.5.2 Right Way: Passing Player Reference

Step 1: Starting state         Step 2: Inside rightReduce
hero──┐                        hero──┐
       └→[health: 100]                └→[health: 100]
                                p  ──┘

Step 3: After p.reduce(5)     Step 4: Method ends
hero──┐                       hero──┐
       └→[health: 95]                └→[health: 95]
 p  ──┘                       (p is discarded)
Quick Comparison:
Action Wrong Way Right Way
What’s passed Copy of 100 Locker number
Method sees New note Same locker
Changes affect Only the copy Real player
After method Original unchanged Player modified
Remember: When you pass a Player, you’re sharing the locker number!

Subsection 3.6.6 Sharing Multiple Lockers: Group Methods

Think of a method that works with multiple objects like a person with multiple locker numbers. Let’s see what happens in a boss battle:
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.

Subsection 3.6.7 Summary: What You Need to Remember

Let’s wrap up with the most important points and common mistakes to avoid:

Subsubsection 3.6.7.1 🎯 Key Concepts

  1. Primitives are Like Sticky Notes
    int x = 42;
    int y = x;     // New note with copy of 42
    y = 10;        // Only changes y's note
    
  2. Objects are Like Lockers
    Player p1 = new Player(100);  // New locker
    Player p2 = p1;               // Shared locker number
    Player p3 = new Player(100);  // Different locker!
    

Subsubsection 3.6.7.2 🚫 Common Mistakes to Avoid

// 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
Remember:
  • Primitives: Each variable has its own independent value
  • Objects: Variables can share access to the same object
  • Methods: Get copies of what you pass, but copies of locker numbers still open the same locker!

Subsection 3.6.8 Exercises: Primitive & Reference Types

Checkpoint 3.6.1. Multiple Choice: Primitives vs. References.

Which statement best describes how primitives differ from reference types (objects) in Java?
  • 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.

Checkpoint 3.6.2. Multiple Choice: Copying References.

Consider the following code:
Player p1 = new Player(100);
Player p2 = p1;
p2.damage(10);
System.out.println(p1.getHealth());
Which outcome is most likely?
  • The code does not compile, because you cannot assign p1 to p2 without using new.
  • It does compile. Assigning references is allowed.
  • p1’s health is still 100, because p2 is a separate Player.
  • No, p2 = p1 means both variables refer to the same Player.
  • p1’s health is 90, because p1 and p2 point to the same object.
  • Correct. Damage via p2 affects the same underlying Player that p1 sees.
  • The code throws a runtime error on p2.damage(10) because p2 has no reference.
  • That would only happen if p2 were null, but here it’s given the same reference as p1.

Checkpoint 3.6.3. True/False: Passing Primitives to Methods.

    If you pass an int to a method and modify it inside the method, the original int in the caller remains unchanged.
  • True.

  • Exactly. Primitives are passed by value, so changes affect only the copy, not the caller’s variable.
  • False.

  • Exactly. Primitives are passed by value, so changes affect only the copy, not the caller’s variable.

Checkpoint 3.6.4. True/False: Passing Object References to Methods.

    If you pass an object reference (like a Player) to a method and the method calls player.damage(10), the caller’s object is affected.
  • True.

  • Correct! Even though Java is pass-by-value for the reference, that “value” is the address, so changes to the object persist.
  • False.

  • Correct! Even though Java is pass-by-value for the reference, that “value” is the address, so changes to the object persist.

Checkpoint 3.6.5. Short Answer: Explaining the "Locker Number" Analogy.

In 1–3 sentences, use the "locker number" analogy to explain why two variables like p1 and p2 might show the same change when you call p2.damage(10) .
Solution.
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.

Checkpoint 3.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.

Checkpoint 3.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.
Solution.
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.
You have attempted of activities on this page.