6.1
Classes, Objects, and `new`
A class defines a type with state and behaviour. Each object is one instance of that type, with its own fields.
Previously on
Section 4.1 stored one roster in four parallel arrays. A swap had to move four entries together.
Nothing connected those entries in the type system. If three moved and one stayed, the compiler accepted the broken roster.
Section 4.2 also separated an array variable from the array object created by new. Classes use the same reference model.
The problem
Suppose one student needs four values.
String name = "Aditya";
int age = 28;
int rollNumber = 101;
String college = "IIT Guwahati";A second student needs four more variables and new names for all of them.
String name2 = "Rohit";
int age2 = 24;
int rollNumber2 = 102;
String college2 = "IIT Guwahati";Java sees eight unrelated variables. It does not know that name2 and rollNumber2 describe the same student.
Methods expose the same weakness.
printDetails(name, age, rollNumber, college);Swapping the two String arguments still compiles. Adding a phone number changes every call site.
Parallel arrays reduce the variable count, but they keep the alignment bug. The program needs a type that makes one student’s data travel together.
The idea
A class defines that type.
class Student {
String name;
int age;
int rollNumber;
String college;
void printDetails() {
System.out.println(rollNumber + ": " + name + ", " + age + ", " + college);
}
boolean isAdult() {
return age >= 18;
}
}The fields hold state. The methods define behaviour available on a Student.
Student is now a reference type, like String or int[]. An object is one instance of that type.
| Class | Object | |
|---|---|---|
| Role | defines a type | is one instance of that type |
| State | declares the fields | holds one value for each instance field |
| Behaviour | declares method code | receives instance method calls |
| Written or made by | class Student { ... } | new Student() |
Here is a complete program.
class Student {
String name;
int age;
int rollNumber;
void birthday() {
age++;
}
String label() {
return rollNumber + ": " + name + " (" + age + ")";
}
}
public class Demo {
public static void main(String[] args) {
Student aditya = new Student();
aditya.name = "Aditya";
aditya.age = 28;
aditya.rollNumber = 101;
Student rohit = new Student();
rohit.name = "Rohit";
rohit.age = 24;
rohit.rollNumber = 102;
aditya.birthday();
System.out.println(aditya.label());
System.out.println(rohit.label());
}
}101: Aditya (29)
102: Rohit (24)birthday() needs no Student parameter. An instance method runs with a current object, so plain age means that object’s field.
A roster now needs one array.
Student[] roster = new Student[100];Swapping two elements moves two references to whole Student objects. A name cannot separate from its roll number during that swap.
Under the hood
Going deeperWhat new Student() does
new creates an object. The full expression performs several ordered steps.
Creating one Student
- Ensure the class is readyThe JVM loads, links, and initialises the class when required. Section 6.4 separates those stages.
- Allocate storageThe JVM reserves enough managed memory for the object and its instance fields.
- Write default field valuesNumeric fields become 0, boolean fields become false, and reference fields become null.
- Run initialisation codeField initialisers, initialiser blocks, and constructors run in a defined order. Section 6.2 traces it.
- Produce a referenceThe expression evaluates to an opaque reference to the new object.
The source code cannot read a memory address from that reference. It cannot add one to it either.
The garbage collector may move an object while the program runs. References continue to reach the object after such a move.
A useful memory model
Student aditya = new Student();
Student rohit = new Student();Stack
main
Heap
Student@1a2b
Student@283c
Real JVMs may keep a local value in a register, move it, or remove it during optimisation. The drawing explains identity and reachability, not a required physical layout.
Declaration, null, and creation are different
Student a; // unassigned local variable
Student b = null; // assigned, but reaches no object
Student c = new Student(); // assigned and reaches an objectReading a does not compile.
System.out.println(a); // ERROR: variable a might not have been initializedReading b is legal. Dereferencing it is not.
System.out.println(b); // null
System.out.println(b.name); // throws NullPointerExceptionThe dot operator asks the object reached by the reference to provide a field or receive a method call. null reaches no object.
Objects may refer to other objects
class Address {
String city;
}
class Student {
String name;
Address address;
}The address field holds either null or a reference to an Address object. It does not contain the Address object’s fields inline.
Stack
main
Heap
Student@1a2b
Address@283c
Two students may hold references to the same Address. A change through either path then reaches that shared object.
Methods are not copied into every object
Each object has its own instance fields. It does not hold another copy of the method bytecode.
The JVM keeps class metadata and method code with the loaded class. Each defining class loader gets its own class identity and static state.
That detail matters in application servers and plugin systems. Section 6.4 returns to it when counting static members.
What it costs
Objects add a header, alignment, and references around your field data. Their measured size depends on the JVM and its options. Section 6.3 uses JOL to inspect one real layout.
An access such as student.address.city follows two references. Either target may be far from the other data in memory, which can hurt cache use.
Objects also create lifetime work. The garbage collector must track reachable objects and recover storage from unreachable ones.
The extra machinery is not free. In return, the type system keeps related state together and checks which operations belong to it.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
What differs between `Student s;`, `Student s = null;`, and `Student s = new Student();`?
Show the answer
The first declares a local variable but does not give it a value. Java will reject any read of that local variable until every path assigns it.
The second gives the variable the reference value
null. The variable may be read, buts.namethrows because there is no object to receive the field access.The third creates and initialises a Student object. It then stores a reference to that object in
s. The reference is an opaque JVM value, not an address your program can inspect or do arithmetic with.A class has one `name` field declaration. You create two objects. How many `name` fields exist?
Show the answer
Two instance fields exist, one in each object. The declaration in the class defines the shape that every instance receives.
Changing one object's field does not change the other object's field. A field marked
staticfollows a different rule and belongs to the class instead.What does a class define besides a group of fields?
Show the answer
A class defines a new reference type. Its fields describe object state, its methods describe available behaviour, and its constructors control how instances begin.
The compiler can now reject assigning a Rectangle to a Student variable. It can also check field and method names on every use.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises90 pointsabout 105 minutes
From Parallel Arrays to Objects
ex-6-1-aGo back to the roster you built in Section 4.1 with four parallel arrays, and rebuild it with a class.
Then look hard at the sorting code. In the array version, every swap had to happen in four places, and nothing stopped you from forgetting one. Count how many places a swap touches now.
That is the whole argument for objects, and you can only see it properly because you wrote the bad version first.
Write the explanation as a comment in your file. Not “objects are better”, but the specific mechanical reason the bug cannot be expressed any more.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class RegistryV2 {
static Unit[] roster()
static Unit highestReadiness(Unit[] units)
static Unit[] sortedByReadinessDescending(Unit[] units)
static Unit findById(Unit[] units, int id)
static int arraysNeededToSort()
}Write a nested Unit class holding name, id, readiness and active. The roster is the same five units from Section 4.1. The sort returns a new array and leaves the one it was given alone. arraysNeededToSort is you counting how many arrays a swap has to touch now.
What your program must do
- Write a Unit class and rebuild the roster with it
- Sort by readiness and confirm no unit comes apart
- Return whole units from the finders, not indexes
- Count how many arrays a swap touches now against Section 4.1
import java.util.*;
public class RegistryV2 {
// TODO: a Unit class holding name, id, readiness and active
static class Unit {
}
// The same five units as Section 4.1:
// Atlas 101 88.5 true, Beacon 102 42.0 true, Cipher 103 95.5 false,
// Drift 104 61.0 true, Ember 105 73.5 true
static Unit[] roster() { return new Unit[0]; } // TODO
static Unit highestReadiness(Unit[] units) { return null; } // TODO
static Unit findById(Unit[] units, int id) { return null; } // TODO
// Sort by readiness, highest first. Return a NEW array.
static Unit[] sortedByReadinessDescending(Unit[] units) { return units; } // TODO
// How many arrays does one swap have to touch now?
static int arraysNeededToSort() { return 4; } // TODO
public static void main(String[] args) {
// TODO: print the roster, sorted by readiness
}
}
Hint 1
this.name = name so the parameter and the field can share a name.Hint 2
Hint 3almost the answer
Draw What new Does
ex-6-1-bDraw this program. On paper, by hand, line by line.
One box per variable on the stack. One block per object on the heap. An arrow for every reference.
Count the objects. There are three, and the number of new keywords tells you that before you run anything.
Then predict the printed city. If your drawing is right, the answer is obvious. If your drawing is wrong, running the program will tell you which arrow you got wrong, which is the most useful kind of feedback there is.
Finish with the null line. Uncomment it, read the exception, and point at the exact box in your drawing that caused it.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Drawing {
static Student make(String name, int age, String city)
static boolean freshStudentHasAddress()
static String freshStudentName()
static int freshStudentAge()
static boolean assignmentSharesTheObject()
static boolean fieldCopyStillSharesAddress()
static int objectsCreatedBy(String what)
}Student and Address are nested classes. objectsCreatedBy takes the text "new Student()" or "make" and answers how many objects that creates. fieldCopyStillSharesAddress copies name, age and address one at a time, then changes the city through the copy.
What your program must do
- Draw the stack and heap for two students before writing any code
- Say what a brand new Student contains, field by field
- Show that assignment shares the object
- Show that copying the fields still shares the Address
public class Drawing {
static class Address {
String city;
Address(String city) { this.city = city; }
}
static class Student {
String name;
int age;
Address address;
}
static Student make(String name, int age, String city) { return null; } // TODO
// What does a brand new Student actually contain? Predict all three.
static boolean freshStudentHasAddress() { return true; } // TODO
static String freshStudentName() { return ""; } // TODO
static int freshStudentAge() { return -1; } // TODO
// Assign one Student to another, change the second, and report on the first.
static boolean assignmentSharesTheObject() { return false; } // TODO
// Copy name, age and address ONE AT A TIME into a new Student, then change
// the city through the copy. Did the original change too?
static boolean fieldCopyStillSharesAddress() { return false; } // TODO
// How many objects does each of these create?
static int objectsCreatedBy(String what) { return 0; } // TODO
public static void main(String[] args) {
// TODO: draw the stack and the heap for two students on paper first
}
}
Hint 1
new Student() creates one object. Its address field is a slot holding null, and no Address exists until something calls new Address.Hint 2
Hint 3almost the answer
name and age came across as values, and address came across as a reference, so both students now point at one Address.Move the Methods In
ex-6-1-cFour methods that all take a Rectangle as their first parameter. That is a sign. When every method in a group needs the same thing, that thing should own the methods.
Move them inside the class and delete the parameter. Watch the code get shorter and clearer at the same time.
Then add scaleBy, which changes the rectangle rather than producing a new one. Ask yourself why it can work at all, given that the caller passed the object in. You already know the answer from Section 6.3, and it is worth stating out loud.
Finally, isSquare has a bug that has nothing to do with objects. Find it, name the phase it comes from, and fix it.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Behaviour {
static Rectangle make(double w, double h)
static int argumentsNeededNow()
static int argumentsNeededBefore()
}Rectangle is a nested class and the four methods move INSIDE it: area(), perimeter(), isSquare(), describe(), plus scale(double). They take no Rectangle argument, because the object is the argument. describe returns a String containing the area and whether it is square.
What your program must do
- Move all four methods inside the class
- Add a scale method that changes the object
- Show that two rectangles keep their own measurements
- Say what happened to the parameter every one of those methods used to need
public class Behaviour {
static class Rectangle {
double width;
double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
// TODO: move the four methods in here. They take NO Rectangle argument.
// double area()
// double perimeter()
// boolean isSquare()
// String describe() returns something containing the area and isSquare
// void scale(double factor) multiplies both sides
}
static Rectangle make(double w, double h) { return null; } // TODO
// How many arguments did area need before, and how many now?
static int argumentsNeededBefore() { return 0; } // TODO
static int argumentsNeededNow() { return 1; } // TODO
public static void main(String[] args) {
// TODO: build a rectangle, describe it, scale it, describe it again
}
}
Hint 1
width means this object's width. There is no argument to pass because the object is already the thing being asked.Hint 2
describe() can call area() and isSquare() directly, with nothing passed between them.Hint 3almost the answer
scale shows the other half of the idea. A method can change the object it belongs to, which is why data and behaviour together is more than a tidy arrangement.Measure the Overhead
ex-6-1-dYou were told an object costs more than its fields. Measure how much more.
Predict the numbers using the configuration stated in the starter. Then run the heap-usage experiment.
Two million ints in an array, against two million objects each holding one int. Same information. Very different memory.
The ratio explains why boxed values can cost more memory. It belongs to this run and this configuration.
The numbers will vary because collection, allocation buffers, and heap growth affect the difference. Compare the trend, then use JOL when you need a field layout.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Overhead {
static long usedBytes()
static long primitiveArrayBytes(int n)
static long objectArrayBytes(int n)
static int bytesPerObject()
static int bytesPerPrimitive()
static int headerBytes()
static long expectedObjectBytes(int n)
}Use this explicit model for the tested arithmetic: 64-bit HotSpot, 12-byte object headers, 4-byte compressed references, and 8-byte object alignment. The heap-difference methods are noisy experiments. They do not measure an individual object's layout.
What your program must do
- Measure both arrays and record the two numbers
- Hold a live reference while measuring, and say why
- Predict the object cost under the stated HotSpot model
- Explain why a heap difference is not an object-layout measurement
public class Overhead {
static class Boxed { int value; Boxed(int v) { value = v; } }
static Object keep; // hold a live reference while measuring
static long usedBytes() { return 0; } // TODO
static long primitiveArrayBytes(int n) { return 0; } // TODO: an int[n]
static long objectArrayBytes(int n) { return 0; } // TODO: a Boxed[n], all filled
// MODEL FOR THIS EXERCISE ONLY: 64-bit HotSpot, compressed references,
// 12-byte headers, 8-byte object alignment. Other configurations differ.
static int headerBytes() { return 0; } // TODO: the model's header
static int bytesPerPrimitive() { return 0; } // TODO
static int bytesPerObject() { return 0; } // TODO: header + int, padded to a multiple of 8
static long expectedObjectBytes(int n) { return 0; } // TODO: objects plus the reference array
public static void main(String[] args) {
// TODO: measure both at 2,000,000 and compare
// TODO: run with -Xms2g -Xmx2g so the heap is not resizing under you
}
}
Hint 1
-Xms2g -Xmx2g so the heap is not growing underneath you either.Hint 2
Hint 3almost the answer
After the credits
The current objects can exist with null names and negative ages. Section 6.2 introduces constructors and one place to enforce valid starting state.
References create sharing. Section 6.3 shows why assignment aliases an object and why Java still passes every argument by value.
Fields are also open to any code that can reach them. Section 7.1 adds access control and keeps state changes behind methods.
Two separate Student objects remain unequal under ==, even when all their fields match. Section 8.3 defines value equality, then Phase X uses it inside hash-based collections.
Threads you opened in this section
- ClassEvery class you have ever written already extends one class.8.3 - The Object Class: `equals`, `hashCode`, `toString`
Class will return in 7.1 - Encapsulation, Inheritance, Packages, and `super`