Cheat SheetsJava A–ZOOP & Advanced Classes

OOP & Advanced Classes — Cheat Sheet

Java A–Z · 18 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
OOP & Advanced Classes
Java A–Z18 topicsQuick revision reference
1

Abstract Classes

  • An abstract class cannot be instantiated — only its concrete subclasses can
  • A class with even one abstract method must be declared abstract
  • Abstract class constructors run via super() — useful to enforce mandatory field initialisation
  • The Template Method pattern uses final + abstract: skeleton fixed, steps customisable
  • Abstract classes can have instance fields and constructors; interfaces cannot
  • A subclass must implement all abstract methods or declare itself abstract
Shape.java
public abstract class Shape {
    // Field shared by all shapes
    protected String color;

    // Constructor — called via super() in subclasses
    public Shape(String color) {
        this.color = color;
    }

    // Abstract method — subclasses MUST implement
    public abstract double area();
    public abstract double perimeter();

    // Concrete method — inherited as-is
    public void printInfo() {
        System.out.printf("%s | color=%s | area=%.2f | perimeter=%.2f%n",
            getClass().getSimpleName(), color, area(), perimeter());
    }
}

public class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);           // calls Shape(String)
        this.radius = radius;
    }

    @Override public double area()      { return Math.PI * radius * radius; }
    @Override public double perimeter() { return 2 * Math.PI * radius; }
}

public class Rectangle extends Shape {
    private double width, height;

    public Rectangle(String color, double w, double h) {
        super(color);
        this.width = w; this.height = h;
    }

    @Override public double area()      { return width * height; }
    @Override public double perimeter() { return 2 * (width + height); }

    public static void main(String[] args) {
        Shape[] shapes = {
            new Circle("red", 5),
            new Rectangle("blue", 4, 6)
        };
        for (Shape s : shapes) s.printInfo();
        // Circle    | color=red  | area=78.54 | perimeter=31.42
        // Rectangle | color=blue | area=24.00 | perimeter=20.00
    }
}
2

Encapsulation

  • Make fields private; expose state through public methods with validation
  • Immutable classes: final class, final fields, no setters, defensive copies of mutable fields
  • "Tell, Don't Ask" — push logic into the class instead of extracting state and computing outside
  • Derived values (getFahrenheit from celsius) eliminate redundant fields and synchronisation bugs
  • Every public method is a public API commitment — minimise the surface area
  • Validate in the constructor and setters so the object is always in a valid state
Temperature.java
public class Temperature {
    private double celsius;  // private — callers cannot access directly

    public Temperature(double celsius) {
        setCelsius(celsius);  // reuse setter validation in constructor
    }

    // Getter
    public double getCelsius() { return celsius; }

    // Setter with validation
    public void setCelsius(double celsius) {
        if (celsius < -273.15)
            throw new IllegalArgumentException("Below absolute zero: " + celsius);
        this.celsius = celsius;
    }

    // Derived values — no extra field needed, calculated on demand
    public double getFahrenheit() { return celsius * 9.0 / 5.0 + 32; }
    public double getKelvin()     { return celsius + 273.15; }

    @Override
    public String toString() {
        return String.format("%.1f°C / %.1f°F / %.1fK", celsius, getFahrenheit(), getKelvin());
    }

    public static void main(String[] args) {
        Temperature t = new Temperature(100);
        System.out.println(t);              // 100.0°C / 212.0°F / 373.2K
        t.setCelsius(0);
        System.out.println(t.getKelvin()); // 273.15

        // t.celsius = -300; // compile error — field is private
    }
}
3

Polymorphism

  • Runtime polymorphism: method dispatch is based on the actual object type, not the variable's declared type
  • Fields and static methods are NOT polymorphic — they are resolved by the declared (compile-time) type
  • Upcasting is always safe and implicit; downcasting requires instanceof guard + explicit cast
  • Pattern-matching instanceof (Java 16+) combines the check and cast: if (a instanceof Dog dog)
  • Overloaded method selection is based on the declared parameter types at compile time
  • Programming to a supertype (interface/abstract class) lets new subtypes be added without changing callers
PaymentDemo.java
abstract class Payment {
    protected double amount;
    public Payment(double amount) { this.amount = amount; }

    public abstract String process();  // each subclass processes differently

    public void printReceipt() {
        System.out.println("Receipt: " + process() + " — $" + amount);
    }
}

class CreditCard extends Payment {
    private String last4;
    public CreditCard(double amt, String last4) { super(amt); this.last4 = last4; }
    @Override public String process() { return "Credit card *" + last4; }
}

class PayPal extends Payment {
    private String email;
    public PayPal(double amt, String email) { super(amt); this.email = email; }
    @Override public String process() { return "PayPal (" + email + ")"; }
}

class Crypto extends Payment {
    private String wallet;
    public Crypto(double amt, String wallet) { super(amt); this.wallet = wallet; }
    @Override public String process() { return "Crypto wallet " + wallet; }
}

public class PaymentDemo {
    // Works for ALL Payment subtypes — past, present, and future
    static void checkout(Payment payment) {
        payment.printReceipt(); // dynamic dispatch selects correct process()
    }

    public static void main(String[] args) {
        Payment[] payments = {
            new CreditCard(99.99, "4242"),
            new PayPal(49.00, "user@example.com"),
            new Crypto(200.00, "0x1A2B..."),
        };
        for (Payment p : payments) checkout(p);
        // Receipt: Credit card *4242 — $99.99
        // Receipt: PayPal (user@example.com) — $49.0
        // Receipt: Crypto wallet 0x1A2B... — $200.0
    }
}
4

Exception Handling

  • Checked exceptions (extend Exception) must be caught or declared; unchecked (extend RuntimeException) do not
  • Never catch Error — JVM errors like OutOfMemoryError are unrecoverable
  • finally always runs; try-with-resources (Java 7+) is the modern way to close AutoCloseable resources
  • Always chain exceptions with cause: throw new MyException("msg", originalException)
  • Never swallow exceptions with an empty catch block — at minimum log them
  • Catch the narrowest applicable exception type; avoid catching bare Exception or Throwable
ExceptionDemo.java
import java.io.IOException;

public class ExceptionDemo {

    // Checked exception — must be caught or declared with throws
    static String readFile(String path) throws IOException {
        if (path == null) throw new IOException("Path cannot be null");
        return "file content";
    }

    public static void main(String[] args) {
        // Basic try-catch-finally
        try {
            String content = readFile(null);
            System.out.println(content);
        } catch (IOException e) {
            System.out.println("IO error: " + e.getMessage()); // IO error: Path cannot be null
        } finally {
            System.out.println("finally always runs"); // always executes
        }

        // Multi-catch (Java 7+) — handle multiple types in one block
        try {
            String s = null;
            int[] arr = new int[3];
            s.length();        // NullPointerException
            arr[5] = 1;        // ArrayIndexOutOfBoundsException
        } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
            System.out.println("Caught: " + e.getClass().getSimpleName());
        }

        // Exception chaining — wrap root cause
        try {
            try {
                int result = 10 / 0;
            } catch (ArithmeticException e) {
                throw new RuntimeException("Calculation failed", e); // wraps original
            }
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());           // Calculation failed
            System.out.println(e.getCause().getMessage()); // / by zero
        }
    }
}
5

Enums

  • Enum constants are instances of the enum class — fully type-safe, no invalid values possible
  • Enum constructors are implicitly private; values(), ordinal(), name(), valueOf() are built-in
  • Enums with abstract methods let each constant carry its own behaviour
  • EnumSet uses a bit-vector internally — always prefer over HashSet<YourEnum>
  • EnumMap is array-backed by ordinal — faster than HashMap<YourEnum, V>
  • Enums are the best Singleton implementation: thread-safe and serialisation-safe by the JVM spec
EnumBasics.java
public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    public boolean isWeekend() {
        return this == SATURDAY || this == SUNDAY;
    }
}

public class EnumBasics {
    public static void main(String[] args) {
        Day today = Day.WEDNESDAY;

        System.out.println(today.name());    // WEDNESDAY
        System.out.println(today.ordinal()); // 2 (zero-based)
        System.out.println(today.isWeekend()); // false

        // Iterate all constants
        for (Day d : Day.values()) {
            System.out.print(d + " ");
        }
        System.out.println();

        // Parse from String
        Day friday = Day.valueOf("FRIDAY");
        System.out.println(friday.isWeekend()); // false

        // Switch expression — compiler verifies exhaustiveness
        String type = switch (today) {
            case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday";
            case SATURDAY, SUNDAY -> "Weekend";
        };
        System.out.println(type);  // Weekday
    }
}
6

Wrapper Classes

  • All numeric wrappers extend Number — intValue(), doubleValue() etc. convert between types
  • Integer cache covers −128 to 127: use .equals() for all wrapper comparisons, never ==
  • Unboxing a null wrapper throws NullPointerException — guard with null checks or Optional
  • parseInt() vs valueOf(): parseInt returns a primitive; valueOf returns a (possibly cached) wrapper
  • Autoboxing in tight loops is costly — use primitive arrays or streams for numeric aggregation
  • Character has rich utility: isDigit(), isLetter(), isWhitespace(), toUpperCase(), toLowerCase()
WrapperConstants.java
public class WrapperConstants {
    public static void main(String[] args) {
        // Range constants
        System.out.println(Integer.MAX_VALUE);   //  2147483647
        System.out.println(Integer.MIN_VALUE);   // -2147483648
        System.out.println(Long.MAX_VALUE);      //  9223372036854775807
        System.out.println(Double.MAX_VALUE);    //  1.7976931348623157E308

        // Parsing — String → primitive
        int  i = Integer.parseInt("42");
        long l = Long.parseLong("9876543210");
        double d = Double.parseDouble("3.14");
        boolean b = Boolean.parseBoolean("true"); // case-insensitive
        System.out.println(i + " " + l + " " + d + " " + b);

        // Conversion methods
        System.out.println(Integer.toBinaryString(255));  // 11111111
        System.out.println(Integer.toHexString(255));     // ff
        System.out.println(Integer.toOctalString(8));     // 10
        System.out.println(Integer.bitCount(255));        // 8
        System.out.println(Integer.reverse(1));           // MSB becomes LSB

        // Number hierarchy — any Number can give any numeric primitive
        Number n = 3.7;               // Double is-a Number
        System.out.println(n.intValue());    // 3  (truncates)
        System.out.println(n.longValue());   // 3
        System.out.println(n.doubleValue()); // 3.7

        // Character utilities
        System.out.println(Character.isDigit('5'));    // true
        System.out.println(Character.isLetter('A'));   // true
        System.out.println(Character.toLowerCase('Z')); // z
        System.out.println(Character.isWhitespace(' ')); // true
    }
}
7

Type Casting

  • Widening (byte→int→long→double) is implicit; narrowing requires an explicit cast and can lose data
  • Narrowing truncates toward zero — (int) 3.9 = 3; it never rounds
  • Byte arithmetic is promoted to int — (byte)(a + b) needs the explicit cast back
  • Upcasting is always safe; downcasting needs instanceof guard or throws ClassCastException
  • Pattern-matching instanceof (Java 16+) is the modern way: if (a instanceof Dog dog)
  • Array covariance (String[] IS-A Object[]) allows upcast but ArrayStoreException on wrong insert
PrimitiveCasting.java
public class PrimitiveCasting {
    public static void main(String[] args) {
        // Widening — implicit, safe
        int   i = 100;
        long  l = i;         // int → long: implicit
        float f = l;         // long → float: implicit (may lose precision!)
        double d = f;        // float → double: implicit
        System.out.println(d); // 100.0

        // Precision loss: long → float
        long big = 123_456_789_123L;
        float approx = big;          // implicit widening
        System.out.println(big);     // 123456789123
        System.out.println(approx);  // 1.23456794E11 — precision lost!

        // Narrowing — explicit cast required
        double pi = 3.14159;
        int truncated = (int) pi;     // truncates, does NOT round
        System.out.println(truncated); // 3

        // Truncation wraps on overflow
        int big2   = 300;
        byte small = (byte) big2;     // 300 % 256 = 44
        System.out.println(small);    // 44

        // Numeric promotion in expressions
        byte a = 10, b = 20;
        // byte result = a + b;  // compile error! a+b is promoted to int
        byte result = (byte)(a + b);  // explicit cast back
        System.out.println(result);   // 30

        // char ↔ int casting
        char c = 'A';
        int ascii = c;               // widening char → int
        System.out.println(ascii);   // 65
        char back = (char)(ascii + 1); // narrowing int → char
        System.out.println(back);    // B
    }
}
8

Packages & Imports

  • Package name maps 1-to-1 to directory structure — javac enforces this
  • Package-private (no modifier) is the default — accessible within the same package only
  • Use reverse-domain naming: com.company.project.module — never use java.* or javax.*
  • Static imports (import static) bring static members into scope — great for Math, assertions
  • Name conflicts: import only one; use the fully-qualified name for the other
  • Never use the default (unnamed) package in production — classes there cannot be imported
Package structure
// File: src/com/example/model/User.java
package com.example.model;

public class User {
    private String email;       // only this class
    String username;            // package-private — visible to all in com.example.model
    protected int age;          // package + subclasses
    public String displayName;  // everywhere

    public User(String email, String username, int age) {
        this.email       = email;
        this.username    = username;
        this.age         = age;
        this.displayName = username;
    }
    public String getEmail() { return email; }
}

// File: src/com/example/model/UserRepository.java
package com.example.model;   // same package

public class UserRepository {
    public User findByUsername(String username) {
        User u = new User("a@b.com", username, 25);
        // Can access package-private field directly — same package
        System.out.println("Looking for: " + u.username);
        return u;
    }
}

// File: src/com/example/service/UserService.java
package com.example.service;  // different package

import com.example.model.User; // must import

public class UserService {
    public void greet(User user) {
        System.out.println("Hello, " + user.displayName); // public — OK
        // user.username; // compile error — package-private, different package
        // user.age;      // compile error — protected, not a subclass
    }
}
9

static Keyword

  • Static fields are class-level — one shared copy for all instances; changes affect all
  • Static methods cannot access instance fields or use this — only static members
  • Static initialisers run once when the class is first loaded, in declaration order
  • Static nested classes have no enclosing-instance reference; non-static inner classes do
  • Non-static inner classes holding outer references can cause memory leaks if they outlive the outer object
  • Call static members on the class name (Counter.getCount()), not on an instance variable
Counter.java
public class Counter {
    // Static field — one per class, shared by all instances
    private static int count = 0;

    // Instance field — one per object
    private final int id;
    private String name;

    public Counter(String name) {
        this.name = name;
        this.id   = ++count;  // increment shared counter
    }

    // Static method — belongs to class, no 'this'
    public static int getCount() { return count; }

    // Static constant — public static final by convention in UPPER_SNAKE_CASE
    public static final int MAX_INSTANCES = 100;

    // Static utility method (no state needed)
    public static boolean isValidName(String name) {
        return name != null && !name.isBlank() && name.length() <= 50;
    }

    @Override public String toString() { return "Counter#" + id + "(" + name + ")"; }

    public static void main(String[] args) {
        Counter a = new Counter("alpha");
        Counter b = new Counter("beta");
        Counter c = new Counter("gamma");

        System.out.println(a);                // Counter#1(alpha)
        System.out.println(Counter.getCount()); // 3 — class-level call
        System.out.println(c.getCount());       // also 3 — works but misleading

        System.out.println(Counter.isValidName("hello")); // true
        System.out.println(Counter.MAX_INSTANCES);        // 100
    }
}
10

final Keyword

  • final variable: assign once — for fields, in declaration or constructor (blank final)
  • final does not make an object immutable — it only prevents reassigning the reference
  • final method: cannot be overridden — use for security-sensitive or template skeleton methods
  • final class: cannot be subclassed — String and all wrappers are final
  • Effectively final (Java 8+): never reassigned after init; can be captured in lambdas
  • static final primitive constants are inlined by the compiler at call sites
FinalDemo.java
import java.util.ArrayList;
import java.util.List;

public class FinalDemo {
    // Static constant — public static final, UPPER_SNAKE_CASE
    public static final double TAX_RATE = 0.18;

    // Blank final — assigned in constructor, not at declaration
    private final String id;
    private final List<String> items = new ArrayList<>(); // final ref, mutable object!

    public FinalDemo(String id) {
        this.id = id;  // assigned exactly once
        // this.id = "other"; // compile error — already assigned
    }

    public void addItem(String item) {
        items.add(item);    // OK — final only prevents reassigning the reference
        // items = new ArrayList<>(); // compile error — cannot reassign final field
    }

    public static void main(String[] args) {
        FinalDemo demo = new FinalDemo("order-42");
        demo.addItem("book");
        demo.addItem("pen");
        System.out.println(demo.items); // [book, pen]

        // final local variable — effectively like a constant in scope
        final int MAX = 10;
        // MAX = 20; // compile error

        // Effectively final (Java 8+) — not declared final but never reassigned
        String prefix = "Hello";  // effectively final
        Runnable r = () -> System.out.println(prefix + " World"); // OK in lambda
        r.run();

        // String prefix2 = "Hello";
        // prefix2 = "Hi"; // reassigned — NOT effectively final
        // Runnable r2 = () -> System.out.println(prefix2); // compile error
    }
}
11

Inner and Nested Classes

  • Static nested class: no enclosing instance reference; use for Builder, helper types.
  • Inner class: implicitly holds enclosing instance reference; can cause memory leaks.
  • Anonymous class: one-shot inline implementation; prefer lambda for single-method interfaces.
  • Local class: defined inside a method; captures effectively-final variables.
  • To create an inner class instance from outside: outer.new Inner().
StaticNested.java
public class Outer {
    private static int staticField = 10;
    private int instanceField = 20;

    // Static nested — no reference to Outer instance
    public static class Builder {
        private String name;
        private int age;

        public Builder name(String name) {
            this.name = name; return this;
        }
        public Builder age(int age) {
            this.age = age; return this;
        }
        public Person build() {
            return new Person(name, age);
        }

        // Can access outer static members
        void show() { System.out.println(staticField); }
        // void bad() { System.out.println(instanceField); } // ERROR
    }
}

// Instantiate without an Outer instance
Outer.Builder b = new Outer.Builder().name("Alice").age(30);
12

Annotations

  • @Override, @Deprecated, @SuppressWarnings, @FunctionalInterface are built-in compiler annotations.
  • Define custom annotations with @interface; control scope with @Retention and @Target.
  • RetentionPolicy.RUNTIME is required for runtime access via reflection.
  • ElementType controls where an annotation can be applied (METHOD, TYPE, FIELD, etc.).
  • Annotation processors (APT) run at compile time and can generate new source files.
BuiltinAnnotations.java
public class Animal {
    public String sound() { return "..."; }
}

public class Dog extends Animal {
    @Override              // compile error if sound() doesn't exist in Animal
    public String sound() { return "Woof"; }

    @Deprecated(since = "2.0", forRemoval = true)
    public void oldMethod() { /* will be removed */ }

    @SuppressWarnings("unchecked")
    public void uncheckedOp(Object obj) {
        List<String> list = (List<String>) obj; // suppresses warning
    }
}

@FunctionalInterface
interface Transformer<T, R> {
    R transform(T input);
    // adding a second abstract method here → compile error
}
13

Reflection API

  • Class<?> is the entry point; obtain via .class, getClass(), or Class.forName().
  • getDeclaredXxx() returns all members; getXxx() returns only public/inherited ones.
  • setAccessible(true) bypasses private access — use cautiously; blocked by JPMS opens.
  • Method.invoke() is slower than direct calls; use MethodHandle for performance-critical reflection.
  • Generic types are erased at runtime but preserved in field/method signatures — accessible via getGenericType().
ReflectFields.java
import java.lang.reflect.*;

Class<?> cls = User.class;

// Basic info
System.out.println(cls.getName());         // com.example.User
System.out.println(cls.getSimpleName());   // User
System.out.println(cls.getSuperclass());   // class java.lang.Object

// Fields
for (Field field : cls.getDeclaredFields()) {
    System.out.printf("  %-20s [%s]%n",
        field.getName(),
        field.getType().getSimpleName());
}

// Access private field
Field nameField = cls.getDeclaredField("name");
nameField.setAccessible(true); // bypass access control
User user = new User("Alice", 30);
String name = (String) nameField.get(user);
System.out.println("Private name: " + name);
14

Advanced Generics

  • PECS: Producer Extends (read), Consumer Super (write).
  • <? extends T> allows reading as T; <? super T> allows writing T into the structure.
  • Recursive bounds <T extends Comparable<T>> constrain T to self-comparable types.
  • Type erasure: List<String> and List<Integer> are the same class at runtime.
  • Workarounds for erasure: Class<T> token, TypeReference anonymous subclass, or @SuppressWarnings("unchecked") cast.
Wildcards.java
// Upper bounded — read from (producer)
public double sumList(List<? extends Number> list) {
    double sum = 0;
    for (Number n : list) sum += n.doubleValue(); // can READ
    // list.add(1.5); // COMPILE ERROR — can't write
    return sum;
}
sumList(new ArrayList<Integer>());  // works
sumList(new ArrayList<Double>());   // works

// Lower bounded — write to (consumer)
public void addNumbers(List<? super Integer> list) {
    list.add(1);    // can WRITE Integer or subtype
    list.add(2);
    // Integer i = list.get(0); // COMPILE ERROR — can only get Object
}
addNumbers(new ArrayList<Integer>());  // works
addNumbers(new ArrayList<Number>());   // works
addNumbers(new ArrayList<Object>());   // works

// PECS in Collections.copy
// src is producer (we read from it)  → extends
// dest is consumer (we write to it)  → super
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T t : src) dest.add(t);
}
15

String Internals

  • String is immutable — every "modification" creates a new object.
  • String literals are pooled; new String("x") creates a separate heap object.
  • Always use equals() for content comparison, never == (unless you know both are interned).
  • Java 9+ Compact Strings store Latin-1 text as byte[] — half the memory of char[].
  • substring() creates a new String (O(n) copy) — not O(1) as in some other languages.
StringPool.java
// String literals are pooled automatically
String a = "hello";
String b = "hello";
System.out.println(a == b);       // true  — same pool object
System.out.println(a.equals(b));  // true  — same content

// new String() always creates a new heap object
String c = new String("hello");
System.out.println(a == c);       // false — different object
System.out.println(a.equals(c));  // true  — same content

// intern() moves heap string into pool
String d = c.intern();
System.out.println(a == d);       // true  — now same pool object

// String pool lives in Heap (since Java 7)
// Pre-Java 7 it was in PermGen — caused OOM for large apps

// Immutability means every "modification" creates a new String
String s = "hello";
s.concat(" world"); // returns new String, original unchanged
String result = s.concat(" world"); // must capture the return
16

hashCode and equals Contract

  • equals() true → hashCode() must be equal. hashCode() equal does NOT imply equals() true.
  • Always override hashCode when you override equals — IDEs and Lombok do this automatically.
  • Use Objects.hash(field1, field2, ...) for a clean, collision-resistant hashCode.
  • Cache hashCode in immutable objects for performance (see String).
  • Bad hashCode (e.g. constant) degrades HashMap to O(n) — evenly distributing hashes is important.
BrokenContract.java
// BROKEN — equals without hashCode
public class BrokenPoint {
    int x, y;
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof BrokenPoint p)) return false;
        return x == p.x && y == p.y;
    }
    // hashCode not overridden — uses Object's identity hash
}

BrokenPoint p1 = new BrokenPoint(1, 2);
BrokenPoint p2 = new BrokenPoint(1, 2);
System.out.println(p1.equals(p2));    // true
System.out.println(p1.hashCode() == p2.hashCode()); // false (probably)

Set<BrokenPoint> set = new HashSet<>();
set.add(p1);
set.contains(p2); // FALSE — looks in wrong bucket!

Map<BrokenPoint, String> map = new HashMap<>();
map.put(p1, "origin");
map.get(p2); // NULL — same bug
17

Immutability

  • Immutable classes: final class, private final fields, no setters, defensive copies in/out.
  • Defensive copy in constructor prevents the caller from mutating internal state indirectly.
  • Never return a mutable internal field reference — return a copy or unmodifiable view.
  • java.time types (LocalDate, Instant) are immutable — prefer them over java.util.Date.
  • Records are shallowly immutable — use List.copyOf() in compact constructors for mutable components.
ImmutableClass.java
// Truly immutable class
public final class DateRange {                // 1. final class
    private final LocalDate start;            // 2. private final
    private final LocalDate end;
    private final List<String> notes;         // mutable field!

    public DateRange(LocalDate start, LocalDate end, List<String> notes) {
        if (start.isAfter(end))
            throw new IllegalArgumentException("start must be before end");
        this.start = start;
        this.end   = end;
        this.notes = List.copyOf(notes);      // 4. defensive copy → unmodifiable
    }

    public LocalDate getStart() { return start; }  // 3. no setters
    public LocalDate getEnd()   { return end;   }

    public List<String> getNotes() {
        return notes;   // 5. safe — List.copyOf returned an unmodifiable list
    }

    // Wither method — returns new instance with one field changed
    public DateRange withStart(LocalDate newStart) {
        return new DateRange(newStart, end, notes);
    }
}
18

Anonymous Classes

  • Anonymous class = nameless class declared and instantiated inline: new Interface() { ... }.
  • Can implement interfaces or extend classes; can have fields and methods but no constructors.
  • Captures effectively-final variables from the enclosing scope.
  • Use lambda for single-method functional interfaces; use anonymous class for multiple methods or local state.
  • TypeReference<List<User>>(){} is an important anonymous class use case that cannot be a lambda.
AnonymousClassSyntax.java
// Anonymous class implementing an interface
Runnable r = new Runnable() {
    private int runCount = 0; // can have fields

    @Override
    public void run() {
        runCount++;
        System.out.println("Run #" + runCount);
    }
};
r.run(); // Run #1
r.run(); // Run #2

// Anonymous class extending an abstract class
abstract class Greeter {
    abstract String greeting();
    void greet(String name) {
        System.out.println(greeting() + ", " + name + "!");
    }
}

Greeter formal = new Greeter() {
    @Override
    String greeting() { return "Good day"; }
    // inherits greet() from Greeter
};
formal.greet("Alice"); // Good day, Alice!

// Capturing effectively-final variable from enclosing scope
String prefix = "Hello"; // effectively final
Greeter casual = new Greeter() {
    @Override String greeting() { return prefix; } // captures prefix
};
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/java