Java Objects
An object is an instance of a class: a concrete thing in memory, with its own values for the fields the class defines. A class is written once; objects are created from it as many times as the program needs. Knowing how objects are created, referenced, compared, and destroyed explains a large share of Java's everyday behavior.
Creating an Object
public class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Point p = new Point(3, 4);
This one line does four things:
- Declares a variable
pof typePoint. - Allocates memory on the heap for a new
Pointobject, with its fields set to defaults (0). - Runs the constructor, which assigns
x = 3andy = 4. - Stores a reference to the new object in
p.
The new keyword is what creates the object. Declaring a variable alone creates nothing:
Point q; // a variable that can refer to a Point; no object exists
q.x = 1; // compile error if q is a local variable: not initialized
Accessing Fields and Methods
The dot operator reaches an object's members:
Point p = new Point(3, 4);
System.out.println(p.x); // 3
p.y = 10; // only possible because y is not private here
With a well-encapsulated class, you call methods instead of touching fields:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; }
public int getY() { return y; }
public double distanceTo(Point other) {
int dx = x - other.x;
int dy = y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
Point a = new Point(0, 0);
Point b = new Point(3, 4);
System.out.println(a.distanceTo(b)); // 5.0
Inside distanceTo, x refers to the field of the object the method was called on (a), and other.x refers to the field of the argument (b). A class's methods can access private fields of any object of that class; privacy is enforced per class, not per object.
Each Object Has Its Own State
public class Counter {
private int count;
public void increment() { count++; }
public int get() { return count; }
}
Counter pageViews = new Counter();
Counter downloads = new Counter();
pageViews.increment();
pageViews.increment();
downloads.increment();
System.out.println(pageViews.get()); // 2
System.out.println(downloads.get()); // 1
Both objects run the same increment code, but each changes only its own count. This is the essential difference between instance fields and static fields: a static field would be shared, and both would report 3.
Variables Hold References, Not Objects
An object variable does not contain the object. It contains a reference: a pointer the JVM uses to find the object on the heap.
Counter a = new Counter();
Counter b = a; // copies the reference: both refer to the SAME object
b.increment();
System.out.println(a.get()); // 1: the change is visible through a
a ──┐
├──► Counter { count = 1 }
b ──┘
Assignment copies the reference, never the object. To get an independent copy, you must create a new object, for example through a copy constructor or a method that builds one.
The same rule applies when passing objects to methods. The method receives a copy of the reference, so it can modify the object the caller passed in:
static void resetTo(Counter c, int times) {
c.increment(); // affects the caller's object
}
null: A Reference to Nothing
A reference variable can hold null, meaning it refers to no object:
Point p = null;
System.out.println(p == null); // true
p.getX(); // NullPointerException
Calling a method or reading a field through null throws NullPointerException. Modern JVMs explain exactly which reference was null:
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "Point.getX()" because "p" is null
Fields of reference type default to null, which is why an object created without initializing such a field can fail later. Initialize fields in the constructor, validate parameters with Objects.requireNonNull, and return empty collections or Optional rather than null where possible.
Comparing Objects: == Versus equals
== on references asks: are these the same object?
equals asks: are these objects equivalent?, according to the class's own definition.
String s1 = new String("java");
String s2 = new String("java");
System.out.println(s1 == s2); // false: two separate objects
System.out.println(s1.equals(s2)); // true: same characters
For your own classes, equals inherited from Object behaves exactly like == until you override it:
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println(a.equals(b)); // false with the inherited Object.equals
To compare by value, override equals and hashCode together:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
hashCode must be overridden whenever equals is, because hash-based collections (HashMap, HashSet) first compare hash codes to decide where an object belongs. Two equal objects with different hash codes would be treated as different keys. IDEs generate both methods correctly, and records generate them automatically:
record Point(int x, int y) {}
System.out.println(new Point(1, 2).equals(new Point(1, 2))); // true
Objects and the Object Class
Every class extends java.lang.Object, directly or indirectly. So every object has these methods:
| Method | Default behavior | Commonly overridden? |
|---|---|---|
toString() | Class name + @ + hash in hex | Yes, for readable output |
equals(Object) | Same as == | Yes, for value comparison |
hashCode() | Identity-based number | Yes, whenever equals is |
getClass() | Returns the runtime class | No (it is final) |
getClass() reveals the object's actual class, which may be more specific than the variable's declared type:
Object o = "hello";
System.out.println(o.getClass().getName()); // java.lang.String
Arrays and Collections of Objects
An array or list of objects holds references:
Point[] corners = new Point[4]; // four null references, no Points yet
corners[0] = new Point(0, 0);
corners[1] = new Point(5, 0);
List<Point> path = new ArrayList<>();
path.add(new Point(0, 0));
path.add(new Point(3, 4));
path.add(new Point(6, 8));
double length = 0;
for (int i = 1; i < path.size(); i++) {
length += path.get(i - 1).distanceTo(path.get(i));
}
System.out.println(length); // 10.0
new Point[4] creates an array, not four points. Each slot starts as null and must be assigned an object before use.
Object Lifetime and Garbage Collection
An object lives as long as it is reachable: as long as some chain of references from active code leads to it. When nothing refers to it anymore, it becomes eligible for garbage collection, and the JVM reclaims its memory automatically.
Point p = new Point(1, 1); // object A, referenced by p
p = new Point(2, 2); // object B; nothing refers to A anymore
p = null; // nothing refers to B either
After the second line, object A is unreachable. After the third, so is B. You never free memory explicitly, and there are no destructors. Objects that hold external resources (files, sockets, database connections) are closed explicitly, usually with try-with-resources, rather than relying on garbage collection.
Anonymous and Temporary Objects
An object does not need to be stored in a variable. It can be created and used immediately:
System.out.println(new Point(3, 4).distanceTo(new Point(0, 0))); // 5.0
String upper = new StringBuilder("abc").reverse().toString().toUpperCase(); // "CBA"
These objects are unreachable as soon as the expression finishes and are collected. Method chaining like the StringBuilder example works because each method returns an object on which the next method is called.
Immutable Objects
An immutable object's state cannot change after it is constructed. String, Integer, LocalDate, BigDecimal, and records are immutable.
LocalDate d = LocalDate.of(2026, 11, 10);
d.plusDays(1); // returns a NEW LocalDate; d is unchanged
LocalDate next = d.plusDays(1); // 2026-11-11
Immutable objects can be shared freely, because no one can modify them unexpectedly; they are automatically thread-safe and make reliable HashMap keys. Making a class immutable requires private final fields, no methods that modify state, and care with mutable fields such as lists (store and return copies).
Practical Example: Shopping Cart
import java.util.ArrayList;
import java.util.List;
public class CartDemo {
record Product(String name, double price) {}
static class LineItem {
private final Product product;
private int quantity;
LineItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
double total() { return product.price() * quantity; }
}
static class Cart {
private final List<LineItem> items = new ArrayList<>();
void add(Product p, int qty) {
for (LineItem item : items) {
if (item.product.equals(p)) { // record equality: same name and price
item.quantity += qty;
return;
}
}
items.add(new LineItem(p, qty));
}
double total() {
double sum = 0;
for (LineItem item : items) sum += item.total();
return sum;
}
int itemCount() { return items.size(); }
}
public static void main(String[] args) {
Product pen = new Product("Pen", 25);
Product book = new Product("Notebook", 120);
Cart mine = new Cart();
Cart yours = new Cart();
mine.add(pen, 3);
mine.add(book, 1);
mine.add(new Product("Pen", 25), 2); // a different object, but equal
yours.add(book, 4);
System.out.println("Mine: " + mine.itemCount() + " lines, total " + mine.total());
System.out.println("Yours: " + yours.itemCount() + " lines, total " + yours.total());
}
}
Output:
Mine: 2 lines, total 245.0
Yours: 1 lines, total 480.0
Two Cart objects maintain completely separate item lists. The same book object is referenced by both carts' line items, which is safe because records are immutable. The third add call passes a new Product object, but equals (generated by the record) recognizes it as the same product, so the quantity of the existing pen line grows to 5 instead of a duplicate line being added. With == in place of equals, the cart would show three lines.