☕ Java
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Objects in Memory — Identity and References
When you create an object with new, the JVM allocates memory on the heap to store the object's fields and returns a reference — a memory address — to that location. The variable you assign to does not contain the object itself; it contains the reference pointing to the object. This distinction is fundamental and explains Java's pass-by-value semantics, how assignment works, and how == compares objects.
Two variables can reference the same object — they both hold the same memory address. Modifying the object through one variable is immediately visible through the other, because they are two names for the same location in memory. This is sharing by reference and is the source of many subtle bugs when objects are mutable.
The == operator on reference types compares the references themselves — are they pointing to the same memory location? It does not compare the contents of the objects. Two distinct objects with identical field values are not == to each other. For content equality, use equals().
Java
// ── Object creation and reference ────────────────────────────────────
//
// StringBuilder sb = new StringBuilder("Hello");
//
// Stack (variables) Heap (objects)
// ┌─────────────────┐ ┌──────────────────────────┐
// │ sb → [0x4A2F] ──┼───►│ StringBuilder object │
// └─────────────────┘ │ value = "Hello" │
// │ capacity = 16 │
// └──────────────────────────┘
StringBuilder sb1 = new StringBuilder("Hello");
StringBuilder sb2 = sb1; // TWO references, ONE object
StringBuilder sb3 = new StringBuilder("Hello"); // NEW object
// ── == compares references (memory addresses) ─────────────────────────
System.out.println(sb1 == sb2); // true — same object
System.out.println(sb1 == sb3); // false — different objects
// ── Shared reference — modification visible through both ──────────────
sb1.append(" World");
System.out.println(sb2.toString()); // "Hello World" — sb2 sees the change
// ── null reference — points nowhere ──────────────────────────────────
String s = null;
System.out.println(s == null); // true
// s.length(); // NullPointerException — no object to call on
// ── References as method parameters — pass-by-value ───────────────────
// Java passes the VALUE of the reference (the address), not the object
// The method gets its own copy of the reference
public static void tryReassign(StringBuilder sb) {
sb = new StringBuilder("New"); // reassigns local copy of reference
// original reference in caller is unchanged
}
public static void mutate(StringBuilder sb) {
sb.append(" mutated"); // modifies the object both references point to
// change IS visible to caller
}
StringBuilder original = new StringBuilder("Original");
tryReassign(original);
System.out.println(original); // "Original" — reassignment did not propagate
mutate(original);
System.out.println(original); // "Original mutated" — mutation propagatedThe Object Class and the Type Hierarchy
Every class in Java implicitly extends java.lang.Object if no explicit superclass is declared. Object is the root of the entire Java type hierarchy — every object, regardless of class, is an instance of Object. This universality means Object defines the small set of methods every Java object supports: toString(), equals(), hashCode(), getClass(), clone(), and the thread synchronisation methods wait(), notify(), and notifyAll().
Understanding Object's default implementations explains surprising default behaviour. The default toString() returns the class name followed by @ and the hash code in hexadecimal — for example, com.myapp.Person@4e50df2e. The default equals() uses reference equality (same as ==). The default hashCode() is typically derived from the object's memory address. Because these defaults are almost never what you want for domain objects, overriding equals() and hashCode() is one of the most important and common tasks in Java class design.
Java
// ── Every class inherits from Object ────────────────────────────────
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// ── Default toString() — not useful ───────────────────────────────
// Point@7852e922 ← class name + hex hashCode
// Override to return meaningful representation:
@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
// ── Default equals() uses == (reference equality) ─────────────────
// Two distinct Point objects with same x,y would NOT be equal
// Override for value-based equality:
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // same reference — definitely equal
if (!(obj instanceof Point other)) return false; // null or wrong type
return this.x == other.x && this.y == other.y;
}
// ── hashCode MUST be consistent with equals ───────────────────────
// Contract: if a.equals(b) then a.hashCode() == b.hashCode()
// Violation breaks HashMap, HashSet, and any hash-based structure
@Override
public int hashCode() {
return Objects.hash(x, y); // combines field hash codes
}
}
// ── Without overrides ─────────────────────────────────────────────────
Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);
System.out.println(p1.equals(p2)); // false (default = ==)
System.out.println(p1.toString()); // Point@7852e922
// ── With overrides ────────────────────────────────────────────────────
System.out.println(p1.equals(p2)); // true (compares x, y)
System.out.println(p1.toString()); // Point(3, 4)
// ── Correct HashMap behaviour requires correct hashCode ──────────────
Set<Point> visited = new HashSet<>();
visited.add(new Point(3, 4));
System.out.println(visited.contains(new Point(3, 4))); // true (with override)
// false (without)Object State — Mutable vs Immutable
An object's state is the collection of current values of its instance fields. When those fields can change after construction, the object is mutable. When they cannot, the object is immutable. Immutability is one of the most powerful design choices available in Java — immutable objects are inherently thread-safe (no synchronisation needed), can be shared freely, are safe to use as HashMap keys, and are easier to reason about because their state never changes unexpectedly.
Java's String class is immutable. Every method that appears to modify a String (replace, toUpperCase, substring) actually returns a new String object. The original is unchanged. BigDecimal, LocalDate, and all the time classes in java.time are also immutable. Creating immutable classes requires: final fields (assigned in constructor, never modified), no setters, defensive copies of mutable parameters passed in, and defensive copies of mutable fields returned.
Java
// ── Mutable class — state can change after construction ─────────────
public class MutablePoint {
private int x;
private int y;
public MutablePoint(int x, int y) { this.x = x; this.y = y; }
public void setX(int x) { this.x = x; }
public void setY(int y) { this.y = y; }
public int getX() { return x; }
public int getY() { return y; }
public void translate(int dx, int dy) {
this.x += dx; // modifies state
this.y += dy;
}
}
// ── Immutable class — state fixed at construction forever ─────────────
public final class ImmutablePoint { // final prevents subclasses
private final int x; // final — cannot be reassigned
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
// No setters — field values can never change
public int getX() { return x; }
public int getY() { return y; }
// Operations return NEW objects rather than modifying this
public ImmutablePoint translate(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
public double distanceTo(ImmutablePoint other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
@Override public String toString() {
return "(" + x + ", " + y + ")";
}
@Override public boolean equals(Object o) {
if (!(o instanceof ImmutablePoint p)) return false;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
// ── Defensive copy for immutability with mutable fields ───────────────
public final class DateRange {
private final LocalDate start; // LocalDate is already immutable
private final LocalDate end;
private final List<String> tags; // List IS mutable — needs defence
public DateRange(LocalDate start, LocalDate end,
List<String> tags) {
if (start.isAfter(end)) throw new IllegalArgumentException(
"Start must not be after end");
this.start = start;
this.end = end;
this.tags = List.copyOf(tags); // defensive copy — immutable view
}
public List<String> getTags() {
return tags; // safe — already an unmodifiable list
}
}equals() and hashCode() Contract
The equals() and hashCode() methods are governed by a strict contract that every Java developer must understand. The contract has two parts. The equals contract requires: reflexivity (x.equals(x) is true), symmetry (x.equals(y) iff y.equals(x)), transitivity (if x.equals(y) and y.equals(z) then x.equals(z)), consistency (returns the same result on repeated calls if no state changes), and x.equals(null) returns false.
The hashCode contract requires: equal objects must have equal hash codes. This is the critical constraint. It does not require unequal objects to have unequal hash codes (collisions are allowed) but equal objects must have the same hash code. Violating this breaks every hash-based data structure — HashMap, HashSet, LinkedHashMap — in subtle and hard-to-debug ways. The implementation is typically delegated to Objects.hash() which combines the hash codes of the relevant fields.
Java
// ── Correct equals() and hashCode() ──────────────────────────────────
public class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
Objects.requireNonNull(amount, "amount cannot be null");
Objects.requireNonNull(currency, "currency cannot be null");
this.amount = amount.stripTrailingZeros();
this.currency = currency.toUpperCase();
}
@Override
public boolean equals(Object obj) {
// Step 1: reference check — cheapest possible equality
if (this == obj) return true;
// Step 2: null and type check — pattern match in Java 16+
if (!(obj instanceof Money other)) return false;
// Step 3: field comparison — the actual equality logic
return this.amount.compareTo(other.amount) == 0
&& this.currency.equals(other.currency);
// Note: BigDecimal.compareTo() not equals() — 2.0 equals 2.00
}
@Override
public int hashCode() {
// Must use the same normalised form as in equals()
return Objects.hash(
amount.stripTrailingZeros(),
currency);
}
@Override
public String toString() {
return amount.toPlainString() + " " + currency;
}
}
// ── Breaking the contract causes silent data loss ─────────────────────
// Bad implementation — hashCode uses 'amount' but equals ignores scale
// new Money("2.00", "USD") and new Money("2.0", "USD") could be
// "equal" per equals() but have DIFFERENT hashCode — this breaks HashMap
// ── Correct behaviour ─────────────────────────────────────────────────
Money m1 = new Money(new BigDecimal("10.00"), "USD");
Money m2 = new Money(new BigDecimal("10.0"), "USD");
System.out.println(m1.equals(m2)); // true — compareTo handles scale
System.out.println(m1.hashCode() == m2.hashCode()); // true — must match
Map<Money, String> labels = new HashMap<>();
labels.put(m1, "price");
System.out.println(labels.get(m2)); // "price" — works correctlyObject Lifecycle — Creation, Use, and Garbage Collection
Every object goes through three phases: creation (allocated on the heap by new, fields initialised, constructor runs), active use (reachable through one or more references, methods called, state potentially modified), and eligibility for garbage collection (when no live reference chain from any GC root leads to the object, it becomes unreachable and eligible for collection).
Java's garbage collector (GC) automatically reclaims the memory of unreachable objects — developers do not call free() or delete. The GC determines reachability by tracing from GC roots (local variables on stack frames, static fields, JNI references) through the reference graph. Any object not reachable through this graph is collected.
The finalize() method runs before collection but is deprecated since Java 9 and removed in Java 18. Resources (files, connections, sockets) should be closed with try-with-resources via the Closeable/AutoCloseable interface, not in finalizers. The timing and even the occurrence of garbage collection is not guaranteed, making finalizers an unreliable mechanism for resource cleanup.
Java
// ── Object lifecycle demonstration ───────────────────────────────────
public class ResourceHolder implements AutoCloseable {
private final String name;
private boolean open = true;
public ResourceHolder(String name) {
this.name = name;
System.out.println("Created: " + name);
}
public void use() {
if (!open) throw new IllegalStateException(
name + " is closed");
System.out.println("Using: " + name);
}
@Override
public void close() {
open = false;
System.out.println("Closed: " + name);
}
}
// ── Lifecycle phases ──────────────────────────────────────────────────
{
// Phase 1: Creation — memory allocated, constructor runs
ResourceHolder r = new ResourceHolder("connection");
// Phase 2: Active use — r references the object on the heap
r.use();
r.use();
} // Phase 3: r goes out of scope — object becomes unreachable
// GC will eventually reclaim the memory
// NOTE: close() was NOT called — resource leak!
// ── Correct resource management — try-with-resources ─────────────────
try (ResourceHolder r = new ResourceHolder("connection")) {
r.use();
r.use();
} // close() called automatically at block exit (even on exception)
// "Closed: connection" always printed
// ── When objects become unreachable ──────────────────────────────────
String s = new String("temporary");
s = "new value"; // original String object now unreachable
// (no reference points to it — eligible for GC)
List<Object> cache = new ArrayList<>();
Object big = new Object();
cache.add(big);
big = null; // big variable released, but cache still holds reference
// Object is NOT eligible for GC — cache is reachable
cache.clear(); // now the Object has no live references — eligible for GC
// ── Soft, Weak, and Phantom references ───────────────────────────────
// java.lang.ref package provides weaker reference strengths:
// SoftReference<T> — collected when JVM needs memory (caches)
// WeakReference<T> — collected at next GC cycle (canonical maps)
// PhantomReference<T> — collected after finalization (cleanup actions)Related Topics in Object-Oriented Programming
OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.
Default Constructor
A default constructor is a constructor that takes no parameters. If you write a class without any constructor, the Java compiler automatically inserts a no-argument constructor that calls the superclass no-argument constructor and does nothing else. The moment you define any constructor yourself — with or without parameters — the compiler no longer inserts the default constructor. Understanding when the default constructor exists, when it disappears, and what it initialises is fundamental to understanding how Java objects come into existence.