JAVA / CLASSES, CONSTRUCTORS AND ENCAPSULATION
Fields, methods and creating instances with new
Declare instance fields and methods, create objects with new, and trace how each instance keeps its own field values behind a reference.
What you will learn
- Declare instance fields for per-object state and instance methods that act on them
- Explain what new does: allocate, zero-fill fields, run constructor, return reference
- Predict default field values (0, 0.0, false, null) before any assignment runs
- Distinguish a reference variable from the object it points at
Understanding Fields, methods and creating instances with new
A class body holds two kinds of declarations that matter here: fields, which are the variables every object of that class carries, and instance methods, which are the operations that read and write those fields. Writing int count; inside a class allocates nothing at all; it states that every Counter which ever exists will have a slot named count of type int. The class is the description, and storage appears only when you ask for an object.
new Counter() does four things in a fixed order: it reserves memory for one object with a slot for every instance field, fills those slots with the zero value for their type (0, 0.0, false, char code 0, null for any reference type), runs the constructor body, and finally evaluates to a reference to that object. A reference is a handle to the object, not the object itself, so Counter a = new Counter(); stores the handle in a and copying a into a second variable copies only the handle, leaving one object with two names.
When you call a.increment(), the expression before the dot chooses the object the call runs on, and inside the method an unqualified count means that object's count. The same compiled method body therefore works on different data depending on the receiver, which is exactly why two counters produced by two separate new expressions move independently. Local variables inside a method behave differently: they live for one call only and get no automatic zero value, so the compiler refuses to let you read one before you assign it.
class Counter {
int count;
String label;
void increment() {
count = count + 1;
}
String describe() {
return label + " -> " + count;
}
}
public class Main {
public static void main(String[] args) {
Counter a = new Counter();
System.out.println("fresh: " + a.count + ", " + a.label);
a.label = "clicks";
a.increment();
a.increment();
Counter b = new Counter();
b.label = "errors";
b.increment();
System.out.println(a.describe());
System.out.println(b.describe());
Counter alias = a;
alias.increment();
System.out.println(a.count + " " + alias.count + " " + (a == alias) + " " + (a == b));
}
}A class only describes fields and methods; each new allocates one independent, zero-filled set of those fields and returns a reference through which methods act on that one object.
Worked examples
Objects you never store
new evaluates to a reference you can use immediately, but nothing survives the statement if you do not keep it.
class Greeter {
int greetings;
String greet(String name) {
greetings = greetings + 1;
return "Hello, " + name + "!";
}
}
public class Main {
public static void main(String[] args) {
System.out.println(new Greeter().greet("Ada"));
System.out.println(new Greeter().greet("Grace"));
Greeter kept = new Greeter();
kept.greet("Alan");
kept.greet("Edsger");
System.out.println("kept.greetings = " + kept.greetings);
}
}Example explained
Line 1new Greeter().greet("Ada") builds an object, calls the method on it, and drops the reference on the same line, so the increment to greetings is lost with the object.
Line 2The second println repeats that with a second, unrelated object; its greetings also went from 0 to 1 and is equally unreachable.
Line 3kept holds the reference that new returned, so both greet calls land on the same field and it reaches 2.
Passing a reference into a method
A method can change the object it was handed, but reassigning the parameter with new only repoints the parameter.
class Tank {
int litres;
void fill(int amount) {
litres = litres + amount;
}
}
public class Main {
static void topUp(Tank t) {
t.fill(5);
t = new Tank();
t.fill(100);
}
public static void main(String[] args) {
Tank tank = new Tank();
tank.fill(2);
topUp(tank);
System.out.println("litres = " + tank.litres);
}
}Example explained
Line 1new Tank() in main allocates one object whose litres slot was zero-filled to 0, and fill(2) raises it to 2.
Line 2topUp gets a copy of the reference, so t.fill(5) reaches through to the caller's object and litres becomes 7.
Line 3t = new Tank() allocates a second object and repoints only the parameter; main's tank variable still holds the original reference.
Line 4The 100 litres land in an object nothing else can reach, so main never observes them.
Field defaults after new
Every instance field is readable straight after new, because new zero-fills each slot according to its type.
class Defaults {
int i;
double d;
boolean b;
char c;
String s;
int[] arr;
void show() {
System.out.println("int " + i);
System.out.println("double " + d);
System.out.println("boolean " + b);
System.out.println("char code " + (int) c);
System.out.println("String " + s);
System.out.println("int[] " + arr);
}
}
public class Main {
public static void main(String[] args) {
new Defaults().show();
}
}Example explained
Line 1No field is ever assigned, yet show() reads all six: new gave each slot the zero value for its declared type.
Line 2The char zero value has no visible glyph, so (int) c prints it as the code 0.
Line 3String and int[] are reference types, so their slots hold null and string concatenation renders that as the text null.
Important notes
Two objects created by two separate new calls are never == to each other even when every field matches, because == on references compares object identity rather than contents.
Zero-filling applies to fields only; a local variable such as int n; inside a method has no default and reading it first is a compile error, "variable n might not have been initialized".
Common mistakes
Touching a field or instance method directly from main, as in count = count + 1;, which fails to compile with "non-static variable count cannot be referenced from a static context" because no object was named to own that count.
Writing Counter c; and then c.increment() without new: the declaration only reserves room for a reference, that reference is null, and the call throws NullPointerException at runtime.
Calling new Counter().increment() each time instead of keeping one reference, so every call builds and abandons a fresh object and the count never gets past 1.
Try it yourself
Change, predict, then run
Write a class Playlist with fields String title and int songs plus a method addSong() that raises songs by one. In main create two Playlist objects, add three songs to the first and one to the second, then print each title and count to show the fields moved independently.
Open the Java workspaceCheck your understanding
Given class Box { int n; } and this code in main: Box a = new Box(); Box b = a; b.n = 5; a = new Box(); System.out.println(a.n + " " + b.n); What does it print?
- 5 5
- 0 5
- 0 0
- 5 0
Show answer
b.n = 5 changes the single object that both variables referred to at that moment. The later a = new Box() allocates a second object with n zero-filled to 0 and stores its reference in a, so a.n reads 0 while b still refers to the first object and reads 5. "5 5" assumes the two variables stay tied together, but assignment replaced only the reference held in a; it did not touch b or the object b points at.