JAVA / CLASSES, CONSTRUCTORS AND ENCAPSULATION
Classes, objects and where state lives
Predict where each piece of state lives: which fields belong to which object, which variables share one object, and what dies when a method returns.
What you will learn
- Count how many copies of a field a program has: one per object, not one per class
- Tell mutation through a reference apart from rebinding the reference itself
- Use == knowing it compares object identity, never field values
- Choose field or local by how long the value must outlive the call
Understanding Classes, objects and where state lives
A class is not a container of values; it is a description telling the compiler what shape a value of that type has: one named slot per instance field, plus the code of its methods. That description exists once, however many objects you make. The new expression is what allocates: it reserves a fresh block of memory laid out according to the description and hands back a reference to it. A program that ran new Counter() four times therefore contains four independent count slots, and count++ inside a method changes exactly the one belonging to the object the call was made on.
A variable whose type is a class never contains the object; it contains a reference, an arrow to one of those blocks. Assignment copies the arrow, so Counter alias = a; produces a second arrow to the same block rather than a second block, and after it alias.bump() and a.bump() are two spellings of the same increment. The same rule explains ==: it compares arrows, so it answers "is this the same object" and never "do these fields agree".
Lifetime is the other half of the model. A parameter or local variable belongs to a single invocation of a method and is discarded when that invocation returns, while an instance field belongs to the object and survives as long as some reference can reach it. That is the real reason a running total goes in a field and a scratch value goes in a local: only the field is still there on the next call. A reference holding null is an arrow pointing nowhere, so there is no field slot to read and the access fails at runtime with a NullPointerException.
Field slots exist and are readable from the instant new allocates them.
public class Main {
static class Counter {
int count; // one slot per object, not one per class
void bump() {
count++; // the count inside the object this call was made on
}
}
static void bumpTwice(Counter c) {
c.bump(); // c is another arrow to the caller's object
c.bump();
}
public static void main(String[] args) {
Counter a = new Counter();
Counter b = new Counter();
Counter alias = a; // copies the arrow, not the object
a.bump();
alias.bump();
b.bump();
bumpTwice(b);
System.out.println("a.count = " + a.count);
System.out.println("alias.count = " + alias.count);
System.out.println("b.count = " + b.count);
System.out.println("a == alias : " + (a == alias));
System.out.println("a == b : " + (a == b));
}
}A class only describes state; each new creates one independent copy of that state, and a variable of class type holds nothing but an arrow to one of those copies.
Worked examples
Field survives the call, local does not
Shows why a value kept in a field accumulates across calls while a local resets on every invocation.
public class Main {
static class Basket {
int items; // lives in the object
void add() {
int justAdded = 1; // lives in this one call
items = items + justAdded;
System.out.println("justAdded=" + justAdded + " items=" + items);
}
}
public static void main(String[] args) {
Basket basket = new Basket();
basket.add();
basket.add();
basket.add();
}
}Example explained
Line 1int items; reserves one slot inside the Basket object, so it still holds whatever the previous call left there.
Line 2int justAdded = 1; is created fresh for this particular call to add() and discarded when add() returns.
Line 3Three calls print items 1, 2, 3 while justAdded prints 1 every time: same code, two different lifetimes.
Writing through the arrow versus replacing it
Demonstrates that a method can change the caller's object but cannot change which object the caller's variable points at.
public class Main {
static class Note {
String text = "empty";
}
static void rename(Note n) {
n.text = "changed inside"; // writes into the shared object
}
static void replace(Note n) {
n = new Note(); // only repoints the parameter
n.text = "brand new";
}
public static void main(String[] args) {
Note note = new Note();
rename(note);
System.out.println(note.text);
replace(note);
System.out.println(note.text);
Note missing = null;
System.out.println("missing holds an object? " + (missing != null));
}
}Example explained
Line 1rename() writes through the arrow with n.text = ..., reaching the object main created, so main sees the new text.
Line 2replace() assigns n = new Note(), which repoints only its own copy of the arrow; note in main still finds the old object.
Line 3missing holds null, an arrow to no object, so there is no text slot at all and missing.text would throw NullPointerException.
An array of references, not of objects
Shows that array slots hold arrows, so one object can occupy two slots and get updated twice.
public class Main {
static class Dot {
int x;
}
public static void main(String[] args) {
Dot[] dots = new Dot[3];
dots[0] = new Dot();
dots[1] = new Dot();
dots[2] = dots[0];
for (int i = 0; i < dots.length; i++) {
dots[i].x = dots[i].x + 10;
}
System.out.println(dots[0].x);
System.out.println(dots[1].x);
System.out.println(dots[2].x);
}
}Example explained
Line 1new Dot[3] allocates three reference slots, all null; not a single Dot object exists yet.
Line 2dots[2] = dots[0]; stores a second arrow to the first object, so three slots reach only two objects.
Line 3The loop adds 10 three times, but the first object is reached twice and ends at 20 while the second stays at 10.
Important notes
Field slots exist the moment new allocates them and start at 0, false or null, which is why an object is readable before you assign anything; a local gets no such default and the compiler rejects reading one that was never assigned.
Heap and stack are how typical JVMs implement this, not a promise of the language; what you can rely on is lifetime, field state living with the object and local state with the call.
Common mistakes
Reading b = a; as copying the object: a later change through b also shows up through a, because there was only ever one object.
Passing a reference into a method and then assigning a new object to the parameter, so the caller keeps the old object and the intended update silently vanishes.
Using == to check that two objects hold the same data: separately allocated objects are never ==, so the test reports false even when every field matches.
Try it yourself
Change, predict, then run
Write a Wallet class with an int field coins and an earn() method that adds 5, then in main create two wallets, copy one into a third variable, call earn() twice through the copy and once on the other wallet. Print all three coins values plus the two == comparisons, and write down your predictions before running it.
Open the Java workspaceCheck your understanding
What does this print? class Box { int n; } // inside main Box p = new Box(); Box q = p; q.n = 7; p = new Box(); System.out.println(p.n + " " + q.n);
- 0 7, because p now points at a fresh object whose n is still 0 while q still reaches the object that was set to 7
- 7 7, because q = p linked the two variables so they always see the same object
- 0 0, because giving p a new object reset the state that q was sharing
- 7 0, because q keeps the value it wrote and p receives the object that already held 7
Show answer
q = p copied the arrow once; it did not tie the names together, so p = new Box() moves only p's arrow to a freshly allocated object whose n is still 0, while q keeps reaching the object it wrote 7 into. Option 2 is tempting because the two variables genuinely did share one object for a moment, but sharing is a fact about the arrows at that instant, not a permanent link that survives reassigning one of them.