Before you begin
Why this matters
Most bugs with references come from forgetting that a reference only says where to look. Knowing what a reference holds lets you predict which object a change reaches, and spot a kept number that has started to name the wrong thing.
What this helps you reason about
- Predict whether an assignment changes an object or only which object a variable leads to.
- Explain why Java never shows you an address, and why a list search can miss an equal object.
- Spot a kept number that now names a different item.
An olive stall keeps its olives in tubs. A note that says “the second tub” is not olives; it tells you where to find them. Every Java object is reached through such a note, a reference, but Java never shows you where the object is.
Already know this? The short version
- A variable of a class, interface, or array type holds a reference: assigning to the variable changes which object it leads to, and assigning to a field changes the object.
- Java never exposes an address; the stepper shows an @ id per object. == compares references, and indexOf and contains call equals, which compares references unless the class overrides it (records do).
- The garbage collector keeps an object while a reference leads to it, so references never dangle; a kept reference can still lead to an object that a list no longer holds.
- A handle is a key or an id, or an object such as a FileInputStream that stands for an open file. A number goes stale when it is given to a new item, so give numbers out from a counter that only goes up.
Words in this lesson (7)
- reference
- A value that leads to an object, or null. A variable of a class or array type holds a reference, never the object itself, and copying the reference does not copy the object.
- indirection
- Reaching a value through another value that says where to find it, such as a reference or a pointer. The second value is followed first, and then the value it leads to is used.
- reference type
- A type whose values are references or null: in Java, every class, interface, and array type. int, double, boolean, and the other primitive types are not reference types.
- identity
- Whether two references lead to the same object. Two objects can hold equal fields and still be two objects.
- handle
- A value, often a small number, that stands for something kept elsewhere, such as an open file or an entry in a table. Code gives the handle back to the code that keeps the thing; the handle does not hold the thing itself.
- stale handle
- A handle kept after the thing it named was removed. It can name nothing, or a different thing that took its place.
- null
- A reference that refers to no object. Map.get and System.getenv return null for a name that is not there.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / What it holds
What does a reference hold?
Copying, identity, and aliasing showed two names leading to one object. This section asks what such a name actually holds.
A reference holds where an object is, not the object. Indirection means reaching a value through something that says where it is. A type whose values are references is a reference type: every class, interface, and array type. int and the other primitive types are not.
So an assignment through a reference and an assignment to it do different things. scoop.kg = 3 follows scoop and changes the object it leads to. scoop = black changes only scoop: from then on it leads to the other object, and the first object is not touched.
Tub green = new Tub("green", 4);
Tub black = new Tub("black", 6);
Tub scoop = green;
scoop.kg = 3; // changes the green tub
scoop = black; // changes only scoop
System.out.println(green.kg + " " + black.kg); // 3 6 A reference says where an object is; assigning to it changes which object it leads to, not the object.
Coming from C: A Java reference is like a pointer that you can only follow, copy, and compare: there is no &, no *, and no arithmetic.
Precisely: what the JVM stores
The Java Language Specification says only that reference values are pointers to objects, or null (JLS 4.3.1). HotSpot, the usual JVM, stores them as machine addresses, often compressed to 32 bits, and its garbage collector can move objects and update every reference to them. A program never sees the address, so the move is invisible.
Experiment
Point a reference at a second tub
scoop holds a reference. scoop.kg = … follows it and changes the object it leads to. scoop = black changes only scoop. new Tub(…) makes a third object, which neither tub variable leads to.
Why it matters Assigning to a reference variable changes which object it leads to. Assigning to a field through it changes an object.
Predict both weights. Then change scoop = black; to scoop = new Tub("black", 6); and predict again.
class Main {
// A Tub object holds an olive name and its weight in kilograms.
static class Tub {
String olive;
int kg;
Tub(String olive, int kg) {
this.olive = olive;
this.kg = kg;
}
}
public static void main(String[] args) {
Tub green = new Tub("green", 4);
Tub black = new Tub("black", 6);
Tub scoop = green;
scoop.kg = scoop.kg - 1;
scoop = black;
scoop.kg = scoop.kg - 1;
System.out.println(green.kg + " " + black.kg);
}
}
What to look for
Each new Tub(…) first steps into the constructor. Back in main, green and black show Main$Tub@ followed by two different numbers. That number is the debugger’s id for one object, not a memory address. scoop first shows green’s id, then black’s. The run prints 3 5. After the edit, scoop shows a third id, the last change reaches only that object, and the run prints 3 6.
Expected output
3 502 / What you can do
What can code do with the reference itself?
Java hides the reference itself. You cannot print an address, add to it, or turn it into a number. The stepper shows each object with an @ and a number, such as Main$Tub@49: the debugger’s id for that object, not an address. Code can do three things with a reference: follow it, copy it, and compare it with ==.
Searching a list compares too. List.of(…) makes a list that cannot be changed. Its indexOf and contains call equals on the elements, and the equals that every class inherits from Object compares references, like ==. So a new Tub with equal fields is not found, unless the class overrides equals. A record, such as record Tub(String olive, int kg) {}, gets an equals that compares its fields. Equality and comparison shows how to write equals yourself.
Tub green = new Tub("green", 4);
List<Tub> tubs = List.of(green, new Tub("black", 6));
System.out.println(tubs.indexOf(green)); // 0
System.out.println(tubs.indexOf(new Tub("green", 4))); // -1 You can follow, copy, and compare a reference, but never see where it leads; a list search without equals finds the same object, not an equal one.
Coming from Python: Python’s list.index compares with ==, which Python classes can define; Java’s indexOf compares with equals, which Object defines as ==.
Precisely: identityHashCode
System.identityHashCode(x) returns a number for an object that stays the same while the object lives, and Object’s toString prints it in hexadecimal, as in Main$Tub@1b6d3586. It is not an address: two objects can share it, and it changes from run to run.
Experiment
Look for an equal tub in a list
indexOf calls equals on the elements. Tub does not override equals, so it uses Object’s equals, which compares references like ==. The new Tub is a different object, so it is not found. A record gets an equals that compares its fields.
Why it matters A list search finds an equal object only when the class compares contents in equals; otherwise it finds the same object or nothing.
Predict both positions. Then replace the whole static class Tub { … } with record Tub(String olive, int kg) {} and predict again.
import java.util.List;
class Main {
// A Tub object holds an olive name and its weight in kilograms.
static class Tub {
String olive;
int kg;
Tub(String olive, int kg) {
this.olive = olive;
this.kg = kg;
}
}
public static void main(String[] args) {
Tub green = new Tub("green", 4);
List<Tub> tubs = List.of(green, new Tub("black", 6));
System.out.println(tubs.indexOf(green));
System.out.println(tubs.indexOf(new Tub("green", 4)));
}
}
What to look for
Step past List.of(…). tubs shows its two elements, each Main$Tub@ with its own id; green shows the same id as the first one. The second search first steps into Tub’s constructor, where this shows a third id: a new object. The run prints 0, then -1. After the edit, the run prints 0, then 0.
Expected output
0
-103 / Handles
What is a handle?
A handle is a value, often a small number, that stands for something kept elsewhere. The code that keeps the thing gives out the handle, and looks the thing up when the handle comes back. A key into a Map is a handle: the stall’s HashMap holds tub 2, and a customer keeps only the number 2. tubs.get(2) looks the tub up. A FileInputStream is a handle too: an object that stands for a file the operating system keeps open, until close gives it back.
A handle is not a reference: nothing in the number 2 leads to the tub. It names the tub only while the table says so. If tub 2 is removed and its number is given to a new tub, the customer’s 2 now names the new tub. A handle kept after the thing it named was removed is a stale handle.
So never give a removed thing’s number to a new one: keep a counter that only goes up. tubs.size() + 1 is not such a counter, because the size goes down when a tub is removed. A position in a list is a weak handle for the same reason: removing an earlier element moves every later one. Find an item by its number, not by where it sits in the list.
int lastNumber = 2; // numbers 1 and 2 are already given out
tubs.remove(2); // the black olives sell out
lastNumber = lastNumber + 1;
tubs.put(lastNumber, "kalamata"); // tub 3, never 2 again
System.out.println(tubs.get(2)); // null A handle names a thing only through the table that keeps it; never give a removed thing’s number to a new one.
Experiment
Give a sold-out tub’s number to a new tub
The customer keeps only the number 2. After tub 2 is removed, the map holds one tub, so size + 1 is 2 again, and the new tub gets the old number. The number 2 now names the kalamata tub. The number 3 has never been given out, so after the edit 2 names nothing.
Why it matters A number taken from the current size can repeat. Give out numbers from a counter that only goes up, so an old number finds nothing instead of the wrong tub. get returns null for a number that names nothing.
Predict what the customer’s number finds. Then change int next = tubs.size() + 1; to int next = 3; and predict again.
import java.util.HashMap;
class Main {
public static void main(String[] args) {
HashMap<Integer, String> tubs = new HashMap<>();
tubs.put(1, "green");
tubs.put(2, "black");
int label = 2; // a customer keeps the black tub's number
tubs.remove(2); // the black olives sell out
int next = tubs.size() + 1;
tubs.put(next, "kalamata");
System.out.println(label + " " + tubs.get(label));
}
}
What to look for
Step past each line. tubs shows java.util.HashMap@ and an id, then its entries. Each key shows as a boxed Integer object, such as java.lang.Integer@59 {value: 1}="green": a HashMap stores objects, not ints. The map holds green and black, then one entry after remove, then green and kalamata. label keeps 2 throughout. The run prints 2 kalamata. After the edit, the run prints 2 null.
Expected output
2 kalamata04 / Gone
What happens when the thing it names is gone?
In C, memory can be freed while a pointer still holds its address. Java prevents that. The garbage collector, the part of the JVM that reuses the memory of objects nothing leads to, never reuses an object while a reference leads to it. So a reference never leads to freed memory.
A kept reference can still lead to the wrong object. Below, kept leads to the green tub. tubs.set(0, …) puts a reference to a new object in the list, and kept still leads to the old one. Changes through kept no longer reach the list.
A reference can also be null and lead nowhere. Calling a method or reading a field through it throws a NullPointerException. Absence, zero, and empty values shows how to check first.
List<Tub> tubs = new ArrayList<>();
tubs.add(new Tub("green", 4));
Tub kept = tubs.get(0);
tubs.set(0, new Tub("black", 6));
kept.kg = 1; // changes the old green tub only
System.out.println(tubs.get(0).kg); // 6 A Java reference never leads to freed memory, but a kept one can lead to an object that the list no longer holds.
Coming from C: There is no free and no dangling pointer in Java: an object lives while a reference leads to it.
05 / Try it
Can you explain a fresh example?
Decide for each line whether it changes a Tub or only which Tub scoop leads to. The next lesson, Stack and heap, shows where the values that references and pointers lead to are kept.
import java.util.ArrayList;
import java.util.List;
class Main {
static class Tub {
String olive;
int kg;
Tub(String olive, int kg) {
this.olive = olive;
this.kg = kg;
}
}
public static void main(String[] args) {
Tub green = new Tub("green", 4);
List<Tub> tubs = new ArrayList<>();
tubs.add(green);
Tub scoop = tubs.get(0);
scoop.kg = 3;
scoop = new Tub("green", 3);
System.out.println(green.kg + " " + tubs.indexOf(scoop) + " "
+ tubs.indexOf(green));
}
}
What is printed?
Reveal the reasoning
3 -1 0. scoop starts with the reference stored in the list, which leads to green, so scoop.kg = 3 changes green. scoop = new Tub(…) makes scoop lead to a new object with equal fields. Tub does not override equals, so indexOf(scoop) does not find it and gives -1, while indexOf(green) finds green at 0.
Practice tickets
- REF-101 · The pickup screen shows the wrong dish once the first order is collected → Free
- REF-102 · Two vegetable boxes on the shelf carry the same number → Sign in · Pro
- REF-103 · An old locker tag opens a crate that belongs to someone else → Sign in · Pro