References and aliasing
On This Lesson 11 topics
Lesson Overview
In the last lesson you drew variables as sticky notes on the desk and objects as items in the warehouse. A sticky note holds an address, not the item. That leads to a question: what happens if you write the same address on two notes?
The answer is aliasing: two variables referring to one object. It is not a bug and not an advanced feature. It happens every time you assign an object variable to another, and every time you pass an object to a method. Understanding it explains behaviours that surprise most beginners, such as a list that changes "by itself" or a method that seems to modify its argument.
This lesson uses the picture from lesson 1 and builds directly on BEG-014, where you learned that Java passes arguments by value. We now see exactly what is being copied.
Learning Objectives
By the end of this lesson you will be able to:
- Explain what
b = acopies for primitives and for object variables. - Draw two variables pointing at one object and say how many objects exist.
- Predict how a change made through one alias appears through the other.
- Explain why a method can change the object you pass it but cannot change which object your variable points to.
- Distinguish
==(same object) from.equals(same content, if the class defines it). - Explain what
nullmeans and what happens when you use it.
Written Explanation
Assignment copies what is in the slot
An assignment copies the value held in the right-hand variable's slot into the left-hand variable's slot.
- For a primitive, the slot holds the value, so you get an independent copy.
- For an object variable, the slot holds a reference, so you get a second reference to the same object. No new object is created.
int x = 5; int y = x; y = 6; StringBuilder a = ...; StringBuilder b = a;
x [ 5 ] y [ 6 ] a [ ●---+
+--> StringBuilder("hi")
b [ ●---+Changing y cannot affect x, because they hold separate copies. Changing the object through b affects what you see through a, because there is only one object.
Aliasing and mutation
Mutation means changing an object's contents after it is created (for example append, add, or assigning a field). If an object has two aliases, a mutation through either is visible through both. This is the central idea of the lesson.
Two different actions are easy to confuse:
| Action | What changes | Effect on the other alias |
|---|---|---|
b.append("!") (mutate the object) | The object on the heap | Visible through a |
b = new StringBuilder("other") (reassign the variable) | Only b's slot | None: a still points to the old object |
Reassignment moves one arrow. Mutation changes the thing the arrows point to.
Passing to a method
BEG-014 said Java is pass-by-value. For objects, the value being copied is the reference. When you call addItem(list), the parameter l is a new slot in the new frame, holding a copy of the reference. So the caller's list and the callee's l are aliases for the duration of the call.
Two consequences, both consistent with the official Java Tutorials:
- The method can mutate the shared object, and the caller sees it after the method returns.
- The method cannot change which object the caller's variable refers to. If it reassigns its parameter, only the callee's slot moves.
Primitives follow the same rule of copy-by-value, but the copy is the whole value, so the method has nothing shared to change. This is why a method that does n++ on an int parameter never changes the caller's variable.
== versus .equals
For object variables, == asks "do these two references point to the same object?". It does not look inside the objects. Two separate objects with equal contents are == false.
.equals asks whether two objects count as equal, and each class decides what that means (BEG-026 covered the contract). Many JDK classes such as String compare content. Not every class does: StringBuilder does not override equals, so on a StringBuilder it behaves like ==. When you want to compare text in a StringBuilder, convert it with toString() first.
null
null is a reference value meaning "no object". A slot holding null has no arrow. Comparing with null using == is safe and common. Calling a method on a null reference fails at runtime with a NullPointerException, because there is no object at the end of the arrow to run the method on (BEG-037 covered why this matters and how Optional helps).
Aliasing is useful, and sometimes dangerous
Sharing is what makes object-oriented programs efficient: a thousand method calls can use one list without copying it. The danger is accidental sharing: a class that hands out its internal list lets any caller change it, which breaks the encapsulation you studied in BEG-021. When you want an independent object, you must create one explicitly, for example new ArrayList<>(original) copies the elements into a new list. (That copy is shallow: the elements themselves are still shared. We will not go deeper here.)
Examples
Example 1: primitive copy versus object alias (simple)
int x = 5; int y = x; y = 6;
StringBuilder a = new StringBuilder("hi");
StringBuilder b = a;
b.append("!");
System.out.println(x + " " + y + " " + a + " " + b + " " + (a == b));Output:
5 6 hi! hi! truex is still 5 because y held its own copy. a shows hi! although we only touched b: one StringBuilder object, two references. a == b is true because they are the same object.
Example 2: passing a list to a method (practical)
static void addItem(List<String> l) { l.add("z"); }
static void replace(List<String> l) { l = new ArrayList<>(); l.add("new"); }
static void bump(int n) { n++; }
List<String> list = new ArrayList<>();
addItem(list);
System.out.println(list);
replace(list);
System.out.println(list);
int n = 1; bump(n);
System.out.println(n);Output:
[z]
[z]
1addItemmutated the shared list, so the caller sees[z].replacemoved its own parameter slot to a new list and added to that. The caller's list is unchanged, so it still prints[z].bumpincremented a copy. The caller'snis still1.
The picture during replace, after its first line:
main: list [ ●-----> ArrayList ["z"]
replace: l [ ●-----> ArrayList ["new"] (a different object)Example 3: a leaky getter and shallow vs deep equality (realistic)
static class Cart {
private final List<String> items = new ArrayList<>();
void add(String s) { items.add(s); }
List<String> getItems() { return items; }
}
Cart cart = new Cart();
cart.add("pen");
List<String> view = cart.getItems();
view.clear();
System.out.println(cart.getItems().size());
StringBuilder s1 = new StringBuilder("hi"), s2 = new StringBuilder("hi");
System.out.println((s1 == s2) + " " + s1.equals(s2) + " " + s1.toString().equals(s2.toString()));
String missing = null;
System.out.println(missing == null);
try { missing.length(); } catch (NullPointerException e) { System.out.println("NPE"); }Output:
0
false false true
true
NPE- The cart's
itemsfield isprivate, yetgetItems()returned the reference itself, soviewis an alias andview.clear()emptied the cart. The fix is to return a copy:return new ArrayList<>(items);. s1 == s2isfalse(two objects) ands1.equals(s2)is alsofalsebecauseStringBuilderdoes not compare content. Comparing theStringforms givestrue.- The last lines show
nullhandling: comparing is safe; using it throws.
Backend Insight
Aliasing is the root of many real bugs in backend code:
- Shared mutable objects. A cached configuration or lookup list is stored once and handed to every request. If one request modifies it, every other request sees the change. Teams avoid this by returning copies or immutable objects (BEG-021 covered immutable classes).
- Services mutating the objects you pass them. A method called
calculateTotal(order)that quietly sortsorder.getLines()has changed the caller's order. Good APIs document, or avoid, such side effects. - Entities and DTOs. In a typical layered application, the same object may be referenced from several layers. Knowing that you hold a shared reference, not a copy, helps you predict when data can change underneath you.
A useful habit when reviewing code: ask of each object, "who else holds a reference to this?"
Common Mistakes
Mistake 1: thinking assignment copies the object
What goes wrong: List<String> backup = list; is written as a "backup", but later changes to list also change backup.
Fix: copy explicitly: new ArrayList<>(list).
Mistake 2: expecting a method to reassign the caller's variable
What goes wrong: a method does p = new Person(); on its parameter and the caller is surprised that nothing changed.
Fix: return the new object and assign it in the caller: p = makePerson();. A method can only mutate the shared object.
Mistake 3: using == to compare content
What goes wrong: == is false for two equal-looking objects.
Fix: use .equals for objects whose class defines it, and keep == for primitives and for "same object?" checks.
Mistake 4: assuming every .equals compares content
What goes wrong: StringBuilder.equals is identity-based, so it is false for equal text.
Fix: check the class's documentation, or compare toString() results.
Exercises
Level A: Foundation
- After
int[] p = {1, 2}; int[] q = p; q[0] = 9;, what isp[0]? How many arrays exist? - After
Person a = new Person("A"); Person b = a; Person c = new Person("C");, how manyPersonobjects exist and how many references? - For
String s1 = new String("a"); String s2 = new String("a");, predicts1 == s2ands1.equals(s2).
Level B: Application
- Given
static void rename(Person p) { p.name = "Zed"; }andstatic void swap(Person a, Person b) { Person t = a; a = b; b = t; }, withx = new Person("Ann")andy = new Person("Bob"), what dox.nameandy.namehold afterrename(x); swap(x, y);? - A class has
private final List<String> tagsand a getter that returnstags. Show how to change the getter so callers cannot change the internal list through it. - Why does
int n = 1; bump(n);never changen, butaddItem(list)changeslist? Answer in two sentences using the word "copy".
Level C: Challenge
- Trace and write the final output:
List<String> a = new ArrayList<>();
List<String> b = a;
a.add("one");
b = new ArrayList<>(a);
b.add("two");
a.add("three");
System.out.println(a + " " + b);Draw how many list objects exist at the end and which variable points to which.
Show solutions
9. One array exists, with two references (pandq).- Two
Personobjects (AandC); three references (a,b,c), withaandbaliases. s1 == s2isfalse(two distinct objects created bynew);s1.equals(s2)istrue(same text).x.nameis"Zed"andy.nameis"Bob".renamemutated the shared object.swaponly exchanged its own parameter slots, soxandyin the caller still point to the same objects as before. Verified by running the code (Zed Bob).public List<String> getTags() { return new ArrayList<>(tags); }. Now callers get a copy and cannot change the internal list.- Both pass a copy:
bumpreceives a copy of the number, so the caller's number is untouched.addItemreceives a copy of the reference, and both references point to the same list, so a change made through the copy is visible through the original. - Output:
[one, three] [one, two]. Two list objects exist at the end:apoints to the first (one,three) andbwas moved to a new list (one,two). Afterb = new ArrayList<>(a)the two variables were no longer aliases. (Verified by running it.)
Mini Challenge
Write a small class Playlist with a private List<String> songs, an add(String) method, and a getSongs() method. First write getSongs() the naive way, then write a main that shows an outside caller emptying the playlist by calling clear(). Then fix getSongs() so the same main leaves the playlist intact. Draw the stack and heap for both versions in one sentence each.
Summary & Next Lesson
Key takeaways
b = acopies the slot: a value for primitives, a reference for objects. No new object is created.- Two references to one object are aliases; mutation through one is visible through the other.
- A method receives a copy of the reference: it can mutate the shared object, but it cannot repoint the caller's variable.
==compares references;.equalscompares content only if the class defines it.- To get an independent object, copy it explicitly.
Next lesson
Next is Garbage collection, gently: when an object that nobody points to any more is cleaned up, and why you never free memory yourself.
Self-check
- After
Person b = a;how many objects exist because of that line? - What is the difference between mutating an object and reassigning a variable?
- Can a method change which object the caller's variable refers to?
- What does
==compare on two object variables? - What happens if you call a method on a
nullreference?
Show answers
- None; the line creates only a second reference.
- Mutation changes the object's contents and is seen by every alias. Reassignment changes which object one variable points to and is seen by no one else.
- No. The method holds a copy of the reference, so it can only mutate the shared object.
- Whether they refer to the same object.
- A
NullPointerExceptionis thrown, because there is no object to call the method on.
References
- The Java Tutorials: Passing Information to a Method: verified that reference arguments are passed by value, that changes to the object's fields persist after the call, and that reassigning the parameter has no lasting effect.
- Java Language Specification, section 15.21.3 (reference equality): the page loaded, but the summarising tool did not reach that section, so my statement that
==tests identity of objects was not verified from this page; I did confirm the behaviour by running the code with JDK 21. - All code was compiled and run with JDK 21 and the output copied. The claim that
StringBuilderdoes not overrideequalsis confirmed by the observedfalseoutput, not by a Javadoc page I fetched.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.