Classes & Objects
A class is a blueprint — fields hold state, methods describe behaviour, constructors initialise instances. Java’s class system underpins everything from POJOs and DTOs to records and sealed hierarchies.
Fields, ctors, encapsulation, records
EXAMPLE
// 1) Minimal class
public class Point {
int x;
int y;
}
Point p = new Point();
p.x = 3;
p.y = 4;
// 2) Encapsulation — private fields, public methods
public class BankAccount {
private final String owner; // immutable after construction
private long balanceCents; // mutable state
public BankAccount(String owner, long openingCents) {
if (owner == null || owner.isBlank()) throw new IllegalArgumentException("owner");
if (openingCents < 0) throw new IllegalArgumentException("negative opening");
this.owner = owner;
this.balanceCents = openingCents;
}
public String owner() { return owner; }
public long balanceCents() { return balanceCents; }
public void deposit(long cents) {
if (cents <= 0) throw new IllegalArgumentException("positive deposit only");
balanceCents += cents;
}
public void withdraw(long cents) {
if (cents <= 0) throw new IllegalArgumentException();
if (cents > balanceCents) throw new IllegalStateException("insufficient funds");
balanceCents -= cents;
}
}
// 3) Multiple constructors + this()-chaining
public class Money {
private final long cents;
private final String currency;
public Money(long cents, String currency) {
this.cents = cents;
this.currency = currency;
}
public Money(long cents) { this(cents, "AUD"); } // delegate to canonical ctor
}
// 4) Static members — belong to the class, not instances
public class IdGen {
private static long counter = 0;
public static synchronized long next() { return ++counter; } // thread-safe enough
private IdGen() {} // utility — no instances
}
long id = IdGen.next();
// 5) Nested types
public class Outer {
private int v = 10;
public class Inner { // non-static — captures Outer
int show() { return v; } // can read Outer's fields
}
public static class Helper { // static — no Outer reference
public int doubled(int x) { return x * 2; }
}
}
// Prefer 'static class' unless you specifically want the captured outer instance.
// 6) toString / equals / hashCode — the boilerplate trio
import java.util.Objects;
public final class Coord {
private final int x, y;
public Coord(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; } public int y() { return y; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Coord c)) return false; // pattern matching for instanceof (Java 16+)
return x == c.x && y == c.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
@Override public String toString() { return "Coord[" + x + "," + y + "]"; }
}
// 7) Records (Java 16+) — concise immutable data classes
public record Coord2(int x, int y) {}
// Auto-generated: canonical constructor, accessors x() and y(), equals, hashCode, toString.
public record Order(String id, long totalCents, List<LineItem> items) {
public Order {
Objects.requireNonNull(id);
if (totalCents < 0) throw new IllegalArgumentException();
items = List.copyOf(items); // defensive copy in the compact constructor
}
public boolean isFree() { return totalCents == 0; }
}
// 8) Inheritance — careful, often LSP-violating
public abstract class Animal {
private final String name;
protected Animal(String name) { this.name = name; }
public String name() { return name; }
public abstract String sound();
}
public class Dog extends Animal {
public Dog(String name) { super(name); }
@Override public String sound() { return "woof"; }
}
// Prefer composition over inheritance unless the IS-A relationship is rock solid.
// 9) Sealed classes — restrict who can extend (Java 17+)
public sealed interface Shape permits Circle, Rect, Triangle {}
public record Circle(double r) implements Shape {}
public record Rect(double w, double h) implements Shape {}
public record Triangle(double a, double b, double c) implements Shape {}
// Pattern matching for switch (Java 21+)
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Rect r -> r.w() * r.h();
case Triangle t -> heron(t.a(), t.b(), t.c());
};
}
// 10) Immutability checklist (a final class to a code reviewer)
// ✓ class is final OR sealed
// ✓ all fields are private final
// ✓ no setters
// ✓ defensive copies for mutable collections / arrays in ctor AND accessors
// ✓ accessor methods return immutable views (List.copyOf, Map.copyOf)
// ✓ equals + hashCode based on the value, not identity
// 11) Common bugs
// • Missing equals OR hashCode — using object as map key behaves randomly
// • Default toString() — class@1a2b3c — useless in logs and tests
// • Mutable Date / Calendar fields — use java.time.* and immutable types instead
// • Setters returning 'this' for chaining — looks like a builder, isn't safe — use records or true builders
// • Allowing 'null' through constructors — Optional or Objects.requireNonNull at the boundary
// • Sharing a non-thread-safe collection field — return List.copyOf or unmodifiableList in getters
Why it matters
Modern Java leans on records and sealed interfaces, and you should too: a record gives you correct equals/hashCode/toString for free, and a sealed hierarchy lets pattern matching cover every case at compile time. Reach for inheritance only when the IS-A relationship is truly stable.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
public class User {
String name;
int age;
public User(String name, int age) { this.name = name; this.age = age; }
public String greet() { return "hi, " + name; }
}
Try it Yourself »
Exercise
Refer to the current instance.
.name = name;
Four letters.
Discussion
Loading…