Parameters and Pass-by-Value

Understand that Java always copies arguments, why a method can change an object you pass but never your variable, and when to copy defensively.

Syntaxvoid method(int count, List<String> items) { count = 5; // caller's int unchanged items.add("x"); // caller's list changed items = List.of(); // caller's variable unchanged }

Java has exactly one way of passing arguments: by value. When you call a method, each parameter gets a copy of the value of the argument. What that copy means depends on the type.

Primitives: the number is copied

For int, long, boolean and the other primitives, the parameter is a fresh copy of the number. Changing it inside the method has no effect on the caller's variable.

Objects: the reference is copied

A variable of a class type such as List<String> holds a reference to an object, not the object itself. Passing it copies the reference. Now the caller and the method both point at the same object, which leads to two different outcomes:

  • Calling a mutating method through the parameter (tags.add("x")) changes the shared object, so the caller sees it.
  • Assigning a new object to the parameter (tags = new ArrayList<>()) only changes the method's copy of the reference. The caller still points at the old object.

People often say "objects are passed by reference" in Java. They are not: if they were, reassigning the parameter would change the caller's variable, and it never does. This is why a classic swap(a, b) method cannot be written in Java.

Why this matters in services

The bug that bites back-end code is the second-hand mutation. A service receives a list from a controller, sorts or filters it in place, and the controller's copy, or a cached copy shared between requests, changes underneath it. Two habits prevent it:

  • Do not mutate inputs. Return a new list instead of editing the one you were given, unless the method's name makes the mutation obvious.
  • Copy on the way in. When an object keeps a collection it was given, store List.copyOf(items). Later changes by the caller cannot leak in, and the stored list is unmodifiable.

String, records with immutable components, and the boxed types like Integer are immutable, so passing them around is always safe: there is no method that could change them.

final parameters

Marking a parameter final stops the method reassigning it. It does not make the object immutable; final List<String> tags can still have tags.add called on it. Many teams skip final on parameters and instead treat reassigning a parameter as a code smell caught in review.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Store List.copyOf(roles) in a user record so a caller's later edits cannot change who is an admin.
  • Return a filtered copy from a service instead of removing items from a list the controller still uses.
  • Explain to a teammate why a helper that reassigns its String parameter never changed the request's value.

More examples

Why swap cannot work, and what to do instead

A method cannot reassign the caller's variables, so return a new value and let the caller assign it. Returning a small record is the usual way to hand back more than one value.

Example · java
void swap(String a, String b) {
    String tmp = a;
    a = b;
    b = tmp;                      // only the local copies are swapped
}

record Pair(String first, String second) {
    Pair swapped() {
        return new Pair(second, first);
    }
}

void main() {
    String from = "eu-west";
    String to = "us-east";
    swap(from, to);
    IO.println(from + " -> " + to);

    Pair route = new Pair(from, to).swapped();
    IO.println(route.first() + " -> " + route.second());
}
// Output:
// eu-west -> us-east
// us-east -> eu-west

Defensive copies in a record

Without the copy, the order would hold the very same list as the cart and change when the cart changes. List.copyOf also rejects null elements, which catches bad input early.

Example · java
record Order(String id, List<String> items) {
    Order {
        items = List.copyOf(items);   // copy in, and make it unmodifiable
    }
}

void main() {
    List<String> cart = new ArrayList<>(List.of("book", "pen"));
    Order order = new Order("o-1", cart);

    cart.add("laptop");               // the caller keeps editing its own list
    IO.println("cart:  " + cart);
    IO.println("order: " + order.items());

    try {
        order.items().add("mug");
    } catch (UnsupportedOperationException e) {
        IO.println("order items are read-only");
    }
}
// Output:
// cart:  [book, pen, laptop]
// order: [book, pen]
// order items are read-only

Returning a new list instead of mutating the input

The second style is safer in services because the caller's data, which may also be logged or cached, stays exactly as it arrived. When a method must mutate its argument, say so in the name.

Example · java
// Mutates the caller's list: easy to miss at the call site.
void normaliseInPlace(List<String> emails) {
    emails.replaceAll(e -> e.strip().toLowerCase(Locale.ROOT));
}

// Leaves the input alone and returns a new, unmodifiable list.
List<String> normalised(List<String> emails) {
    return emails.stream().map(e -> e.strip().toLowerCase(Locale.ROOT)).toList();
}

void main() {
    List<String> submitted = new ArrayList<>(List.of(" [email protected]", "[email protected] "));

    List<String> clean = normalised(submitted);
    IO.println("submitted: " + submitted);
    IO.println("clean:     " + clean);

    normaliseInPlace(submitted);
    IO.println("submitted: " + submitted);
}
// Output:
// submitted: [ [email protected], [email protected] ]
// clean:     [[email protected], [email protected]]
// submitted: [[email protected], [email protected]]

Discussion

  • Be the first to comment on this lesson.