Course outline · 0% complete

0/29 lessons0%

Course overview →

Scope and pass-by-value

lesson 4-3 · ~7 min · 12/29

What a method can actually change

Sooner or later you will pass a variable into a method, watch the method appear to change it, and find the original untouched. The reverse also happens, where a method quietly modifies an array you never expected it to.

Both surprises come from one rule, and misunderstanding it causes real bugs, so it is worth one focused lesson.

The rule is that Java always passes a copy of the variable's value into the method. This is called pass-by-value, and Java has no other mode.

  • For a primitive, such as int, double, or boolean, the value is the number, so the method gets a copy of the number. Nothing the method does can touch the caller's variable.
  • For an object type, meaning arrays, Strings, and your own classes, the value stored in the variable is a reference, the heap location from lesson 2-2. The method gets a copy of the reference, and both copies point at the same object.

So a method can never re-point your variable, yet it can freely modify the object your variable points at. Those two halves of the rule explain every case below.

primitive: the number is copied main score = 50 addBonus score = 150 independent object type: the reference is copied main scores zeroFirst scores heap array [0, 60, 70] same object
Both calls copy the variable. Copying a number leaves the caller untouched, while copying a reference hands the method a second arrow to the same array.

A copied number and a shared array

One method receives a primitive, the other receives an array, and only one of them affects the caller.

public class Main {
  static void addBonus(int score) {
    score = score + 100;
    System.out.println("inside addBonus: " + score);
  }

  static void zeroFirst(int[] scores) {
    scores[0] = 0;
  }

  public static void main(String[] args) {
    int score = 50;
    addBonus(score);
    System.out.println("after addBonus: " + score);

    int[] scores = {50, 60, 70};
    zeroFirst(scores);
    System.out.println("scores[0] after zeroFirst: " + scores[0]);
  }
}

Output

inside addBonus: 150
after addBonus: 50
scores[0] after zeroFirst: 0

addBonus received a copy of the number 50, so raising it to 150 changed only the copy and the caller still sees 50. The parameter died at the closing brace of the method.

zeroFirst received a copy of the array reference, but that copy points at the same array on the heap, so writing through it is visible to the caller. Nothing here contradicts the other case: in both, a copy of the variable was passed.

Scope, where a name is alive

A variable's scope is the region of code where its name means something, running from its declaration to the closing brace of the block it was declared in. The rule exists so names cannot collide across a large program, and the i in one method is a stranger to the i in another.

for (int i = 0; i < 3; i++) {
  int doubled = i * 2;   // born fresh each pass
}
// i and doubled do not exist here, using either is a compile error

Two practical consequences follow.

A loop variable dies at the loop's closing brace. When the result is needed afterwards, declare it before the loop, which is exactly what the running-maximum pattern in lesson 3-3 did.

A method's parameters are local to that method. The score inside addBonus above is a different variable from the score in main even though the names match, which is the real reason the copy rule felt surprising.

Mutating through a copied reference

A method that receives an ArrayList<String> parameter, a resizable list covered properly in unit 7, and calls list.add("x") on it does affect the caller. The caller's list now contains "x".

Pass-by-value copied the reference, not the object. The caller's variable and the parameter both point at one ArrayList on the heap, so adding through either one is visible through both.

Reassigning the parameter is the case that stays invisible:

static void wipe(ArrayList<String> list) {
  list = new ArrayList<>();   // re-points the copy only
}

static void clear(ArrayList<String> list) {
  list.clear();               // empties the shared object
}

wipe changes where the local copy points and the caller notices nothing. clear reaches through to the shared object, and the caller's list comes back empty.

Doubling an array in place

A void method that modifies the caller's array through the shared reference.

public class Main {
  static void doubleAll(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
      nums[i] = nums[i] * 2;
    }
  }

  public static void main(String[] args) {
    int[] nums = {3, 5, 8};
    doubleAll(nums);
    for (int n : nums) {
      System.out.println(n);
    }
  }
}

Output

6
10
16

The classic for is required here because writing back needs the index. nums[i] = nums[i] * 2 reads a slot and stores into the same slot, and i < nums.length keeps it inside the array.

An enhanced for (int n : nums) would not work for this, because n is a copy of the element and assigning to it changes only the copy. That is pass-by-value again, one level down, and it is why the two loop forms are not interchangeable.