Phase 1 · Objects

A variable never holds an object

By the end of this page you'll be able to draw exactly where every value in a Java program is sitting — and that one drawing settles the three arguments beginners have most often.

⏱ ~75 minutes🧩 Needs: all of Phase 0💻 Code: src/

Write your friend's address on a sticky note. Now copy it onto a second note. You have two notes. Your friend still has one house. Cross out the address on the second note and write a different one instead. Nothing at all happened to the house. That plodding little paragraph is the most useful idea in Java, and hardly anybody gets told it plainly.

Almost every early Java confusion is secretly one confusion wearing different hats. A value changes when you swear you never touched it. Two words that look identical refuse to count as equal. Someone on the internet insists Java passes things "by reference" and someone else insists it doesn't.

All three dissolve the moment you know what a variable actually holds. So that's what we're going to nail down, with programs you run yourself.

Fair warning about one thing coming later. This chapter contains a result where Java appears to decide that 100 equals 100 but 1000 doesn't equal 1000. It's not a bug, and it's not random. You'll find the exact cut-off point by hand.

What you'll be able to do

Section 1The blank form and the filled-in copies

Everything you've stored so far has been small. A number. A single letter. A true or false. Text, which was a bit special, and we'll come back to that.

But real programs need to store things. A dog with a name. A player with a score and three lives. An order with a date and a price. You need one bundle that holds several values at once and travels around as a single item.

Think of it like this

At a doctor's front desk there's a stack of blank forms. Every blank has the same printed lines on it — name, date of birth, phone number. The blank form itself isn't a patient. It's the shape a patient's details will take. Then a hundred people fill one in each, and now there are a hundred filled-in copies. Same printed lines, different details on every one.

The blank form is a class — a description of what shape something has. A filled-in copy is an object — one actual thing built to that shape, with its own values in it.

You already know the word class. You've typed it at the top of every file since Chapter 0.1. Back then it was scenery, a word you needed so the file would compile. Now it starts doing its real job.

Here's the smallest useful one. It's the Dog that all three labs in this chapter are built on.

the Dog class — it lives inside each lab file
static class Dog {
    String name;
    Dog(String name) { this.name = name; }
}

Four lines. All four matter, so let's take them one at a time.

Line 1, static class Dog {, announces the blank form and names it Dog. The { opens a box, and everything up to the matching } describes the shape. The word static is there because this small class is tucked inside another file. Take it off and main won't compile: it can't build a Dog without first building the whole outer class. What static really means is the subject of 1.2's Section 6, "static, finally".

Line 2, String name;, is one printed line on the form. Every Dog object gets its own box called name, holding text. A variable that belongs to an object like this has its own word: a field. That's the whole difference from the variables you've written before. Your old ones lived inside a method. A field lives inside an object.

Hold on to the words its own box, because that's the part people drop. A field is not one shared box that every dog reads from. Every field gets its own box inside its own object. Ten dogs means ten separate name boxes, and writing in one leaves the other nine alone.

Line 3 is a constructor. That's the bit of code that runs once, while a new object is being built. Its job is to fill in the fields. You can spot one by its shape. It's got the same name as the class, and no return type in front.

Now the one line inside it, and this one deserves a full stop. this.name = name; has two things called name in it. That breaks the pattern you're used to. Until now, one name has always meant one box.

Here it means two boxes, doing two different jobs. So here they are, side by side.

The parameter name — what was handed in

name = "Rex"
read out of here … →

this.name — the object's own field

this.name = "Rex"
… and written in here

Two boxes, one arrow, one direction. The left box belongs to the constructor. Whoever asked for a new dog handed "Rex" in, and it landed there. It goes away when the constructor finishes.

The right box belongs to the dog. It's the field from line 2, one box inside this one object. It lasts as long as the dog does.

So which box does the bare word name mean? Java picks the nearer one, which is the parameter. And this means "the object being built right now". So this.name is the only way to say "the dog's own box".

Read the line right to left, like every assignment you've written. Take what's in the parameter. Put a copy of it in the field. One box gets read and the other gets written.

That is an honest one-line meaning of this, and it's all you need today. Leave the this. off and the dog comes out nameless. Here's that happening for real.

Going deeper What happens if you forget the this.

You don't have to take my word for it. src/ShadowedName.java is the same class with the this. removed.

src/ShadowedName.java — the broken constructor
Dog(String name) { name = name; }   // no "this." — this is the bug
what actually happens (verified, JDK 17)
$ java ShadowedName
field name is: null

javac compiles that without a word of complaint. It's a legal line. It copies the parameter onto itself, and the field is never mentioned.

A field nobody wrote into starts out empty, and for text that means null. Section 6 is entirely about that value. The symptom is a dog with no name, hundreds of lines away from the constructor that caused it.

this has a second job as well. It names the whole object, not just one of its fields. You'll use it that way in Chapter 1.2, writing your own .equals.

Line 4, the closing }, ends the class. That's the form printed. Nothing has been filled in yet.

What new does

Phase 0 mentioned new once and walked away from it. Time to pay that up.

making one dog
Dog a = new Dog("Rex");

Read it right to left, because that's the order things happen in.

new Dog("Rex") does two jobs. First, new asks Java to set aside enough memory for one fresh Dog object. One blank form, pulled off the pile. Then the constructor runs on it, with "Rex" as the handed-in text. So the object's name field ends up holding "Rex".

Then Dog a = on the left does three things. It makes a variable called a. It promises that a will only ever deal with Dog objects. And it puts the result of all that into the box.

And that last bit is where every beginner's mental picture goes wrong. What exactly went into a?

Not the dog. Hold that thought for one section, because we need to look at where things live first.

In Dog(String name) { this.name = name; }, which box does that one line write into?

Section 2Two places, not one

Back in Chapter 0.4 you met the spike of plates. Every time a method gets called, Java writes that method's variables onto a fresh plate. Then it drops that plate on top of the spike. When the method finishes, the top plate comes off and everything on it goes away. That spike is the call stack, and one plate is a frame.

Frames are brilliant at what they do. They're also completely wrong for objects, and it's worth seeing why.

A frame is thrown away the instant its method finishes. That's the point of it. But objects usually need to outlive the method that made them. You build a dog in one method and hand it to three others. If the dog lived on the plate, it would be gone before the second method ever saw it.

So objects don't live on the spike. They live somewhere else. It's a big open area of memory that Java keeps for exactly this. No plates, no automatic tidying. That area is called the heap. Every single object your program ever makes is built on the heap. It stays there for as long as anything still needs it.

Think of it like this

The heap is the cloakroom at a big venue. You hand over your coat and you get a small numbered ticket. The coat is in the back, on a rail, out of sight. The ticket is in your pocket. The ticket isn't the coat, and it isn't a small coat. It's a thing that finds the coat.

That ticket is the answer to the question we parked. A variable of object type holds a reference. That's a small value, and its only job is to locate an object on the heap. Never the object itself.

So Dog a = new Dog("Rex"); makes a dog on the heap. Then it puts a ticket for that dog in the box named a. And a itself is a local variable. So its box sits on the plate, like every local variable you've ever written.

Be precise about fields

"Local variables live in a frame" is the rule, and the word local is load-bearing. A field is a variable too. But a field doesn't get a plate of its own. It sits inside its object, on the heap, for exactly as long as that object does. Each object carries its own box for every field the class lists. So a is on the plate and the dog's name field is on the heap, even though both are variables.

One more thing about references, and this is the bit that makes Java pleasant. You never see the ticket number. Java won't show it to you, and there's no way to do arithmetic on it. Try to print a reference and you get a made-up label instead of an address. In Phase 3 we write C. There the ticket numbers sit right in your hands. You can add 4 to one by accident. You'll appreciate this chapter more afterwards.

Going deeper What a reference actually is, under the metaphor

The ticket is a fair picture, and a picture is all it is. Here's the machinery underneath.

Every byte of memory your program can touch has a number, called its address. An address is only a position, the way a house has a number on a street. An object sits at some address on the heap.

A reference is that address, held as one plain number. Following a dot means reading the number and going to that spot in memory. There's no magic in the ticket. A reference is a value like any other value.

Two things stop you seeing it. Java won't let you read the number or do arithmetic on it. And Java is allowed to move an object while tidying memory, then correct every reference that pointed at it. A number you'd copied out yourself would go stale.

The exact shape is not fixed by the JVM specification, so it varies. Take HotSpot, the usual JVM, on a 64-bit machine. If the heap is small enough it stores a shrunken 32-bit value and scales it up. That saves memory over a full address, and you can see the switch on your own machine.

verified on this machine, JDK 17
$ java -XX:+PrintFlagsFinal -version | grep UseCompressedOops
     bool UseCompressedOops                        = true                           {product lp64_product} {ergonomic}

"Oops" there means "ordinary object pointers", which is the JVM's own word for references. Phase 4 opens the JVM up properly. Phase 3 hands you real addresses in C, where nothing corrects them for you.

Worth remembering

Inside main you write Dog a = new Dog("Rex");. What is actually in the box named a?

Section 3Lab 1 — two names, how many dogs?

Time to make Java prove all of that. Open src/Lab1_TwoNames.java and read the main method.

src/Lab1_TwoNames.java — inside main
Dog a = new Dog("Rex");
Dog b = a;                 // what exactly got copied here?

b.name = "Bruno";
System.out.println("A: " + a.name);

b = new Dog("Tommy");   // and what changed here — the object, or b?
System.out.println("B: " + a.name);
System.out.println("C: " + b.name);

System.out.println("D: " + (a == b));

Line by line, before you guess. Line 1 builds a dog named Rex on the heap and puts a ticket for it in a. Line 2 copies whatever is in a into a new box called b. Line 4 follows b's ticket to an object and writes "Bruno" into that object's name field. Line 5 prints what a's dog is called.

Line 7 builds a second dog, named Tommy, and puts its ticket into b, replacing what was there. Lines 8 and 9 print both names again. Line 11 asks a == b, which on object types asks one narrow question. We'll take that question apart properly in Section 5.

Now commit to an answer. Four lines of output, and the interesting one is A. Write your guess down before you press the button. A guess you only kept in your head is one you can lie to yourself about.

Predict before you run — Lab 1

What do the four lines print? The one to think hardest about is A. You changed b.name, and then you asked a what its dog is called. Also: how many Dog objects exist by the last line?

what actually happens (verified, JDK 17)
$ java Lab1_TwoNames
A: Bruno
B: Bruno
C: Tommy
D: false

A prints Bruno. You never mentioned a on that line. If that felt wrong, your picture had two dogs in it, and there was only ever one.

Dog b = a; copied the ticket, not the coat. Two boxes, two identical tickets, one dog in the cloakroom. So b.name = "Bruno" and a.name = "Bruno" are two ways of writing on the same object.

Then line 5 does something different in kind. b = new Dog("Tommy") builds a brand new dog and overwrites b's box with a ticket for it. Nobody wrote on the Bruno dog. Nobody told a anything. So B is still Bruno and C is Tommy.

And D is false because a and b now hold tickets for two different objects. Three dogs got named in this program, and two objects exist at the end. Rex never existed as a separate dog — that was the same object, before it got renamed.

That result is worth watching happen rather than reading about. Step through it.

Lab 1 in memory, one line at a time

Stack — main's plate

(empty)

Heap — the objects

nothing here yet

Step 1 — the program starts. Java drops one plate on the spike, for main. No lines have run, so no boxes on it yet. The heap is empty. Your program has not made a single object.

Stack — main's plate

a = → dog #1

Heap — the objects

dog #1 name = "Rex"

Step 2 — Dog a = new Dog("Rex"); Two things happened, in this order. A dog object appeared on the heap with "Rex" in its name field. Then a box called a appeared on the plate, holding a ticket that finds it.

Stack — main's plate

a = → dog #1
b = → dog #1

Heap — the objects

dog #1 name = "Rex"

Step 3 — Dog b = a; Here it is. A second box on the same plate, holding a copy of the ticket. The heap didn't change. No second dog got built. Count the objects on the right: one. Count the arrows on the left: two.

Stack — main's plate

a = → dog #1
b = → dog #1

Heap — the objects

dog #1 name = "Bruno"

Step 4 — b.name = "Bruno"; The dot means "follow the ticket". Java reads b, finds dog #1, and writes into that object's own field. Nothing on the plate moved. But a's arrow points at the changed object, so line A prints Bruno.

Stack — main's plate

a = → dog #1
b = → dog #2

Heap — the objects

dog #1 name = "Bruno"
dog #2 name = "Tommy"

Step 5 — b = new Dog("Tommy"); A second object at last. And notice what changed on the plate: only b's box. There's no dot on this line, so no ticket got followed and no object got written into. Assigning to b only ever changes b.

Stack — main's plate

a = → dog #1
b = → dog #2

Heap — the objects

dog #1 name = "Bruno"
dog #2 name = "Tommy"

Step 6 — a == b. This compares the two boxes on the plate, and only those. One holds a ticket for dog #1. The other holds a ticket for dog #2. Two different tickets, so the answer is false. Both dogs happen to be alive and well.

Stack — main's plate

a = → dog #1
b = → dog #2

Heap — the objects

dog #1 name = "Bruno" · unreachable
dog #2 name = "Tommy" · unreachable

Step 7 — main ends. The plate comes off the spike, and both boxes go with it. Both dogs are still sitting on the heap. But nothing points at either one any more, so Java is free to reclaim that memory later. Phase 4 is where we look at who does the reclaiming, and when.

The one-line version

b.name = ... reaches through the ticket and changes the coat. b = ... changes the ticket in your pocket. A dot means "go to the heap". No dot means "stay on the plate". Nearly every mystery in this chapter is that one distinction.

Section 4Java is always pass-by-value. Always.

Here's the argument that never dies. Someone says Java passes objects by reference, because a method can change your object. Someone else says Java is pass-by-value, because Java is pass-by-value. Both sound right, so it goes on forever.

It's settleable, and Lab 2 settles it. Open src/Lab2_PassByValue.java. There are three tiny methods above main.

src/Lab2_PassByValue.java — the three methods
static void swap(Dog x, Dog y) {
    Dog tmp = x;
    x = y;
    y = tmp;
}

static void rename(Dog x) {
    x.name = x.name.toUpperCase();
}

static void grow(int n) {
    n = n * 100;
}

swap takes two dogs. It stashes x in tmp, points x at y's dog, then points y at the stashed one. Classic three-line swap, and it has no dots in it anywhere.

rename takes one dog. x.name.toUpperCase() reads the object's name and makes a new all-capitals piece of text from it. Then the x.name = part writes that back into the object's field. That line has dots in it.

grow takes a plain int and multiplies it by 100. No objects involved at all, which makes it the control experiment.

src/Lab2_PassByValue.java — inside main
Dog first = new Dog("Rex");
Dog second = new Dog("Bruno");

swap(first, second);
System.out.println("E: " + first.name + ", " + second.name);

rename(first);
System.out.println("F: " + first.name);

int count = 7;
grow(count);
System.out.println("G: " + count);

String s = "rex";
s.toUpperCase();
System.out.println("H: " + s);

Lines 1 and 2 build two dogs. Line 4 calls swap on them, line 5 prints both names. Line 7 calls rename on the first, line 8 prints its name. Then count is set to 7, grow is called on it, and line 12 prints it.

The last three lines are the trap. s holds the text "rex". Then s.toUpperCase() is called and the result is thrown away — nothing catches it. Then line H prints s. You met toUpperCase in Chapter 0.6, so you have everything you need here.

I'd bet on you getting at least one of these wrong. So would anyone who's taught it. Two of these methods look like siblings. One does something the caller can see, and one's a total no-op.

Predict before you run — Lab 2

Four lines: E, F, G, H. For each one, say what prints and whether the method managed to affect main. Extra credit for E and F together — why does one of them stick and the other not?

what actually happens (verified, JDK 17)
$ java Lab2_PassByValue
E: Rex, Bruno
F: REX
G: 7
H: rex

E did nothing. The dogs are in the order you left them. swap ran, all three of its lines ran, and main can't tell.

Because swap got copies of your two tickets, on its own plate. It shuffled its own copies around beautifully. Then its plate came off the spike and took them with it. Your boxes were never involved.

F worked. rename also got a copy of a ticket. But a copy of a ticket still finds the same coat. It followed the ticket to the heap and wrote in the object. That change is in the object, not on any plate, so it survives the method ending.

G is 7, which probably surprised nobody — this is the same story with no heap in it. grow got a copy of the number 7 and multiplied its own copy.

H is lowercase, and it catches people every single time. toUpperCase doesn't shout at your text. It builds a new piece of text and hands it back. Line H throws that gift away without opening it. Chapter 0.6 told you this returns a new String, and here's the bill for forgetting it.

Now the argument, settled. Pass-by-value means this. When you call a method, each thing you hand it gets copied. The copy goes into a fresh box on the new plate. Pass-by-reference would mean something else. The method would get your actual box, and could overwrite it from the inside.

Java always does the first one. Every time, for everything. What gets copied for an object is the reference — the ticket, not the coat.

So the people arguing are both half right and both using the wrong words. Java passes references, by value. That's why rename can rename your dog and swap can never replace it. E and F are the proof, and they're on your screen.

Why swap can't work

Stack — top plate first

main first → #1 · second → #2

Heap

#1 name = "Rex"
#2 name = "Bruno"

Step 1 — before the call. One plate, two boxes, two tickets. Two dogs on the heap. This is the state we want swap to change.

Stack — top plate first

swap x → #1 · y → #2 · tmp = ?
main first → #1 · second → #2

Heap

#1 name = "Rex"
#2 name = "Bruno"

Step 2 — swap(first, second). A fresh plate goes on the spike. Your two tickets get copied into its boxes x and y. There are now four tickets in this program and still two dogs.

Stack — top plate first

swap x → #1 · y → #2 · tmp → #1
main first → #1 · second → #2

Heap

#1 name = "Rex"
#2 name = "Bruno"

Step 3 — Dog tmp = x; Another copy of the same ticket, in a third box on the top plate. Look at the bottom plate. It hasn't moved and it won't move.

Stack — top plate first

swap x → #2 · y → #1 · tmp → #1
main first → #1 · second → #2

Heap

#1 name = "Rex"
#2 name = "Bruno"

Step 4 — x = y; then y = tmp; The swap has genuinely worked. On the top plate. x and y are now the other way round, exactly as asked. Nothing in the heap was touched, because there was never a dot.

Stack — top plate first

swap x → #2 · y → #1 · tmp → #1
main first → #1 · second → #2

Heap

#1 name = "Rex"
#2 name = "Bruno"

Step 5 — swap returns. The plate comes off, and all three of its boxes go with it. The swap is deleted along with the plate that held it. main prints Rex, Bruno and never knew a thing.

Stack — top plate first

rename x → #1
main first → #1 · second → #2

Heap

#1 name = "REX"
#2 name = "Bruno"

Step 6 — now compare rename. Same copied ticket, same disposable plate. But x.name = ... has a dot, so the change lands on the heap instead of on the plate. When this plate goes, the change stays. That's the entire difference.

You want a method that really does swap two dogs, so the caller sees it. What's your best move?

Worth remembering

Section 5Same object, or merely equal?

Chapter 0.6 gave you a rule and a promise. The rule: compare text with .equals, never with ==. The promise: Phase 1 explains why. This is that.

Start with what == means on an object type. One narrow question, and it isn't the one you'd hope for.

a == b asks: are these two boxes holding tickets for the same single object? That's all. It doesn't look inside anything. It doesn't compare names or numbers. It compares the tickets.

Think of it like this

Two people hold identical twenty-pound notes. Are those the same note? No — there are two notes, and either one buys the same lunch. "Is it the same note" and "is it worth the same" are different questions. Asking the first when you meant the second is the bug.

Meanwhile .equals isn't an operator at all. It's a method, written by whoever wrote the class, answering whatever that author decided "equal" ought to mean. The author of String decided it means "same letters in the same order". A different author could decide something else entirely.

So there's no general "comparing objects" in Java. There's same one, and there's whatever this class's author wrote. Chapter 1.2 is where you write your own.

And on primitives — int, double, boolean, char — there are no tickets involved anywhere. The box holds the value itself. So == compares the actual values, which is why it behaves so obediently on numbers and so strangely on objects. Same operator, two different situations.

Lab 3, and the two surprises inside it

src/Lab3_EqualsVsIdentity.java — inside main
String a = "hello";
String b = "hello";
String c = new String("hello");

System.out.println("I: " + (a == b));
System.out.println("J: " + (a == c));
System.out.println("K: " + a.equals(c));

Integer x = 100, y = 100;
Integer p = 1000, q = 1000;
System.out.println("L: " + (x == y));
System.out.println("M: " + (p == q));
System.out.println("N: " + p.equals(q));

Lines 1 and 2 make two String variables from the same typed-out text. A piece of text typed straight into your code like that is called a literal. Line 3 makes a third one, using new String("hello"). Same letters, built the long way round, with an explicit new.

Lines 5, 6 and 7 ask three questions about those three. Is a the same object as b? Is a the same object as c? Does a hold the same letters as c?

Then line 9 introduces something new. Integer with a capital I isn't the same as int. It's a real class. An Integer variable holds a ticket, and the object it finds has a number inside it. Java lets you write Integer x = 100; and quietly builds that object for you. Why anyone wants an object version of a number is a Phase 2 story. That's when you'll build things that can only hold objects.

Line 10 does the same with 1000. Lines 11, 12 and 13 ask the same three-way comparison as before. Six answers, all true or false.

Take your time on this one. Two of the six catch nearly everybody, including people who've shipped Java for years. Getting them wrong here is a much better deal than getting them wrong in a code review.

Predict before you run — Lab 3

Six answers, I through N. Write down all six. The pair worth staring at is L and M — same code shape, one small number and one big one.

what actually happens (verified, JDK 17)
$ java Lab3_EqualsVsIdentity
I: true
J: false
K: true
L: true
M: false
N: true

Read L and M together. 100 == 100 is true. 1000 == 1000 is false. Identical code, opposite answers, and Java isn't being random.

If you predicted false for I, that was a smart wrong answer. You applied the rule from 0.6 honestly. The next two sections explain why the rule is still right even though I came out true.

K and N are the calm ones. Both .equals calls say true, because both authors defined equal as "same contents". That's the point of .equals — it answers the question you usually meant.

Why two identical literals are the same object

Text is everywhere in programs. The same short piece of it shows up hundreds of times. Building a separate object for every copy would waste real memory.

So Java keeps a shared cupboard of text, one shelf per distinct piece. It's called the String pool. When javac sees a literal like "hello", it checks the pool. If those letters are already on a shelf, your variable gets a ticket for the one that's there. Only if they're new does a new object get built.

That's I. Lines 1 and 2 both said "hello", so both got a ticket for the one pooled object. Two boxes, one object, and == honestly reports true.

Then new String("hello") in line 3 does something different. You wrote new, and new means "build me a fresh object, right now". Java obeys. It skips the cupboard entirely and puts a brand new String on the heap. Same letters, different object. That's J being false.

Why 0.6's rule still stands

You might think I means == is fine for text after all. It isn't, and here's the trap. Pooling only happens for literals that javac can see in your source. Text built while your program runs is a fresh object every time. Read from a file, typed into a Scanner, glued together from pieces — all fresh. So == on text works in tiny test programs and fails on real input. That's the worst possible way for something to be broken.

The argument you'll actually have about this

One day you'll flag this in someone's code and they'll push back. Their line will be: "but == works, my tests pass." Here's the thing. They are not lying to you.

Their tests really do pass. That's exactly what makes passing tests the trap here rather than the evidence. Look at what a test like that contains.

Text typed straight into the test file, on both sides of the comparison. Both sides are literals, so javac pooled both. They are one object. == answers true, the test goes green, and nothing whatsoever was proved.

The test agreed with the pool. It never touched the question the code is asking.

Then the same code meets a String that was not in the source. One typed by a user, or read out of a file, or glued together with + while the program ran. Those get built fresh, one new object each. Same letters, different object, so == says false.

Two lines of proof, and you can run them yourself. src/RuntimeString.java compares one literal against the same letters built at run time.

what actually happens (verified, JDK 17)
$ java RuntimeString
same letters?      true
same object?       false

Now notice when that lands on you. Not at compile time. Not in the test suite. On the first real user, down the one path nobody wrote a test for.

The sentence to say out loud

"Your test passes because both sides are literals in the source, so they're the same pooled object. Once one side comes from input, a file or a +, it's a new object and this goes false."

That is the whole case, and it fits in a breath. You're not quoting a style rule at them. You're naming the exact input that breaks their code, which is an argument they can check.

The number cache, and finding its edge yourself

The Integer story is the same trick with a different cupboard.

Small numbers are used constantly. Loop counters, ages, scores, the number 0. So when Java needs to wrap a small number into an Integer object, it doesn't build one. It keeps a set of ready-made objects for a fixed little range and hands out tickets for those. That set is the Integer cache.

So Integer x = 100, y = 100; gets two tickets for one cached object, and x == y is true. Ask for 1000 and you're outside the cached range, so Java builds a fresh object each time. Two objects, two different tickets, M is false.

Which raises the obvious question. Where does the cache stop? Don't take my word for it — the file src/CacheBoundary.java tests the edge from both sides.

src/CacheBoundary.java — the comparisons
Integer a = 127, b = 127;
Integer c = 128, d = 128;

Integer e = -128, f = -128;
Integer g = -129, h = -129;

Four pairs, chosen to straddle a suspected edge. One pair either side of it at the top, one pair either side at the bottom. The file prints == for all four pairs. Then it prints .equals for the two outer ones, as a control.

Predict before you run — the boundary

Six lines, P to U. Which of the four == pairs come out true? And what do you expect the two .equals lines to say? Guess the exact edge if you dare.

what actually happens (verified, JDK 17)
$ java CacheBoundary
P:  127 ==  127  -> true
Q:  128 ==  128  -> false
R: -128 == -128  -> true
S: -129 == -129  -> false
T:  128 .equals 128  -> true
U: -129 .equals -129 -> true

There it is. The cache covers −128 up to 127, and the very next number in each direction falls off the edge. Move by one and the answer flips.

That range isn't a coincidence. It's exactly the range of a byte, the smallest whole-number type from Chapter 0.2. But "it's one byte's worth" only describes the range. It doesn't say why anyone chose it, so the box below this lab does.

Now look at T and U. Both true. .equals gave a sane answer on both sides of the boundary. It compares the numbers inside rather than the tickets. It didn't care where the objects came from, which is what you wanted all along.

The rule you take away

== on any object type asks "same object?". It sometimes gives the answer you wanted by accident, and cupboards are why. Never rely on the accident. Use .equals when you mean "same contents", and save == for when you genuinely mean "the very same object".

Going deeper Why −128 to 127, and how to move the top edge

Three separate questions hide behind that range. Let's take them one at a time.

Why have a cache at all? Because wrapping small numbers into objects happens constantly. Every counter, every flag, every 0 and 1 that ends up in an Integer. With no cache, Java would build a fresh object for each one. The same tiny handful of numbers would get built thousands of times over.

Why that exact range? It is not a preference. The Java Language Specification requires a minimum, and −128 to 127 is that minimum. Any JVM must hand back the same object for those. Outside the range the rule only says "may", so a JVM is free to cache more, or nothing at all.

You can read that in the JDK's own source, sitting above the cache. Here are its first lines, with the middle trimmed.

JDK 17 source — java/lang/Integer.java, above class IntegerCache
/**
 * Cache to support the object identity semantics of autoboxing for values between
 * -128 and 127 (inclusive) as required by JLS.
 *
 * The cache is initialized on first usage.  The size of the cache
 * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
 * ...
 */

The documentation on Integer.valueOf says the same thing in plainer words. It always caches −128 to 127, and it may cache other values outside that range.

Can you move the edge? The top one, yes. That second sentence in the comment is an open invitation. Run src/CacheMaxFlag.java twice and watch identical code change its answer.

what actually happens (verified, JDK 17)
$ java CacheMaxFlag
Y: 1000 == 1000     -> false
Z: 1000 .equals 1000 -> true
$ java -XX:AutoBoxCacheMax=1000 CacheMaxFlag
Y: 1000 == 1000     -> true
Z: 1000 .equals 1000 -> true

Sit with that for a second. A setting typed outside your program changed what == answers inside it. The .equals line never moved.

The bottom edge won't budge. In that same source file the low end is a fixed −128, and the flag only feeds the high end. I checked it too. With -XX:AutoBoxCacheMax=1000, the comparison -1000 == -1000 is still false.

So here's the lesson, bigger than the number itself. On boxed numbers, == depends on a JVM setting your code never sees. That was never a rule you could lean on. It is a coincidence with a command-line switch attached to it.

Worth remembering

Section 6The empty pocket

One thing left, and Phase 0 promised it. A variable of object type holds a ticket. What if you don't have a ticket yet?

Java has a value for that, and it's spelled null. It means: this box is a ticket box, and there is no ticket in it. Your pocket is empty. Not "a ticket for an empty coat" — no ticket at all.

That distinction is the whole thing. Compare two of them. "" is a real String object that happens to contain no letters. null is the absence of any String at all. An empty envelope and no envelope. Programs go wrong when people treat them as the same.

You can put null in a variable, print it, and test it with ==. All fine. What you can't do is follow it, because there's nothing to follow. Try to use a dot on null and Java stops your program immediately.

The file src/NullDog.java does that on purpose.

src/NullDog.java — inside main
Dog d = new Dog("Rex");
System.out.println("V: " + d.name);

d = null;
System.out.println("W: d now holds " + d);

System.out.println("X: " + d.name);   // this line is the crash

Line 1 builds a dog and puts its ticket in d. Line 2 follows the ticket and prints the name, so we know it worked. Line 4 overwrites d with null — it throws the ticket away without touching the dog. Line 5 prints d itself, which is allowed. Line 7 tries a dot on an empty pocket.

This file compiles cleanly. javac has no complaint at all, and that's worth noticing before you predict.

Predict before you run — null

Three questions. Does W print anything, or blow up first? What exactly does line X print instead of a name? And what happens to the Rex dog when d is set to null?

what actually happens (verified, JDK 17)
$ java NullDog
V: Rex
W: d now holds null
Exception in thread "main" java.lang.NullPointerException: Cannot read field "name" because "<local1>" is null
	at NullDog.main(NullDog.java:22)

W printed fine. Printing an empty pocket is a normal thing to do, and Java writes the word null for you.

Then line X stopped the program. You've seen this shape in Chapter 0.4 — a name for what went wrong, then a line saying where. Read the middle bit, because it's doing you a favour. Cannot read field "name" because ... is null. It names the field you asked for and tells you the pocket was empty.

That's what a NullPointerException always means. Not "something is broken somewhere". It means: you used a dot on a box that had no ticket in it. One kind of mistake, always.

And the last line, at NullDog.main(NullDog.java:22), is your line number. Line 22 is the X line. Older Java stopped there and left you guessing which of five dots on the line was the guilty one. Modern Java names it.

As for Rex: nothing happened to him. d = null is an assignment with no dot in it, so it only changed the box. The dog is still on the heap. But nothing points at him any more, so Java is free to reclaim that memory later.

Why it says <local1> and not d

Java wanted to tell you the variable's name and couldn't. By default javac doesn't record the names of local variables in the compiled file. Your program doesn't need them to run. So Java falls back on <local1>, meaning "the first local variable on this plate". Compile with javac -g NullDog.java and run it again. The message becomes because "d" is null. Same crash, better report. It's also a small reminder that the compiled file isn't your source file.

Your turn

Do these in src/. Compile everything once with javac *.java, then run each program. Write your prediction on paper before each run. A wrong prediction is the good outcome. It points at one wrong idea in your head, and now you know which one.

Section 7Check yourself

A method takes a Dog and does x = new Dog("Spot"); inside. The caller prints its dog's name afterwards. What comes out?

Judgment call. In a code review you see if (a == b), where a and b are Strings. What's your first move?

Where does the name field of a Dog object live?

Section 8What broke, and what surprised you

Write this down in your own words before moving on. The predictions you got wrong are the valuable ones. Each one is a signpost to a single wrong belief you can now replace.

Worth answering properly, on paper:

  1. Which lines did you get wrong, and what were you picturing instead? Name the picture.
  2. In one sentence: why can rename change your dog when swap can't?
  3. How many Dog objects exist at the end of Lab 1, and how many are still reachable?
  4. Could you write a method that genuinely swaps two dogs for the caller? Say how, or say why not.
  5. Where exactly is the Integer cache boundary, and how did you check it?
Next up

You now know what == asks and that .equals is a method someone chose to write. Which raises a fair question: what happens when you write it? Chapter 1.2 builds a Money type and then attacks it. Getting .equals right is easy. Getting it right without breaking a HashMap is where people fall over.

Corrections

Nice. Chapter marked complete.