Last Minute Notes

Java Fundamentals

Core Java language notes: types, strings, OOP in Java, exceptions, enums, records, and modern language features for interviews.

📘 Java Fundamentals

Table of Contents

  1. The Java Platform
  2. Data Types and Variables
  3. Wrapper Classes and Autoboxing
  4. Pass by Value
  5. Strings
  6. Control Flow and Modern Switch
  7. Arrays
  8. Classes, Constructors, and Initialization Order
  9. Access Modifiers, static, and final
  10. The Object Class: equals, hashCode, toString
  11. Inheritance and Polymorphism in Java
  12. Abstract Classes vs Interfaces
  13. Nested and Inner Classes
  14. Enums
  15. Records and Sealed Types
  16. Immutability
  17. Exception Handling
  18. Modern Java Feature Cheat Sheet
  19. Common Pitfalls
  20. Common Interview Questions
  21. One-Line Senior Summary

1. The Java Platform

Java is a statically typed, object-oriented, compiled-to-bytecode language. The source is compiled by javac into platform-independent bytecode (.class files), which the JVM executes. This is the "write once, run anywhere" idea: the bytecode is portable, and each OS has its own JVM.

1.1 JDK vs JRE vs JVM

TermWhat it is
JVMVirtual machine that loads, verifies, and executes bytecode. Includes the JIT compiler and garbage collector.
JREJVM plus the standard class libraries needed to run programs.
JDKJRE plus developer tools (javac, jar, jshell, javadoc, profilers).

Since Java 11 there is no separate JRE download for most distributions, you install a JDK.

1.2 Compile and Run Flow

javac Hello.java      # produces Hello.class (bytecode)
java Hello            # JVM loads Hello.class and runs main

java Hello.java       # Java 11+: compile in memory and run a single-file program
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Java");
    }
}
  • The entry point is public static void main(String[] args).
  • A public top-level class must live in a file with the same name (Hello.java).
  • One file can hold at most one public top-level class.

1.3 How the JVM Runs Code

The JVM starts by interpreting bytecode. The JIT (Just-In-Time) compiler watches for hot methods and compiles them to native machine code at runtime. That is why Java programs are slower during warm-up and then get very fast, and why microbenchmarks need a warm-up phase.

Flowchart: From Source to Running Code

The JVM interprets first, then compiles only the hot methods to native code, which is why Java has a warm-up period.

1.4 LTS Versions

Java ships every 6 months, but only Long-Term-Support releases are commonly used in production: 8, 11, 17, 21. These notes target Java 17 and 21 and call out the version where a feature arrived.


2. Data Types and Variables

Java has 8 primitive types and everything else is a reference type.

TypeSizeRange / notesDefault
byte8 bit-128 to 1270
short16 bit-32,768 to 32,7670
int32 bitabout -2.1 billion to 2.1 billion0
long64 bitabout -9.2e18 to 9.2e180L
float32 bitabout 7 decimal digits of precision0.0f
double64 bitabout 15 decimal digits of precision0.0d
char16 bitone UTF-16 code unit, unsigned (0 to 65,535)'\u0000'
booleanJVM-dependenttrue or false onlyfalse
  • Local variables have no default value, they must be assigned before use or the code will not compile.
  • Fields (instance and static) get the defaults above, and reference fields default to null.

2.1 Literals

long big = 3_000_000_000L;   // L suffix required beyond int range; underscores aid readability
float f = 3.14f;              // f suffix required, otherwise it is a double
int hex = 0xFF;               // 255
int bin = 0b1010;             // 10
char c = 'A';
var list = new ArrayList<String>(); // Java 10+: local variable type inference

var only works for local variables with an initializer. The type is still fixed at compile time, it is not dynamic typing.

2.2 Type Conversion

Widening (smaller to larger) is implicit and safe. Narrowing (larger to smaller) needs an explicit cast and may lose data.

int i = 100;
long l = i;            // widening, implicit
double d = i;          // widening, implicit

double pi = 3.99;
int truncated = (int) pi;   // 3, decimals dropped (not rounded)

int big = 130;
byte b = (byte) big;        // -126, overflow wraps around

Arithmetic on byte, short, and char is promoted to int first:

byte a = 10, b = 20;
// byte c = a + b;          // compile error: a + b is an int
byte c = (byte) (a + b);    // OK

char ch = 'a';
System.out.println(ch + 1);        // 98 (int)
System.out.println((char) (ch + 1)); // b
ch += 1;                            // OK: compound assignment casts implicitly

2.3 Integer Math Gotchas

System.out.println(5 / 2);          // 2   (integer division truncates toward zero)
System.out.println(-5 / 2);         // -2
System.out.println(-5 % 2);         // -1  (result takes the sign of the dividend)
System.out.println(5 / 2.0);        // 2.5

System.out.println(Integer.MAX_VALUE + 1); // -2147483648, silent overflow

// Detect overflow instead of wrapping
Math.addExact(Integer.MAX_VALUE, 1);       // throws ArithmeticException

2.4 Floating Point Precision

float and double use binary floating point, so many decimal fractions cannot be represented exactly.

System.out.println(0.1 + 0.2);           // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);    // false

For money or anything needing exact decimals, use BigDecimal and construct it from a String, not a double:

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b));                    // 0.3
System.out.println(new BigDecimal(0.1));         // 0.1000000000000000055511151231257827...

Also, double division by zero gives Infinity or NaN, while int division by zero throws ArithmeticException.


3. Wrapper Classes and Autoboxing

Each primitive has an object wrapper: Integer, Long, Double, Boolean, Character, Byte, Short, Float. Wrappers are required for generics (List<Integer>, never List<int>) and can hold null.

Autoboxing is the automatic primitive to wrapper conversion, and unboxing is the reverse.

Integer boxed = 42;        // autoboxing: Integer.valueOf(42)
int prim = boxed;          // unboxing: boxed.intValue()

List<Integer> nums = new ArrayList<>();
nums.add(5);               // autoboxed
int first = nums.get(0);   // unboxed

3.1 The Integer Cache Trap

Integer.valueOf caches instances for values -128 to 127. That makes == behave inconsistently:

Integer a = 127, b = 127;
System.out.println(a == b);       // true  (same cached object)

Integer c = 128, d = 128;
System.out.println(c == d);       // false (two different objects)
System.out.println(c.equals(d));  // true

Always compare wrapper objects with .equals(), never ==.

3.2 Unboxing NullPointerException

Map<String, Integer> stock = new HashMap<>();
int count = stock.get("apple");   // NullPointerException: get returns null, unboxing null fails

int safe = stock.getOrDefault("apple", 0); // fix

Boxing inside hot loops also allocates objects, so prefer primitives (int, IntStream) in performance-sensitive code.


4. Pass by Value

Java is always pass by value. For objects, the value being copied is the reference, not the object itself.

static void reassign(StringBuilder sb) {
    sb = new StringBuilder("new");   // changes only the local copy of the reference
}

static void mutate(StringBuilder sb) {
    sb.append(" world");             // follows the copied reference to the SAME object
}

StringBuilder sb = new StringBuilder("hello");
reassign(sb);
System.out.println(sb);   // hello
mutate(sb);
System.out.println(sb);   // hello world
  • Mutating the object through the parameter is visible to the caller.
  • Reassigning the parameter is not.
  • Primitives are copied entirely, so changes to them never propagate.

Flowchart: Pass by Value With Objects

The reference is copied, so the method can change the object but cannot change where the caller points.


5. Strings

String is an immutable class. Every "modification" returns a new String and leaves the original untouched.

String s = "hello";
s.toUpperCase();              // result discarded, s is still "hello"
s = s.toUpperCase();          // now s refers to a new "HELLO"

5.1 The String Pool

String literals are stored in the string pool (a special area inside the heap since Java 7) and reused.

String a = "java";
String b = "java";
String c = new String("java");
String d = c.intern();

System.out.println(a == b);        // true  (same pooled object)
System.out.println(a == c);        // false (new String always creates a new heap object)
System.out.println(a.equals(c));   // true  (same characters)
System.out.println(a == d);        // true  (intern returns the pooled instance)

Always use .equals() for string content comparison. == compares references.

Why is String immutable?

  • Security: strings hold file paths, URLs, and credentials that must not change after validation.
  • Thread safety: an immutable object can be shared freely without locking.
  • Caching: the pool is only safe because strings never change, and hashCode can be cached, which makes strings ideal HashMap keys.

Flowchart: The String Pool

Literals share one pooled object, while new String always makes a separate one, which is why == and equals differ.

5.2 StringBuilder vs StringBuffer

Repeated + in a loop creates many temporary strings (O(n^2) copying). Use a mutable builder instead.

// Bad
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i;
}

// Good
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result2 = sb.toString();
ClassMutableThread-safeSpeed
StringNoYes (immutable)Slow for repeated concatenation
StringBuilderYesNoFast, the default choice
StringBufferYesYes (synchronized methods)Slower, rarely needed now

5.3 Commonly Used Methods

String s = "  Hello, World  ";

s.trim();                       // "Hello, World"  (strip() is Unicode-aware, Java 11+)
s.length();                     // 16
s.charAt(2);                    // 'H'
s.substring(2, 7);              // "Hello"  (end index is exclusive)
s.indexOf("World");             // 9
s.contains("Hello");            // true
s.replace("l", "L");            // "  HeLLo, WorLd  "
s.toLowerCase();
s.isBlank();                    // Java 11+: true for empty or whitespace-only
"a,b,c".split(",");             // ["a", "b", "c"]
String.join("-", "a", "b");     // "a-b"
"ab".repeat(3);                 // "ababab" (Java 11+)
String.valueOf(42);             // "42"
"Java".equalsIgnoreCase("JAVA"); // true

5.4 Formatting and Text Blocks

String msg = String.format("%s scored %d (%.1f%%)", "Sam", 42, 87.456);
// Sam scored 42 (87.5%)

// Java 15+: text block preserves line breaks, no escaping needed for quotes
String json = """
    {
      "name": "Sam",
      "age": 30
    }
    """;

6. Control Flow and Modern Switch

6.1 Loops

for (int i = 0; i < 5; i++) { /* ... */ }

for (String name : names) { /* enhanced for over arrays and Iterables */ }

while (condition) { /* ... */ }
do { /* runs at least once */ } while (condition);

outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (j == 2) continue outer;   // labeled continue
        if (i == 2) break outer;      // labeled break
    }
}

6.2 Classic Switch and Fall-Through

The classic switch falls through to the next case unless you break.

switch (day) {
    case 1:
        System.out.println("Mon");
        // no break: falls through into case 2
    case 2:
        System.out.println("Tue");
        break;
    default:
        System.out.println("Other");
}

6.3 Switch Expressions (Java 14+)

Arrow form: no fall-through, no break, and the switch returns a value.

String type = switch (day) {
    case 6, 7 -> "Weekend";
    case 1, 2, 3, 4, 5 -> "Weekday";
    default -> throw new IllegalArgumentException("Invalid day: " + day);
};

int len = switch (word) {
    case "one" -> 1;
    case "two" -> 2;
    default -> {
        System.out.println("unknown");
        yield 0;               // yield returns a value from a block
    }
};

6.4 Pattern Matching

// Java 16+: pattern matching for instanceof
if (obj instanceof String s && s.length() > 3) {
    System.out.println(s.toUpperCase());   // s is already cast and in scope
}

// Java 21: pattern matching for switch, with guards and null handling
static String describe(Object o) {
    return switch (o) {
        case null -> "null";
        case Integer i when i > 100 -> "big int " + i;
        case Integer i -> "int " + i;
        case String s -> "string of length " + s.length();
        default -> "something else";
    };
}

7. Arrays

Arrays are fixed-size, zero-indexed objects that live on the heap.

int[] a = new int[5];              // [0, 0, 0, 0, 0]
int[] b = {1, 2, 3};
int[][] grid = new int[3][4];      // 3 rows, 4 columns
int[][] jagged = new int[3][];     // rows can have different lengths
jagged[0] = new int[2];

System.out.println(b.length);      // length is a field, not a method
b[3] = 10;                         // ArrayIndexOutOfBoundsException

7.1 The Arrays Utility Class

int[] nums = {5, 3, 9, 1};

Arrays.sort(nums);                          // [1, 3, 5, 9]  (dual-pivot quicksort for primitives)
Arrays.binarySearch(nums, 5);               // 2 (array must be sorted)
Arrays.toString(nums);                      // "[1, 3, 5, 9]"
Arrays.deepToString(new int[][]{{1}, {2}}); // "[[1], [2]]"
Arrays.fill(nums, 0);
int[] copy = Arrays.copyOf(nums, 6);        // pads with defaults if longer
int[] part = Arrays.copyOfRange(nums, 1, 3);
Arrays.equals(new int[]{1}, new int[]{1});  // true (== would be false)

List<String> fixed = Arrays.asList("a", "b"); // fixed-size list backed by the array
fixed.set(0, "x");                             // OK
// fixed.add("c");                             // UnsupportedOperationException

Printing an array directly (System.out.println(nums)) shows something like [I@1b6d3586, so always use Arrays.toString.

7.2 Array Covariance

Arrays are covariant and checked at runtime, unlike generics:

Object[] objs = new String[2];
objs[0] = 42;   // compiles, but throws ArrayStoreException at runtime

8. Classes, Constructors, and Initialization Order

public class Person {
    private final String name;
    private int age;

    public Person(String name) {
        this(name, 0);                  // constructor chaining with this(...)
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
  • If you write no constructor, the compiler adds a default no-arg constructor.
  • If you write any constructor, the default one is not generated.
  • this(...) or super(...) must be the first statement in a constructor.
  • Every constructor implicitly calls super() first if you do not write it, so the parent needs an accessible no-arg constructor, or you must call super(args) explicitly.
  • Constructors are not inherited and cannot be abstract, static, or final.

8.1 Initialization Order

class Parent {
    static { System.out.println("1. Parent static block"); }
    { System.out.println("3. Parent instance block"); }
    Parent() { System.out.println("4. Parent constructor"); }
}

class Child extends Parent {
    static { System.out.println("2. Child static block"); }
    { System.out.println("5. Child instance block"); }
    Child() { System.out.println("6. Child constructor"); }
}

new Child();

Output order:

  1. Parent static block (once, when the class is first loaded)
  2. Child static block
  3. Parent instance block
  4. Parent constructor
  5. Child instance block
  6. Child constructor

Rule: static init runs top-down parent to child once per class, then for each new, parent instance init and constructor run before the child's.

A classic bug follows from this: calling an overridable method from a constructor. The child override may run before the child's fields are initialized.

class Base {
    Base() { init(); }              // calls overridden method
    void init() {}
}

class Derived extends Base {
    private String value = "set";
    @Override void init() {
        System.out.println(value);  // prints null: Derived's field initializers have not run yet
    }
}

Flowchart: Initialization Order

Static blocks run once per class, parent first, then every new runs parent init and constructor before the child.


9. Access Modifiers, static, and final

9.1 Access Modifiers

ModifierSame classSame packageSubclass (other package)Everywhere
privateYesNoNoNo
default (none)YesYesNoNo
protectedYesYesYesNo
publicYesYesYesYes

Default to the most restrictive modifier that works.

9.2 static

static members belong to the class, not to any instance.

class Counter {
    private static int total = 0;      // one copy shared by all instances
    private int id;

    Counter() { id = ++total; }

    static int getTotal() { return total; }   // cannot touch instance fields or use this
}
  • A static method cannot access instance members directly, there is no this.
  • static methods are hidden, not overridden, and resolved at compile time by the declared type.
  • A static block runs once when the class is loaded.

9.3 final

Applied toMeaning
variableCan be assigned only once. For references, the reference is fixed but the object can still change.
methodCannot be overridden.
classCannot be extended (String, Integer, and other wrappers are final).
final List<String> names = new ArrayList<>();
names.add("Sam");              // OK: the list is mutable
// names = new ArrayList<>();  // compile error: cannot reassign

final, finally, and finalize are three unrelated things. finalize() is deprecated for removal and should never be used, prefer try-with-resources or Cleaner.


10. The Object Class: equals, hashCode, toString

Every class extends java.lang.Object, which gives you equals, hashCode, toString, getClass, clone, and the wait/notify methods.

10.1 The equals and hashCode Contract

  • equals must be reflexive, symmetric, transitive, consistent, and x.equals(null) must be false.
  • If a.equals(b) then a.hashCode() == b.hashCode().
  • The reverse is not required: equal hash codes do not imply equality (that is a collision).
  • If you override equals, you must override hashCode.
public class Point {
    private final int x, y;

    public Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;   // pattern matching, Java 16+
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }
}

What breaks if you override only equals:

Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
System.out.println(set.contains(new Point(1, 2)));
// false if hashCode is not overridden: the two objects land in different buckets

Also, never use mutable fields in hashCode for objects stored in hash-based collections. If the key changes after insertion, the entry can no longer be found.

10.2 Reference vs Content Equality

  • == on objects compares references (identity).
  • equals compares logical equality, and the default Object.equals is just ==.

11. Inheritance and Polymorphism in Java

Java has single class inheritance (extends one class) but multiple interface implementation.

class Animal {
    String sound() { return "..."; }
}

class Dog extends Animal {
    @Override
    String sound() { return "Woof"; }
}

Animal a = new Dog();          // upcasting, always safe and implicit
System.out.println(a.sound()); // Woof: method chosen at runtime from the actual object
Dog d = (Dog) a;               // downcasting needs an explicit cast, can throw ClassCastException

11.1 Overriding Rules

  • Same name and parameter list.
  • Return type must be the same or a covariant (more specific) type.
  • Access level cannot be narrower than the parent's.
  • It cannot throw broader checked exceptions than the parent method.
  • private, static, and final methods cannot be overridden.
  • Always add @Override so the compiler catches typos.

11.2 Overloading vs Overriding

OverloadingOverriding
WhereSame class (or subclass)Subclass vs superclass
SignatureDifferent parameter listSame
ResolvedCompile time (by declared static types)Runtime (by actual object type)
Return typeCan differSame or covariant
class Printer {
    void print(Object o) { System.out.println("Object"); }
    void print(String s) { System.out.println("String"); }
}

Object x = "hello";
new Printer().print(x);        // Object: overload chosen by the declared type of x
new Printer().print("hello");  // String

11.3 Fields and Static Methods Are Not Polymorphic

Only instance methods are dispatched dynamically. Field access and static method calls use the declared type.

class A { String name = "A"; String get() { return "A.get"; } }
class B extends A { String name = "B"; String get() { return "B.get"; } }

A obj = new B();
System.out.println(obj.name);   // A       (field hiding, resolved at compile time)
System.out.println(obj.get());  // B.get   (overriding, resolved at runtime)

11.4 super

class Manager extends Employee {
    Manager(String name) {
        super(name);                    // call parent constructor
    }

    @Override
    double pay() {
        return super.pay() + 1000;      // reuse parent behavior
    }
}

12. Abstract Classes vs Interfaces

abstract class Shape {
    private final String color;

    Shape(String color) { this.color = color; }   // abstract classes can have constructors and state

    abstract double area();                         // no body, subclasses must implement

    String describe() { return color + " shape of area " + area(); }
}

interface Drawable {
    void draw();                                    // implicitly public abstract

    default void drawTwice() { draw(); draw(); }    // Java 8+: default method with a body

    static Drawable noop() { return () -> {}; }     // Java 8+: static method

    private void helper() {}                        // Java 9+: private helper for defaults
}

class Circle extends Shape implements Drawable, Comparable<Circle> { /* ... */ }
Abstract classInterface
InheritanceA class extends oneA class implements many
StateInstance fields, constructorsOnly public static final constants
MethodsAbstract and concrete, any access modifierAbstract, default, static, private
Use it forShared code and state among closely related classes ("is-a")A capability or contract ("can-do")

An interface with exactly one abstract method is a functional interface and can be implemented with a lambda.

Flowchart: Abstract Class or Interface?

Ask whether you are modelling what something is or what it can do.

12.1 The Diamond Problem

If two interfaces provide the same default method, the implementing class must override it to resolve the conflict.

interface A { default String hello() { return "A"; } }
interface B { default String hello() { return "B"; } }

class C implements A, B {
    @Override
    public String hello() {
        return A.super.hello();     // explicitly pick one
    }
}

Flowchart: The Diamond Problem

The compiler never guesses which default method you meant.


13. Nested and Inner Classes

KindDeclaredHolds outer instance?Use case
Static nestedstatic class X inside a classNoHelper type tied to the outer class (Builder, Map.Entry)
Inner (non-static)class X inside a classYesObject that needs the enclosing instance's state
LocalInside a methodYes (plus captured locals)Rare
Anonymousnew Type() { ... } inlineYesOne-off implementation
class Outer {
    private int value = 10;

    static class StaticNested {
        // cannot access Outer.this.value
    }

    class Inner {
        int read() { return value; }   // has an implicit reference to Outer.this
    }
}

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
Outer.StaticNested nested = new Outer.StaticNested();

// Anonymous class
Runnable r = new Runnable() {
    @Override public void run() { System.out.println("run"); }
};

// Lambda: same idea, far shorter
Runnable r2 = () -> System.out.println("run");

Prefer static nested unless you truly need the outer instance. Inner classes keep a hidden reference to the outer object, which can cause memory leaks.

Local and anonymous classes and lambdas can only capture effectively final local variables (never reassigned after initialization). A lambda's this refers to the enclosing class, while an anonymous class's this refers to the anonymous instance itself.


14. Enums

An enum is a fixed set of named constants and is a full class (it extends java.lang.Enum).

enum Status { PENDING, ACTIVE, CLOSED }

Status s = Status.ACTIVE;
s.name();                       // "ACTIVE"
s.ordinal();                    // 1
Status.valueOf("CLOSED");       // CLOSED (throws IllegalArgumentException if missing)
Status.values();                // [PENDING, ACTIVE, CLOSED]

Enums can carry fields, constructors, and behavior, even a different implementation per constant:

enum Operation {
    ADD("+") {
        @Override int apply(int a, int b) { return a + b; }
    },
    MULTIPLY("*") {
        @Override int apply(int a, int b) { return a * b; }
    };

    private final String symbol;

    Operation(String symbol) { this.symbol = symbol; }   // constructors are implicitly private

    abstract int apply(int a, int b);
    String symbol() { return symbol; }
}

Operation.ADD.apply(2, 3);   // 5

Useful facts:

  • Compare enums with == (there is exactly one instance per constant).
  • Use EnumMap and EnumSet for maps and sets keyed by enums, they are fast and compact.
  • Enums work directly in switch.
  • A single-element enum is the safest Singleton (serialization-proof and reflection-proof).
  • Avoid ordinal() for persisted data, reordering constants would silently change meaning.

15. Records and Sealed Types

15.1 Records (Java 16+)

A record is a concise, immutable data carrier.

public record User(String name, int age) {
    // compact constructor for validation
    public User {
        if (age < 0) throw new IllegalArgumentException("age must be >= 0");
    }

    // extra methods are allowed
    public boolean isAdult() { return age >= 18; }
}

User u = new User("Sam", 30);
u.name();            // accessor (no "get" prefix)
System.out.println(u);          // User[name=Sam, age=30]
u.equals(new User("Sam", 30));  // true

The compiler generates a canonical constructor, accessors, equals, hashCode, and toString. Records are implicitly final, their fields are private final, and they cannot extend another class (they can implement interfaces). Note that they are shallowly immutable: a List field can still be mutated unless you copy it defensively.

15.2 Sealed Classes and Interfaces (Java 17+)

sealed restricts which classes may extend or implement a type, which enables exhaustive pattern matching.

public sealed interface Shape permits Circle, Square {}
public record Circle(double r) implements Shape {}
public record Square(double side) implements Shape {}

static double area(Shape s) {
    return switch (s) {                       // no default needed: compiler knows all cases
        case Circle c -> Math.PI * c.r() * c.r();
        case Square q -> q.side() * q.side();
    };
}

Permitted subclasses must be final, sealed, or non-sealed.


16. Immutability

An immutable object cannot change after construction, which makes it inherently thread-safe and safe as a map key.

Recipe for an immutable class:

  1. Declare the class final (or make constructors private).
  2. Make all fields private final.
  3. Provide no setters.
  4. Defensively copy mutable inputs and outputs.
public final class Order {
    private final String id;
    private final List<String> items;

    public Order(String id, List<String> items) {
        this.id = id;
        this.items = List.copyOf(items);          // defensive copy, immutable
    }

    public String getId() { return id; }
    public List<String> getItems() { return items; }   // already immutable, safe to return
}

final on a field alone is not enough: a final List can still be mutated.


17. Exception Handling

17.1 The Hierarchy

CheckedUnchecked
ExtendsException (not RuntimeException)RuntimeException or Error
Compiler enforces handlingYes (catch or throws)No
Typical causeRecoverable external failure (I/O, network)Programming bug or violated precondition

17.2 try, catch, finally

try {
    int result = Integer.parseInt(input);
    process(result);
} catch (NumberFormatException e) {
    log.warn("Bad number: " + input, e);
} catch (IOException | SQLException e) {          // multi-catch (types must be unrelated)
    throw new ServiceException("Failed to process", e);   // wrap and keep the cause
} finally {
    cleanup();                                    // runs whether or not an exception occurred
}
  • Catch blocks are checked top to bottom, so put more specific exceptions first.
  • finally runs even on return, but not if the JVM exits (System.exit) or crashes.
  • A return inside finally overrides any earlier return and swallows exceptions:
static int test() {
    try {
        return 1;
    } finally {
        return 2;      // method returns 2. Never do this.
    }
}

Flowchart: try, catch, finally

finally runs on every path, including a return inside the try.

17.3 try-with-resources

Any AutoCloseable resource declared in the try (...) header is closed automatically, in reverse order, even if an exception is thrown.

try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"));
     PrintWriter writer = new PrintWriter("out.txt")) {
    String line;
    while ((line = reader.readLine()) != null) {
        writer.println(line.toUpperCase());
    }
}   // writer then reader are closed here

If both the body and close() throw, the body's exception is primary and the close exception is attached as suppressed (e.getSuppressed()), unlike the old finally pattern, where the close exception would mask the original.

Flowchart: try-with-resources

Resources are closed in reverse order, and a close failure never hides the original exception.

17.4 Custom Exceptions

public class InsufficientFundsException extends Exception {     // checked
    private final double shortfall;

    public InsufficientFundsException(String message, double shortfall) {
        super(message);
        this.shortfall = shortfall;
    }

    public double getShortfall() { return shortfall; }
}

void withdraw(double amount) throws InsufficientFundsException {
    if (amount > balance) {
        throw new InsufficientFundsException("Not enough funds", amount - balance);
    }
    balance -= amount;
}

17.5 Best Practices

  • Never swallow an exception with an empty catch.
  • Do not catch Exception or Throwable broadly unless at a top-level boundary.
  • Preserve the cause when wrapping: new MyException("msg", e).
  • Throw early with specific messages (Objects.requireNonNull(x, "x must not be null")).
  • Do not use exceptions for normal control flow, they are slow and hide intent.
  • Prefer unchecked exceptions for programming errors, checked ones for recoverable conditions the caller can realistically handle.

18. Modern Java Feature Cheat Sheet

VersionFeature
8Lambdas, streams, Optional, default and static interface methods, java.time
9Modules, List.of / Map.of, private interface methods
10var for locals
11String helpers (isBlank, strip, repeat), HttpClient, single-file run
14Switch expressions
15Text blocks
16Records, pattern matching for instanceof, Stream.toList()
17 (LTS)Sealed classes
21 (LTS)Virtual threads, pattern matching for switch, record patterns, sequenced collections

19. Common Pitfalls

  • Comparing strings or wrapper objects with == instead of equals.
  • Overriding equals without hashCode.
  • Using double for money instead of BigDecimal.
  • NullPointerException from unboxing a null wrapper.
  • Modifying a collection while iterating over it (ConcurrentModificationException).
  • Calling overridable methods from constructors.
  • String concatenation with + inside loops.
  • Catching Exception and doing nothing.
  • Returning a mutable internal collection from a getter, breaking encapsulation.
  • Assuming final makes an object immutable.
  • Forgetting that integer division truncates and integer arithmetic silently overflows.

20. Common Interview Questions

  • "Is Java pass by value or pass by reference?" Always pass by value, and for objects the reference is what gets copied (see Section 4).
  • "Difference between == and equals?" Identity vs logical equality (see Section 10).
  • "Why is String immutable?" Security, thread safety, and pooling and hash caching.
  • "String vs StringBuilder vs StringBuffer?" See Section 5.2.
  • "What is the equals and hashCode contract?" Equal objects must have equal hash codes, and overriding one requires overriding the other.
  • "Abstract class vs interface?" State and constructors vs pure contract with multiple inheritance (see Section 12).
  • "Overloading vs overriding?" Compile-time vs runtime resolution (see Section 11.2).
  • "Checked vs unchecked exceptions?" Compiler-enforced handling for recoverable conditions vs programming errors.
  • "What does finally do if there is a return in try?" It still runs, and a return in finally overrides the earlier one.
  • "What is the difference between final, finally, and finalize?" A modifier, a cleanup block, and a deprecated GC hook.
  • "What are records and when would you use them?" Immutable data carriers such as DTOs and value objects.
  • "How does the static block order differ from instance block order?" See Section 8.1.
  • "Why is the Integer cache a problem for ==?" Values from -128 to 127 are cached, others are new objects (see Section 3.1).
  • "Can you override a static method?" No, it is hidden, and resolved at compile time.
  • "How do you make a class immutable?" See Section 16.

21. One-Line Senior Summary

Java gives you a statically typed, garbage-collected language where correctness comes from a few habits: prefer immutability, compare with equals, keep equals and hashCode in sync, use the most restrictive access, model data with records and enums, and handle exceptions deliberately instead of swallowing them.

On this page

📘 Java Fundamentals
Table of Contents
1. The Java Platform
1.1 JDK vs JRE vs JVM
1.2 Compile and Run Flow
1.3 How the JVM Runs Code
1.4 LTS Versions
2. Data Types and Variables
2.1 Literals
2.2 Type Conversion
2.3 Integer Math Gotchas
2.4 Floating Point Precision
3. Wrapper Classes and Autoboxing
3.1 The Integer Cache Trap
3.2 Unboxing NullPointerException
4. Pass by Value
5. Strings
5.1 The String Pool
5.2 StringBuilder vs StringBuffer
5.3 Commonly Used Methods
5.4 Formatting and Text Blocks
6. Control Flow and Modern Switch
6.1 Loops
6.2 Classic Switch and Fall-Through
6.3 Switch Expressions (Java 14+)
6.4 Pattern Matching
7. Arrays
7.1 The Arrays Utility Class
7.2 Array Covariance
8. Classes, Constructors, and Initialization Order
8.1 Initialization Order
9. Access Modifiers, static, and final
9.1 Access Modifiers
9.2 static
9.3 final
10. The Object Class: equals, hashCode, toString
10.1 The equals and hashCode Contract
10.2 Reference vs Content Equality
11. Inheritance and Polymorphism in Java
11.1 Overriding Rules
11.2 Overloading vs Overriding
11.3 Fields and Static Methods Are Not Polymorphic
11.4 super
12. Abstract Classes vs Interfaces
12.1 The Diamond Problem
13. Nested and Inner Classes
14. Enums
15. Records and Sealed Types
15.1 Records (Java 16+)
15.2 Sealed Classes and Interfaces (Java 17+)
16. Immutability
17. Exception Handling
17.1 The Hierarchy
17.2 try, catch, finally
17.3 try-with-resources
17.4 Custom Exceptions
17.5 Best Practices
18. Modern Java Feature Cheat Sheet
19. Common Pitfalls
20. Common Interview Questions
21. One-Line Senior Summary