Table of Contents

0. Setup & Environment

Install Java via Homebrew

Homebrew is the most common way to install Java on macOS.

# Install latest OpenJDK
brew install openjdk

# Or install a specific LTS version (recommended)
brew install openjdk@21

# Symlink so macOS java_home utility can find it
sudo ln -sfn $(brew --prefix)/opt/openjdk@21/libexec/openjdk.jdk \
  /Library/Java/JavaVirtualMachines/openjdk-21.jdk

# Verify
java --version    # e.g. openjdk 21.0.x ...
javac --version   # e.g. javac 21.0.x

# Set JAVA_HOME (add to ~/.zshrc or ~/.bash_profile)
export JAVA_HOME=$(/usr/libexec/java_home)
Which version to install? Java 21 is the current LTS (Long-Term Support) release — it's what most production systems and job descriptions target in 2024–2025. Java 17 is the previous LTS and still widely used.

Alternative: SDKMAN (Multiple JDK Versions)

SDKMAN lets you install and switch between multiple JDK distributions and versions — useful when projects require different Java versions.

# Install SDKMAN
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"

# List available distributions (Temurin, Corretto, GraalVM, etc.)
sdk list java

# Install Eclipse Temurin 21 (community build of OpenJDK)
sdk install java 21.0.2-tem

# Switch version for the current shell session
sdk use java 21.0.2-tem

# Set a version as the default
sdk default java 21.0.2-tem
Distribution guide: -tem = Eclipse Temurin (recommended default), -amzn = Amazon Corretto (good for AWS), -graalce = GraalVM Community (native image support). All are based on OpenJDK — pick Temurin if unsure.

Build Tools

Maven and Gradle are the two dominant build tools in the Java ecosystem. Most projects ship with a wrapper script so a global install is often optional.

# Install globally (optional — most projects include a wrapper)
brew install maven
brew install gradle

# Use project wrapper instead (preferred)
./mvnw clean install      # Maven wrapper
./gradlew build           # Gradle wrapper
ToolConfig fileWrapperStrengths
Mavenpom.xmlmvnwStrict conventions, widely adopted in enterprise, large plugin ecosystem
Gradlebuild.gradle / build.gradle.ktsgradlewFaster incremental builds, flexible DSL, preferred in Android
Starting a new project? Use start.spring.io (Spring Initializr) to scaffold a Maven or Gradle project with the dependencies you need — it generates the wrapper and project structure automatically.

Editor Setup

Two editors dominate Java development:

  • IntelliJ IDEA — the industry standard. Community Edition is free and covers all core Java features. Ultimate adds Spring, databases, and profiling tools.
  • VS Code — install the Extension Pack for Java (Microsoft) which bundles Language Support, Debugger, Test Runner, Maven, and Gradle extensions.
For most interview prep and learning, VS Code + Extension Pack is lightweight and sufficient. For professional Spring Boot development, IntelliJ Ultimate is worth the license.

Quick Verify

Confirm your install end-to-end by compiling and running a minimal program:

mkdir ~/java-refresher && cd ~/java-refresher

cat > Hello.java << 'EOF'
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}
EOF

javac Hello.java   # compiles → Hello.class
java Hello         # runs    → Hello, Java!
Single-file shortcut (Java 11+): You can run a single-file program without compiling first: java Hello.java. Useful for quick experiments — skips the javac step entirely.

1. Program Basics

JVM Architecture

Java source compiles to bytecode (.class), which the JVM interprets/JIT-compiles at runtime. This is the "write once, run anywhere" model.

TermWhat it isIncludes
JDKJava Development Kitjavac, java, javadoc, jar, JRE
JREJava Runtime EnvironmentJVM + standard class libraries
JVMJava Virtual MachineClass loader, bytecode verifier, JIT compiler, GC
Tip
Since Java 11, standalone JRE distributions are no longer shipped separately by Oracle. You ship a JDK or use jlink to build a minimal custom runtime image.

Compilation and Execution

# Compile
javac -cp lib/*:. -d out src/com/example/App.java

# Run
java -cp out:lib/* com.example.App

# Single-file programs (Java 11+) — no explicit compile step
java App.java

# Create JAR
jar --create --file=app.jar --main-class=com.example.App -C out .
java -jar app.jar

The main Method

// Classic form — always works
public class App {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

// Java 21 preview, standard in Java 23+ — instance main, no public/static required
class App {
    void main() {
        System.out.println("Hello from instance main!");
    }
}

// Accepting args
public static void main(String[] args) {
    if (args.length < 1) {
        System.err.println("Usage: App ");
        System.exit(1);
    }
    System.out.println("Hello, " + args[0]);
}

Classpath Essentials

# Classpath separators: colon on Unix, semicolon on Windows
java -cp "lib/*:build/classes" com.example.Main

# Module path (Java 9+ JPMS)
java --module-path mods --module com.example/com.example.Main

# Common JVM flags
java -Xms256m -Xmx1g          # Heap min/max
java -XX:+UseG1GC              # GC algorithm
java -Xss512k                  # Thread stack size
java -verbose:gc               # GC logging
JPMS (Module System) Java 9
Modules add a higher-level encapsulation layer above packages. A module-info.java declares requires (dependencies) and exports (public API). Reflection across module boundaries requires opens. Most projects still rely on the classpath, not JPMS.

2. Type System

Primitives

TypeSizeRange / NotesDefault
byte8-bit-128 to 1270
short16-bit-32,768 to 32,7670
int32-bit-2^31 to 2^31-10
long64-bit-2^63 to 2^63-1 (suffix: L)0L
float32-bit IEEE 754~7 decimal digits (suffix: f)0.0f
double64-bit IEEE 754~15 decimal digits0.0d
char16-bit Unicode'\u0000' to '\uffff''\u0000'
booleanJVM-definedtrue / falsefalse
// Numeric literals
int million   = 1_000_000;        // underscores for readability
long bigNum   = 9_876_543_210L;
int hex       = 0xFF;
int binary    = 0b1010_1010;
double sci    = 1.5e10;

// Integer overflow wraps silently — use Math.addExact for safety
int max = Integer.MAX_VALUE;
System.out.println(max + 1);      // -2147483648 (wraps!)
Math.addExact(max, 1);            // throws ArithmeticException

Boxing and Unboxing

// Autoboxing (primitive -> wrapper)
Integer boxed = 42;           // compiler inserts Integer.valueOf(42)
int unboxed   = boxed;        // compiler inserts boxed.intValue()

// Wrapper types: Integer, Long, Double, Float, Short, Byte, Character, Boolean
List<Integer> list = new ArrayList<>();
list.add(5);                  // autoboxed

// Null unboxing throws NullPointerException
Integer n = null;
int x = n;                    // NullPointerException at runtime!

// Cache range: Integer.valueOf() caches -128 to 127
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 (different objects)
System.out.println(c.equals(d)); // true — always use equals()
Pitfall: Unboxing null
Integer count = map.get("key"); int c = count; — if the key is absent, get() returns null and unboxing throws NPE. Use map.getOrDefault("key", 0).

var — Local Variable Type Inference Java 10

// var infers the type from the right-hand side
var name   = "Alice";                     // String
var count  = 42;                          // int
var list   = new ArrayList<String>();     // ArrayList<String>
var map    = Map.of("a", 1, "b", 2);      // Map<String, Integer>

// Works in for loops
for (var entry : map.entrySet()) {
    System.out.println(entry.getKey() + "=" + entry.getValue());
}

// Cannot use var for:
// - fields, method params, return types
// - null initializers:  var x = null;   // compile error
// - array initializers: var arr = {1,2}; // compile error
var arr = new int[]{1, 2, 3};             // OK — type is clear
var usage guidance
Use var when the type is obvious from context (constructor calls, factory methods). Avoid when it obscures intent — e.g., var result = process(data); tells you nothing about what result is.

Type Casting

// Widening (implicit) — no data loss
int i = 100;
long l = i;         // int -> long, automatic
double d = i;       // int -> double, automatic

// Narrowing (explicit) — potential data loss
double pi = 3.14;
int truncated = (int) pi;   // 3 — truncates, no rounding
long big = 300L;
byte b = (byte) big;        // 44 — wraps around (300 % 256)

// Object casting
Object obj = "Hello";
String s = (String) obj;            // OK
Integer n = (Integer) obj;          // ClassCastException at runtime

// Safe: check first
if (obj instanceof String str) {    // pattern matching — Java 16
    System.out.println(str.length());
}

3. Strings

Immutability and the String Pool

Strings are immutable objects. String literals are interned (stored in the String Pool in the heap's metaspace area). Every "modification" creates a new object.

String a = "hello";          // interned in pool
String b = "hello";          // same pool reference
String c = new String("hello"); // new heap object, NOT pooled

System.out.println(a == b);       // true (same pool ref)
System.out.println(a == c);       // false (different object)
System.out.println(a.equals(c));  // true (same content)

// Force interning
String d = c.intern();
System.out.println(a == d);       // true

StringBuilder vs StringBuffer

StringBuilderStringBuffer
Thread-safeNoYes (synchronized)
PerformanceFasterSlower (lock overhead)
Use whenSingle thread (99% of cases)Shared across threads
// StringBuilder — use this for building strings in a loop
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
    sb.append(i).append(", ");
}
sb.setLength(sb.length() - 2);  // trim trailing ", "
String result = sb.toString();   // "0, 1, 2, 3, 4, 5, 6, 7, 8, 9"

// Useful StringBuilder methods
sb.insert(0, "prefix: ");
sb.delete(0, 8);
sb.reverse();
sb.replace(2, 5, "XYZ");

// String.join — cleaner for fixed lists
String joined = String.join(", ", "a", "b", "c"); // "a, b, c"

Text Blocks Java 15

// Before: messy escaping
String json = "{\n" +
              "  \"name\": \"Alice\",\n" +
              "  \"age\": 30\n" +
              "}";

// With text blocks — indentation determined by closing """
String json = """
        {
          "name": "Alice",
          "age": 30
        }
        """;

// The incidental whitespace (up to the closing """) is stripped.
// trailing newline is included unless closing """ is on the same line as content.
String noTrailingNewline = """
        hello""";

// Formatted text block
String html = """
        <h1>%s</h1>
        <p>%s</p>
        """.formatted("Title", "Content");

Essential String Methods

String s = "  Hello, World!  ";

// Inspection
s.length()                    // 17
s.isEmpty()                   // false
s.isBlank()                   // false (Java 11) — checks whitespace only
s.charAt(7)                   // ','
s.indexOf("World")            // 9
s.contains("Hello")           // true
s.startsWith("  H")           // true
s.endsWith("!  ")             // true

// Transformation
s.trim()                      // "Hello, World!" (strips ASCII whitespace)
s.strip()                     // Java 11 — strips Unicode whitespace too
s.stripLeading()              // "Hello, World!  "
s.stripTrailing()             // "  Hello, World!"
s.toUpperCase()
s.toLowerCase()
s.replace("World", "Java")
s.replaceAll("\\s+", "_")     // regex
s.substring(2, 7)             // "Hello"
s.split(", ")                 // ["  Hello", "World!  "]

// Java 11+
"  \n ".isBlank()             // true
"abc".repeat(3)               // "abcabcabc"
"a\nb\nc".lines()             // Stream<String>

// Comparison
"apple".compareTo("banana")   // negative (a < b)
"abc".equalsIgnoreCase("ABC") // true
Never concatenate in loops
"result" + item in a loop creates a new String object each iteration. O(n²) memory allocations. Use StringBuilder or String.join()/Collectors.joining().
String formatting cheat sheet
// String.format / formatted()
String.format("%-10s %5d %.2f", "item", 42, 3.14);
// "item          42 3.14"

// Common format specifiers
// %s   - String
// %d   - integer
// %f   - float (%.2f = 2 decimal places)
// %n   - platform line separator
// %x   - hex
// %05d - zero-padded
// %-10s - left-aligned in 10 chars

// printf (prints directly)
System.out.printf("Name: %s, Age: %d%n", name, age);

4. Collections Framework

Quick Selection Guide

NeedUseWhy
Ordered list, fast random accessArrayListO(1) get, O(n) insert/remove middle
Frequent insert/remove from endsLinkedList (as Deque)O(1) head/tail ops, poor cache locality
Fast membership test, no order neededHashSetO(1) avg add/contains/remove
Sorted unique elementsTreeSetO(log n), natural or custom order
Insertion-ordered setLinkedHashSetO(1) ops + predictable iteration
Key-value, fast lookupHashMapO(1) avg, unordered
Sorted map by keyTreeMapO(log n), NavigableMap API
Insertion-ordered mapLinkedHashMapIteration in insertion order
Thread-safe mapConcurrentHashMapSegment locks, no full lock on reads
FIFO queueArrayDequeFaster than LinkedList for queues
Priority orderingPriorityQueueMin-heap by default, O(log n) poll

List

// Mutable
List<String> list = new ArrayList<>();
list.add("a"); list.add("b"); list.add("c");
list.add(1, "x");            // insert at index
list.remove("b");             // by value
list.remove(0);               // by index
list.set(0, "z");             // replace
list.get(0);                  // "z"
list.size();                  // 2
list.contains("c");           // true
list.subList(0, 2);           // view, not copy

// Immutable (Java 9+) — throws on mutation
List<String> fixed = List.of("a", "b", "c");
List<String> copy  = List.copyOf(existingList);  // defensive copy

// Sort
Collections.sort(list);
list.sort(Comparator.naturalOrder());
list.sort(Comparator.reverseOrder());
list.sort(Comparator.comparing(String::length).thenComparing(Comparator.naturalOrder()));

Map

Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.get("a");               // 1
map.get("z");               // null — key absent
map.getOrDefault("z", 0);   // 0
map.containsKey("a");       // true
map.remove("a");

// Java 8+ compute methods — avoid double lookup
map.putIfAbsent("b", 2);
map.computeIfAbsent("words", k -> new ArrayList<>()).add("hello");
map.merge("count", 1, Integer::sum);  // increment or initialize to 1

// Iteration
for (Map.Entry<String, Integer> e : map.entrySet()) {
    System.out.println(e.getKey() + "=" + e.getValue());
}
map.forEach((k, v) -> System.out.println(k + "=" + v));

// Immutable (Java 9+)
Map<String, Integer> fixed = Map.of("a", 1, "b", 2);
Map<String, Integer> large = Map.ofEntries(
    Map.entry("x", 10), Map.entry("y", 20)
);

Set

Set<String> set = new HashSet<>();
set.add("a");
set.add("a");               // duplicate ignored
set.contains("a");          // true
set.remove("a");

// Set operations
Set<Integer> s1 = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> s2 = new HashSet<>(Set.of(2, 3, 4));

s1.retainAll(s2);           // intersection (mutates s1)
s1.addAll(s2);              // union
s1.removeAll(s2);           // difference

// Sorted
Set<String> sorted = new TreeSet<>(Set.of("banana", "apple", "cherry"));
// iterates: apple, banana, cherry

Queue and Deque

// Deque — use ArrayDeque for both stack and queue
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("front");    // or push() for stack semantics
deque.addLast("back");      // or offer() for queue semantics
deque.peekFirst();          // view without removing
deque.pollFirst();          // remove and return, null if empty
deque.removeFirst();        // remove and return, throws if empty

// As a stack (LIFO)
Deque<String> stack = new ArrayDeque<>();
stack.push("a");
stack.pop();

// PriorityQueue — min-heap
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5); pq.offer(1); pq.offer(3);
pq.poll();  // 1 (smallest)
pq.peek();  // next smallest, no removal

// Max-heap
PriorityQueue<Integer> maxPq = new PriorityQueue<>(Comparator.reverseOrder());

Collections Utility Class

List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5));

Collections.sort(nums);           // [1, 1, 3, 4, 5]
Collections.reverse(nums);        // [5, 4, 3, 1, 1]
Collections.shuffle(nums);        // random order
Collections.min(nums);            // 1
Collections.max(nums);            // 5
Collections.frequency(nums, 1);   // 2
Collections.swap(nums, 0, 1);
Collections.fill(nums, 0);        // all zeros
Collections.nCopies(5, "x");      // ["x","x","x","x","x"]

// Wrappers
List<String> unmod = Collections.unmodifiableList(list);
List<String> sync  = Collections.synchronizedList(list); // prefer CopyOnWriteArrayList
List<String> empty = Collections.emptyList();
Sequenced Collections Java 21
New interfaces SequencedCollection, SequencedSet, and SequencedMap add getFirst(), getLast(), addFirst(), addLast(), reversed() to List, Deque, LinkedHashSet, and LinkedHashMap.
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.getFirst();   // "a"
list.getLast();    // "c"
list.reversed();   // reversed view

5. Generics

Basics

// Generic class
public class Box<T> {
    private T value;
    public Box(T value) { this.value = value; }
    public T get() { return value; }
}

Box<String> strBox = new Box<>("hello");  // diamond operator infers String
String s = strBox.get();

// Generic method
public <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

// Multiple type parameters
public class Pair<A, B> {
    public final A first;
    public final B second;
    public Pair(A first, B second) { this.first = first; this.second = second; }
}

Bounded Type Parameters

// Upper bounded — T must be Number or subtype
public <T extends Number> double sum(List<T> list) {
    return list.stream().mapToDouble(Number::doubleValue).sum();
}
sum(List.of(1, 2, 3));      // integers OK
sum(List.of(1.5, 2.5));     // doubles OK

// Multiple bounds (class must come first)
public <T extends Comparable<T> & Cloneable> T cloneMax(T a, T b) { ... }

// Lower bounded — T must be Integer or supertype
public void addNumbers(List<? super Integer> list) {
    list.add(42);
}

Wildcards and PECS

PECS: Producer Extends, Consumer Super. If a structure produces (you read from it), use ? extends T. If it consumes (you write to it), use ? super T.

// ? extends T — read only, can't add (except null)
List<? extends Number> numbers = List.of(1, 2.5, 3L);
Number n = numbers.get(0);    // OK — guaranteed to be a Number
numbers.add(5);               // COMPILE ERROR

// ? super T — write OK, reading gives Object
List<? super Integer> sink = new ArrayList<Number>();
sink.add(42);                 // OK — Integer IS-A Number
Object o = sink.get(0);       // only Object guaranteed

// Unbounded wildcard — unknown type, read-only as Object
void printList(List<?> list) {
    for (Object e : list) System.out.println(e);
}

// Real example: Collections.copy
// <T> void copy(List<? super T> dest, List<? extends T> src)
List<Number> dest = new ArrayList<>(Collections.nCopies(3, null));
List<Integer> src = List.of(1, 2, 3);
Collections.copy(dest, src);  // works — Integer extends Number

Type Erasure

// At compile time: List<String> and List<Integer> are different types
// At runtime: both become List (raw type) — type info is ERASED

List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// strings.getClass() == ints.getClass() — true!

// Cannot do:
// new T()                  // can't instantiate type parameter
// new T[]                  // can't create generic array
// instanceof List<String>  // can't check parameterized type at runtime
// List<String>.class       // no generic class literal

// Workaround for runtime type: pass Class<T>
public <T> T deserialize(String json, Class<T> type) {
    return objectMapper.readValue(json, type);
}
deserialize(json, User.class);
Heap pollution and @SafeVarargs
// Varargs + generics can cause heap pollution
@SafeVarargs  // suppress unchecked warning — only use if method is safe
public static <T> List<T> asList(T... elements) {
    return Arrays.asList(elements);
}

// The problem: arrays are covariant, generics are not
Object[] objs = new String[3];  // compiles! arrays are covariant
objs[0] = 42;                   // ArrayStoreException at runtime
// Generics prevent this at compile time (invariant)

6. Object-Oriented Programming

Classes and Interfaces

// Interface — default and static methods since Java 8
public interface Shape {
    double area();                           // abstract — must implement
    double perimeter();

    default String describe() {              // default — optional override
        return "Area: %.2f".formatted(area());
    }

    static Shape circle(double r) {          // factory helper
        return new Circle(r);
    }

    // private methods in interfaces — Java 9
    private void logArea() {
        System.out.println("area=" + area());
    }
}

// Abstract class — use when sharing state or partial implementation
public abstract class Vehicle {
    protected final String brand;            // shared state

    public Vehicle(String brand) { this.brand = brand; }

    public abstract int maxSpeed();          // subclass must implement

    public String info() {                   // shared behavior
        return brand + " @ " + maxSpeed() + " km/h";
    }
}

// Concrete class
public class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        if (radius <= 0) throw new IllegalArgumentException("radius must be positive");
        this.radius = radius;
    }

    @Override public double area() { return Math.PI * radius * radius; }
    @Override public double perimeter() { return 2 * Math.PI * radius; }
    @Override public String toString() { return "Circle(r=" + radius + ")"; }
    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Circle c)) return false;
        return Double.compare(radius, c.radius) == 0;
    }
    @Override public int hashCode() { return Double.hashCode(radius); }
}

Records Java 16

Records are immutable data carriers. The compiler generates constructor, accessors, equals, hashCode, and toString automatically.

// Equivalent to a class with final fields + boilerplate
public record Point(double x, double y) {}

Point p = new Point(1.0, 2.0);
p.x();          // accessor (not getX())
p.y();
p.toString();   // "Point[x=1.0, y=2.0]"

// Compact constructor — for validation
public record Range(int min, int max) {
    public Range {                          // compact: no parameter list
        if (min > max) throw new IllegalArgumentException("min > max");
    }
}

// Records can implement interfaces
public record EmailAddress(String value) implements Comparable<EmailAddress> {
    public EmailAddress {
        if (!value.contains("@")) throw new IllegalArgumentException("invalid email");
        value = value.toLowerCase();        // can normalize in compact constructor
    }

    @Override public int compareTo(EmailAddress other) {
        return this.value.compareTo(other.value);
    }
}

// Records are implicitly final — cannot extend other classes
// Local records work too (useful in methods)

Sealed Classes Java 17

Sealed classes restrict which classes can extend/implement them. Enables exhaustive pattern matching.

// sealed + permits = closed hierarchy
public sealed interface Shape permits Circle, Rectangle, Triangle {}

public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
public final class Triangle implements Shape {
    // ... must be final, sealed, or non-sealed
}

// With sealed hierarchy, switch can be exhaustive (no default needed)
double area = switch (shape) {
    case Circle c     -> Math.PI * c.radius() * c.radius();
    case Rectangle r  -> r.w() * r.h();
    case Triangle t   -> t.area();
    // compiler knows these are ALL subtypes — no default required
};

Enums

public enum Planet {
    MERCURY(3.303e+23, 2.4397e6),
    VENUS  (4.869e+24, 6.0518e6),
    EARTH  (5.976e+24, 6.37814e6);

    private final double mass;
    private final double radius;

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    static final double G = 6.67300E-11;

    double surfaceGravity() { return G * mass / (radius * radius); }
    double surfaceWeight(double otherMass) { return otherMass * surfaceGravity(); }
}

// Usage
Planet.EARTH.surfaceWeight(75.0);     // 735.0

// Enum utilities
Planet.values()                        // all values array
Planet.valueOf("EARTH")                // from string
Planet.EARTH.name()                    // "EARTH"
Planet.EARTH.ordinal()                 // 2

// Enums in switch
switch (planet) {
    case EARTH -> System.out.println("Home");
    case MARS  -> System.out.println("Next stop");
    default    -> System.out.println("Far away");
}

Inheritance vs Composition

// Prefer composition over inheritance
// BAD: tight coupling via inheritance
class Stack<T> extends ArrayList<T> {   // exposes all ArrayList methods!
    public void push(T e) { add(e); }
    public T pop() { return remove(size() - 1); }
}

// GOOD: composition — wrap, don't extend
class Stack<T> {
    private final List<T> storage = new ArrayList<>();
    public void push(T e) { storage.add(e); }
    public T pop() { return storage.remove(storage.size() - 1); }
    public boolean isEmpty() { return storage.isEmpty(); }
    public int size() { return storage.size(); }
    // Only expose what you intend to expose
}

7. Functional Java

Lambdas

// Lambda syntax variations
Runnable r1 = () -> System.out.println("hello");
Runnable r2 = () -> { System.out.println("hello"); };  // block body

Comparator<String> c1 = (a, b) -> a.compareTo(b);
Comparator<String> c2 = (String a, String b) -> a.compareTo(b);  // explicit types

// With return value
Function<Integer, Integer> square = x -> x * x;
square.apply(5);   // 25

// Multi-statement block
Function<String, String> transform = s -> {
    String trimmed = s.trim();
    return trimmed.toLowerCase();
};

Method References — 4 Kinds

// 1. Static method reference
Function<String, Integer> parse = Integer::parseInt;
parse.apply("42");  // equivalent to s -> Integer.parseInt(s)

// 2. Instance method reference (on a specific instance)
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;
greet.apply("World");   // "Hello, World"

// 3. Instance method reference (on arbitrary instance of type)
Function<String, String> upper = String::toUpperCase;
upper.apply("hello");   // "HELLO" — equivalent to s -> s.toUpperCase()

// 4. Constructor reference
Supplier<ArrayList<String>> makeList = ArrayList::new;
makeList.get();         // new ArrayList<>()

// In practice — with streams
List<String> words = List.of("hello", "world");
words.stream()
    .map(String::toUpperCase)     // instance method ref (kind 3)
    .forEach(System.out::println); // instance method ref (kind 2)

Key Functional Interfaces

InterfaceMethodSignatureExample
Function<T,R>applyT -> Rtransform a value
BiFunction<T,U,R>apply(T,U) -> Rcombine two values
Predicate<T>testT -> booleanfilter condition
BiPredicate<T,U>test(T,U) -> booleantwo-arg filter
Consumer<T>acceptT -> voidside-effect action
BiConsumer<T,U>accept(T,U) -> voidtwo-arg side effect
Supplier<T>get() -> Tlazy factory
UnaryOperator<T>applyT -> Ttransform same type
BinaryOperator<T>apply(T,T) -> Treduce two to one
// Composing functions
Function<String, String> trim    = String::trim;
Function<String, String> lower   = String::toLowerCase;
Function<String, Integer> length = String::length;

Function<String, String> trimThenLower = trim.andThen(lower);
// equivalent to: s -> lower.apply(trim.apply(s))

Function<String, Integer> pipeline = trim.andThen(lower).andThen(length);
pipeline.apply("  HELLO  ");  // 5

// Predicate composition
Predicate<String> notEmpty = Predicate.not(String::isEmpty);  // Java 11
Predicate<String> notBlank = Predicate.not(String::isBlank);
Predicate<String> valid = notEmpty.and(notBlank);
Predicate<String> anyInvalid = notEmpty.or(notBlank);
Predicate<String> isEmpty = notEmpty.negate();

8. Streams API

Creating Streams

// From collections
Stream<String> s1 = list.stream();
Stream<String> s2 = list.parallelStream();

// From values
Stream<String> s3 = Stream.of("a", "b", "c");
Stream<String> empty = Stream.empty();

// From arrays
Stream<String> s4 = Arrays.stream(array);
IntStream ints = Arrays.stream(new int[]{1, 2, 3});

// Generated / iterated
Stream<Integer> generated = Stream.generate(() -> 0).limit(5);
Stream<Integer> iterated  = Stream.iterate(0, n -> n + 1).limit(10);
Stream<Integer> bounded   = Stream.iterate(0, n -> n < 10, n -> n + 1); // Java 9

// Primitive specializations — avoid boxing overhead
IntStream.range(0, 10)          // 0..9
IntStream.rangeClosed(0, 10)    // 0..10
LongStream.of(1L, 2L, 3L)
DoubleStream.of(1.0, 2.0)

Intermediate Operations (lazy)

List<String> words = List.of("apple", "banana", "apricot", "blueberry", "avocado");

words.stream()
    .filter(w -> w.startsWith("a"))        // predicate filter
    .map(String::toUpperCase)              // transform each element
    .sorted()                              // natural order sort
    .distinct()                            // remove duplicates
    .limit(3)                              // take at most 3
    .skip(1)                               // skip first
    .peek(w -> System.out.println("~" + w))// debug side-effect, keep elements
    .forEach(System.out::println);

// flatMap — flatten nested structures
List<List<Integer>> nested = List.of(List.of(1,2), List.of(3,4));
nested.stream()
    .flatMap(Collection::stream)           // Stream<Integer> from Stream<List<Integer>>
    .collect(Collectors.toList());         // [1, 2, 3, 4]

// mapToInt, mapToLong, mapToDouble — avoid boxing
int totalLen = words.stream()
    .mapToInt(String::length)
    .sum();

Terminal Operations

// Collecting
List<String> result = stream.collect(Collectors.toList());    // mutable
List<String> result2 = stream.toList();                       // Java 16, immutable

// Count, min, max
long count = stream.count();
Optional<String> min = stream.min(Comparator.naturalOrder());
Optional<String> max = stream.max(Comparator.comparingInt(String::length));

// Finding
Optional<String> first = stream.findFirst();  // deterministic, sequential
Optional<String> any   = stream.findAny();    // faster for parallel
boolean anyMatch  = stream.anyMatch(s -> s.isEmpty());
boolean allMatch  = stream.allMatch(s -> !s.isEmpty());
boolean noneMatch = stream.noneMatch(s -> s.isEmpty());

// Reducing
Optional<Integer> product = IntStream.rangeClosed(1, 5)
    .boxed()
    .reduce((a, b) -> a * b);   // Optional — empty stream possible
int sum = IntStream.rangeClosed(1, 5).reduce(0, Integer::sum);  // identity + accumulator

// forEach vs forEachOrdered
stream.forEach(System.out::println);          // order not guaranteed for parallel
stream.forEachOrdered(System.out::println);   // order preserved

Collectors

// groupingBy — Map<K, List<V>>
Map<Integer, List<String>> byLength = words.stream()
    .collect(Collectors.groupingBy(String::length));

// groupingBy with downstream collector
Map<Integer, Long> countByLength = words.stream()
    .collect(Collectors.groupingBy(String::length, Collectors.counting()));

Map<Integer, String> joinedByLength = words.stream()
    .collect(Collectors.groupingBy(
        String::length,
        Collectors.joining(", ")));

// partitioningBy — Map<Boolean, List<V>>
Map<Boolean, List<String>> partition = words.stream()
    .collect(Collectors.partitioningBy(w -> w.length() > 5));
partition.get(true);   // words longer than 5
partition.get(false);  // words 5 chars or shorter

// joining
String csv = words.stream().collect(Collectors.joining(", "));
String wrapped = words.stream().collect(Collectors.joining(", ", "[", "]"));

// toMap
Map<String, Integer> wordLengths = words.stream()
    .collect(Collectors.toMap(
        w -> w,
        String::length,
        (existing, replacement) -> existing  // merge fn for duplicate keys
    ));

// Statistics
IntSummaryStatistics stats = words.stream()
    .collect(Collectors.summarizingInt(String::length));
stats.getAverage(); stats.getMax(); stats.getCount();

Optional — avoiding null

Optional<String> opt = Optional.of("hello");
Optional<String> empty = Optional.empty();
Optional<String> maybe = Optional.ofNullable(possiblyNull);

// Extracting values
opt.get();                                // throws if empty — avoid
opt.orElse("default");                    // fallback value
opt.orElseGet(() -> computeDefault());    // lazy fallback
opt.orElseThrow(() -> new RuntimeException("missing")); // throw

// Chaining
opt.map(String::toUpperCase)             // Optional<String>
   .filter(s -> s.length() > 3)
   .ifPresent(System.out::println);

opt.ifPresentOrElse(                     // Java 9
    s -> System.out.println(s),
    () -> System.out.println("empty"));

Optional<String> result = opt.or(() -> Optional.of("fallback")); // Java 9

// flatMap — when mapper returns Optional
Optional<Integer> len = opt.flatMap(s -> Optional.of(s.length()));
Parallel streams — use with care
Parallel streams use the common ForkJoinPool (size = CPU cores - 1). They help for CPU-bound work on large datasets. They hurt for small datasets, I/O-bound work, or when elements have ordering dependencies. Always benchmark before using. Never use parallel streams with stateful lambdas or external side effects.

9. Concurrency

Thread Basics

// Extend Thread (less flexible — Java prefers Runnable)
Thread t1 = new Thread(() -> System.out.println("running"));
t1.start();   // start() creates a new OS thread, don't call run() directly
t1.join();    // wait for completion
t1.interrupt(); // request interruption (cooperative)

// Check for interruption in long loops
while (!Thread.currentThread().isInterrupted()) {
    // do work
}

// Thread states: NEW -> RUNNABLE -> BLOCKED/WAITING/TIMED_WAITING -> TERMINATED
Thread.sleep(100);  // throws InterruptedException — always handle it
Thread.yield();     // hint to scheduler (rarely useful)

ExecutorService

// Prefer ExecutorService over raw threads
ExecutorService pool = Executors.newFixedThreadPool(4);
ExecutorService single = Executors.newSingleThreadExecutor();
ExecutorService cached = Executors.newCachedThreadPool();  // unbounded — be careful
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

// Submit tasks
Future<Integer> future = pool.submit(() -> {
    Thread.sleep(100);
    return 42;
});
int result = future.get();        // blocks, throws ExecutionException, InterruptedException
int result2 = future.get(5, TimeUnit.SECONDS); // with timeout

// Always shut down cleanly
pool.shutdown();                  // stop accepting, finish in-progress
pool.awaitTermination(10, TimeUnit.SECONDS);
// or force stop:
pool.shutdownNow();               // returns list of not-started tasks

// Try-with-resources (Java 19+)
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
    exec.submit(() -> doWork());
}  // auto-shutdown

CompletableFuture

// Async task
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    // runs in ForkJoinPool.commonPool()
    return fetchData();
});

// Or with custom executor
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(
    () -> fetchData(), myExecutor);

// Chain transformations
cf.thenApply(String::toUpperCase)         // transform result (sync)
  .thenApplyAsync(s -> process(s))        // transform (async, in pool)
  .thenAccept(System.out::println)        // consume, no return value
  .thenRun(() -> System.out.println("done")); // no input, no output

// Compose (flatMap equivalent)
CompletableFuture<User> user = fetchUserId()
    .thenCompose(id -> fetchUser(id));    // avoids CompletableFuture<CompletableFuture<...>>

// Combine two futures
CompletableFuture<String> combined =
    cf1.thenCombine(cf2, (r1, r2) -> r1 + r2);

// Wait for all / any
CompletableFuture.allOf(cf1, cf2, cf3).join();  // wait for all
CompletableFuture.anyOf(cf1, cf2, cf3).join();  // wait for fastest

// Error handling
cf.exceptionally(ex -> "fallback")
  .handle((result, ex) -> ex != null ? "error" : result)  // both paths
  .whenComplete((r, ex) -> log(r, ex));  // inspect but don't transform

Virtual Threads Java 21

// Virtual threads: lightweight threads managed by JVM, not OS
// Thousands to millions can run concurrently with minimal memory
Thread vThread = Thread.ofVirtual().start(() -> doWork());

// Via ExecutorService (one virtual thread per task)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) {
        executor.submit(() -> handleRequest());
    }
}

// Virtual threads block cheaply — blocking I/O is fine
// They "park" the virtual thread and release the carrier thread
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
    exec.submit(() -> {
        var data = Files.readString(path);  // blocking I/O — OK for virtual threads
        process(data);
    });
}
Virtual threads and thread pools
Don't pool virtual threads — they're cheap to create. Use newVirtualThreadPerTaskExecutor(). Avoid synchronized blocks that hold locks during I/O inside virtual threads (pinning issue). Prefer ReentrantLock instead.

Synchronization Primitives

// synchronized — intrinsic lock
public class Counter {
    private int count = 0;

    public synchronized void increment() { count++; }
    public synchronized int get() { return count; }

    // Synchronized block (finer granularity)
    public void method() {
        synchronized (this) { count++; }
    }
}

// volatile — visibility guarantee, no atomicity
private volatile boolean running = true;
// Write to running in one thread is visible to all other threads immediately.
// Does NOT prevent race conditions on compound operations (check-then-act).

// Atomic types — lock-free, CAS operations
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();          // thread-safe ++
counter.compareAndSet(5, 10);       // CAS: set to 10 only if currently 5
counter.getAndUpdate(x -> x * 2);  // apply function atomically

AtomicReference<Node> head = new AtomicReference<>();
AtomicLong, AtomicBoolean, AtomicReferenceArray...

// ReentrantLock — more control than synchronized
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // critical section
} finally {
    lock.unlock();  // ALWAYS in finally
}

// Try to acquire with timeout
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    try { /* work */ } finally { lock.unlock(); }
} else {
    // couldn't acquire — handle accordingly
}

// ReadWriteLock — allow concurrent reads
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();   // multiple readers allowed
rwLock.writeLock().lock();  // exclusive
CountDownLatch, CyclicBarrier, Semaphore
// CountDownLatch — wait for N events
CountDownLatch latch = new CountDownLatch(3);
// In each worker:
latch.countDown();   // decrement
// In coordinator:
latch.await();       // block until count reaches 0

// CyclicBarrier — rendezvous point for N threads (reusable)
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("phase done"));
// Each worker calls:
barrier.await();     // blocks until all 3 arrive, then runs action

// Semaphore — limit concurrent access
Semaphore sem = new Semaphore(5);  // at most 5 threads in section
sem.acquire();
try { /* limited resource */ } finally { sem.release(); }

10. Modern Java Features

Switch Expressions Java 14

// Old switch statement — fall-through trap
String old;
switch (day) {
    case MONDAY: case TUESDAY: old = "weekday"; break;
    case SATURDAY: case SUNDAY: old = "weekend"; break;
    default: old = "midweek";
}

// Switch expression — exhaustive, no fall-through, returns value
String result = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "weekday";
    case SATURDAY, SUNDAY -> "weekend";
};

// With blocks and yield
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY -> 7;
    case THURSDAY, SATURDAY -> 8;
    case WEDNESDAY -> {
        System.out.println("hump day");
        yield 9;                 // yield, not return
    }
};

// Switch on strings, enums, integers — all work
// Switch on types (pattern matching) — Java 21

Pattern Matching for instanceof Java 16

// Old — explicit cast after check
if (obj instanceof String) {
    String s = (String) obj;  // redundant cast
    System.out.println(s.length());
}

// New — binding variable in the pattern
if (obj instanceof String s) {
    System.out.println(s.length());  // s is scoped to this branch
}

// Negation works too
if (!(obj instanceof String s)) {
    return;  // s NOT in scope here
}
System.out.println(s.length());  // s in scope here

// Combine with &&
if (obj instanceof String s && s.length() > 5) {
    System.out.println("long string: " + s);
}

Pattern Matching in Switch Java 21

// Type patterns in switch
static String format(Object obj) {
    return switch (obj) {
        case Integer i -> "int: " + i;
        case Long l    -> "long: " + l;
        case Double d  -> "double: " + d;
        case String s  -> "string: " + s;
        case null      -> "null";
        default        -> "other: " + obj;
    };
}

// Guarded patterns (when clause)
static String classify(Number n) {
    return switch (n) {
        case Integer i when i < 0  -> "negative int";
        case Integer i when i == 0 -> "zero";
        case Integer i             -> "positive int";
        case Double d              -> "double";
        default                    -> "other number";
    };
}

// With sealed hierarchy (exhaustive — no default needed)
sealed interface Result<T> permits Ok, Err {}
record Ok<T>(T value) implements Result<T> {}
record Err<T>(String message) implements Result<T> {}

String describe(Result<Integer> r) {
    return switch (r) {
        case Ok<Integer> ok   -> "value: " + ok.value();
        case Err<Integer> err -> "error: " + err.message();
    };
}

Helpful NullPointerExceptions Java 14

// Before Java 14:
// NullPointerException at line 42
// (which field was null??)

// After Java 14+ (enabled by default in Java 15+):
// Cannot invoke "String.length()" because "str" is null
// Cannot read field "name" because "user.address" is null
// Cannot store to int[] because "array" is null

// No code change needed — JVM provides better messages automatically

Text Blocks, Records, Sealed Classes

Covered in earlier sections. Quick version markers:

  • Text blocks Java 15 — multiline string literals
  • Records Java 16 — immutable data classes
  • Sealed classes Java 17 — restricted inheritance
  • Pattern matching instanceof Java 16
  • Switch expressions Java 14
  • Virtual threads Java 21 — lightweight concurrency
  • Sequenced collections Java 21 — first/last access on List, Set, Map
String Templates (preview) and other upcoming features
// String Templates — Java 21-22 preview (withdrawn; not yet standardized)
// (Syntax may change — verify against your Java version)
String name = "Alice";
int age = 30;

// STR template processor
String s = STR."Hello \{name}, you are \{age} years old.";

// FMT processor — like printf
String formatted = FMT."Pi is approximately %.2f\{Math.PI}";

// RAW processor — returns StringTemplate, not String
StringTemplate raw = RAW."Hello \{name}";

11. Error Handling

Exception Hierarchy

// Throwable
//   Error         — JVM problems (OutOfMemoryError, StackOverflowError) — don't catch
//   Exception
//     RuntimeException (unchecked) — programming errors, not declared in throws
//       NullPointerException, IllegalArgumentException, IndexOutOfBoundsException
//       ClassCastException, UnsupportedOperationException, ArithmeticException
//     IOException (checked) — must declare or handle
//     SQLException (checked)
//     ... other checked exceptions

Checked vs Unchecked

CheckedUnchecked (RuntimeException)
Must handle/declareYes — compiler enforcesNo
When to useRecoverable conditions (file not found, network error)Programming bugs, invalid state
ExamplesIOException, SQLException, ParseExceptionNPE, IllegalArgumentException

try-with-resources

// AutoCloseable resources are automatically closed, even on exception
try (var conn = dataSource.getConnection();
     var stmt = conn.prepareStatement("SELECT 1")) {

    ResultSet rs = stmt.executeQuery();
    while (rs.next()) { /* process */ }
} catch (SQLException e) {
    log.error("DB error", e);
    throw new RuntimeException("query failed", e);  // wrap, preserve cause
}
// conn and stmt are closed here, in reverse order

// Implementing AutoCloseable
public class ResourceHolder implements AutoCloseable {
    @Override
    public void close() {
        // cleanup — suppress exceptions or rethrow as needed
    }
}

Multi-catch

// Before: duplicate handling
try {
    riskyMethod();
} catch (IOException e) {
    handle(e);
} catch (SQLException e) {
    handle(e);
}

// Multi-catch (Java 7+)
try {
    riskyMethod();
} catch (IOException | SQLException e) {
    handle(e);  // e is effectively final
}

// Catch hierarchy — catch specific before general
try {
    parse(input);
} catch (NumberFormatException e) {
    // specific — handle differently
} catch (IllegalArgumentException e) {
    // parent of NumberFormatException — catches others
} catch (Exception e) {
    // broad fallback
}

Custom Exceptions

// Checked — forces callers to handle
public class InsufficientFundsException extends Exception {
    private final double amount;

    public InsufficientFundsException(double amount) {
        super("Insufficient funds: need " + amount + " more");
        this.amount = amount;
    }

    public InsufficientFundsException(String message, Throwable cause) {
        super(message, cause);
        this.amount = 0;
    }

    public double getAmount() { return amount; }
}

// Unchecked — for programming errors or unrecoverable states
public class ConfigurationException extends RuntimeException {
    public ConfigurationException(String message) { super(message); }
    public ConfigurationException(String message, Throwable cause) {
        super(message, cause);
    }
}

// Usage — always preserve the cause chain!
try {
    loadConfig();
} catch (IOException e) {
    throw new ConfigurationException("failed to load config", e);  // wrap with cause
}

Best Practices

Exception handling guidelines
  • Never catch Exception or Throwable unless you're a framework/top-level handler
  • Never swallow exceptions silently — at minimum, log them
  • Preserve the cause chain: new RuntimeException("msg", originalException)
  • Prefer unchecked exceptions in application code — checked exceptions are for truly recoverable conditions
  • Don't use exceptions for control flow (expensive, obscures intent)
  • Validate inputs early with Objects.requireNonNull() and Preconditions
// Input validation
public void process(String name, int count) {
    Objects.requireNonNull(name, "name must not be null");
    if (count <= 0) throw new IllegalArgumentException("count must be positive, got: " + count);
    // proceed...
}

// Rethrowing
try {
    riskyOp();
} catch (Exception e) {
    log.error("operation failed", e);
    throw e;                           // rethrow same exception (preserves stack trace)
}

12. I/O and NIO

java.nio.file — Modern File API

// Path — immutable, OS-independent
Path path = Path.of("/tmp/data.txt");           // Java 11
Path relative = Path.of("data", "file.txt");    // data/file.txt
Path resolved = base.resolve("child.txt");       // base/child.txt
Path parent = path.getParent();                  // /tmp
Path filename = path.getFileName();              // data.txt
path.toString();
path.toAbsolutePath();

// Reading
String content = Files.readString(path);          // whole file as String (Java 11)
byte[] bytes   = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path);
Stream<String> lineStream = Files.lines(path);   // lazy, must close

// Writing
Files.writeString(path, "content");               // Java 11
Files.write(path, bytes);
Files.write(path, lines, StandardOpenOption.APPEND);
Files.writeString(path, "more", StandardOpenOption.CREATE, StandardOpenOption.APPEND);

// Copy, move, delete
Files.copy(src, dst);
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE);
Files.delete(path);                   // throws if not exists
Files.deleteIfExists(path);           // silent if not exists

// Directory operations
Files.createDirectory(path);          // single level
Files.createDirectories(path);        // create full path
Files.list(dir);                      // Stream<Path>, one level
Files.walk(dir);                      // Stream<Path>, recursive
Files.walk(dir, 2);                   // max depth
Files.find(dir, 5, (p, attr) -> p.toString().endsWith(".java"));

// Metadata
Files.exists(path);
Files.isDirectory(path);
Files.isReadable(path);
Files.size(path);
Files.getLastModifiedTime(path);

Buffered I/O for Large Files

// BufferedReader/Writer — wraps stream for performance
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = reader.readLine()) != null) {
        process(line);
    }
}

try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
    writer.write("line 1");
    writer.newLine();
    writer.write("line 2");
}

// PrintWriter — convenient for formatted output
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(path))) {
    pw.printf("Name: %s%n", name);
    pw.println("Done.");
}

NIO Channels and ByteBuffer (high-performance I/O)

// Channel + ByteBuffer — direct memory, minimal copies
try (FileChannel fc = FileChannel.open(path, StandardOpenOption.READ)) {
    ByteBuffer buf = ByteBuffer.allocateDirect(4096);
    while (fc.read(buf) > 0) {
        buf.flip();              // switch from write to read mode
        // process buf.array() or use buf.get()
        buf.clear();             // reset for next read
    }
}

// Memory-mapped file — large file processing
try (FileChannel fc = FileChannel.open(path, StandardOpenOption.READ)) {
    MappedByteBuffer mmap = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
    // Access like array — OS handles paging
    byte b = mmap.get(1000);
}
Charset pitfall
new FileReader(path) uses the platform default charset (system-dependent). Always specify explicitly: new InputStreamReader(stream, StandardCharsets.UTF_8) or use Files.newBufferedReader(path, StandardCharsets.UTF_8).

13. Memory & GC

Memory Layout

AreaWhat lives thereGC'd
HeapAll objects (new), class instancesYes
StackMethod frames, local variables, referencesNo (LIFO, auto)
MetaspaceClass metadata, method bytecode, String pool (Java 8+)Limited
NativeJNI code, NIO direct buffersManual/GC
# Useful memory flags
-Xms512m              # initial heap (set equal to Xmx to avoid resizing)
-Xmx2g                # max heap
-Xss512k              # per-thread stack size
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m

# GC logging (Java 9+)
-Xlog:gc*:file=gc.log:time,level,tags

# Heap dump on OOM
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof

GC Algorithm Comparison

GCTuning goalPauseUse when
Serial -XX:+UseSerialGCMinimal footprintStop-the-worldSingle-core, small heaps
Parallel -XX:+UseParallelGCThroughputStop-the-world (parallel)Batch jobs, throughput priority
G1 -XX:+UseG1GCBalanced< 200ms targetDefault in Java 9+, most apps
ZGC -XX:+UseZGCUltra-low latency< 1msLatency-sensitive, large heaps
ShenandoahLow latency< 10msSimilar to ZGC

Common Memory Leaks

// 1. Static collection accumulation
static List<Object> cache = new ArrayList<>();
// Objects added but never removed — leaks forever

// Fix: bounded cache, WeakReference, or proper invalidation
static Map<Key, WeakReference<Value>> weakCache = new WeakHashMap<>();

// 2. Unclosed resources
FileInputStream fis = new FileInputStream(path);
// exception thrown before close() — stream never released
// Fix: try-with-resources always

// 3. Inner class holding outer reference
class Outer {
    class Inner implements Runnable {
        public void run() { /* uses Outer.this implicitly */ }
    }
    // If Inner is submitted to long-lived executor, Outer can't be GC'd
}
// Fix: make Inner static, pass only needed fields

// 4. ThreadLocal leaks in thread pools
ThreadLocal<Connection> local = new ThreadLocal<>();
// Set in request thread from pool, never removed
// Fix: always call local.remove() in finally block

// 5. Listener/observer not removed
button.addActionListener(this);
// this is now referenced by button — can't be GC'd until button removed
// Fix: keep reference, call removeActionListener when done

finalize is Deprecated Removed Java 18

Don't use finalize()
finalize() is deprecated since Java 9, made for removal in Java 18+. It's unpredictable, slow, and can cause deadlocks. Use try-with-resources / AutoCloseable or java.lang.ref.Cleaner for resource cleanup.
// Use Cleaner for off-heap resource cleanup
import java.lang.ref.Cleaner;

public class NativeResource implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();

    private final Cleaner.Cleanable cleanable;

    public NativeResource() {
        // Capture only what the action needs (not 'this')
        long handle = allocateNative();
        this.cleanable = CLEANER.register(this, () -> freeNative(handle));
    }

    @Override public void close() {
        cleanable.clean();  // explicit cleanup
    }
}

14. Build Tools

Maven vs Gradle

MavenGradle
Config formatXML (pom.xml)Kotlin DSL (build.gradle.kts) or Groovy
Build modelFixed lifecycle phasesDAG of tasks (flexible)
PerformanceSlower (no incremental default)Faster (incremental, build cache)
FlexibilityConvention over configHighly customizable
Learning curveLowerHigher
EcosystemHuge, matureLarge, growing
IDE supportExcellentExcellent

Maven

# Lifecycle: validate -> compile -> test -> package -> verify -> install -> deploy
mvn clean package           # compile + test + create jar
mvn clean package -DskipTests  # skip tests
mvn test                    # run tests only
mvn install                 # install to local ~/.m2 repo
mvn dependency:tree         # visualize dependency graph
mvn dependency:analyze      # find unused/undeclared deps
mvn versions:display-dependency-updates  # check for updates
// pom.xml essentials
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>my-app</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <java.version>21</java.version>
    <maven.compiler.source>${java.version}</maven.compiler.source>
    <maven.compiler.target>${java.version}</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <!-- Compile dependency -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.16.0</version>
    </dependency>

    <!-- Test-only dependency -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.10.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Gradle (Kotlin DSL)

# Common tasks
./gradlew build          # compile + test + package
./gradlew test           # tests only
./gradlew clean build    # clean first
./gradlew dependencies   # dependency tree
./gradlew tasks          # list all available tasks
./gradlew run            # run application (with application plugin)
// build.gradle.kts
plugins {
    java
    application
}

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

application {
    mainClass.set("com.example.App")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.16.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
    useJUnitPlatform()
}
Maven dependency scopes
ScopeCompileTestRuntimeTransitively exported
compileYesYesYesYes (default)
providedYesYesNoNo
runtimeNoYesYesNo
testNoYesNoNo
optionalYesYesYesNo

15. Common Pitfalls

== vs equals()

// == compares references (identity)
// equals() compares values (content)

String a = new String("hello");
String b = new String("hello");
a == b;        // false — different objects
a.equals(b);   // true — same content

// Integer cache trap (applies to all Integer.valueOf in range -128..127)
Integer x = 127; Integer y = 127;
x == y;        // true — same cached object
Integer p = 128; Integer q = 128;
p == q;        // false — different objects
p.equals(q);   // true

// Rule: ALWAYS use equals() for object comparison
// Exception: comparing with null (use == null or != null)
if (str == null || str.isEmpty()) { ... }
Null comparison order matters
"literal".equals(variable) — the literal can never be null, preventing NPE. Alternatively use Objects.equals(a, b) which handles nulls safely on both sides.

ConcurrentModificationException

// WRONG — modifying a list while iterating
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
    if (s.equals("b")) list.remove(s);  // ConcurrentModificationException!
}

// RIGHT option 1 — removeIf (Java 8+)
list.removeIf(s -> s.equals("b"));

// RIGHT option 2 — collect what to remove, then remove after
List<String> toRemove = list.stream()
    .filter(s -> s.equals("b"))
    .collect(Collectors.toList());
list.removeAll(toRemove);

// RIGHT option 3 — iterator with explicit remove
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("b")) it.remove();  // safe!
}

// RIGHT option 4 — operate on a copy
new ArrayList<>(list).stream()
    .filter(s -> !s.equals("b"))
    .collect(Collectors.toList());

String Concatenation in Loops

// WRONG — O(n²) allocations
String result = "";
for (String s : items) {
    result += s + ", ";  // creates new String each iteration
}

// RIGHT — StringBuilder
StringBuilder sb = new StringBuilder();
for (String s : items) {
    sb.append(s).append(", ");
}
String result = sb.toString();

// BETTER — String.join or Collectors.joining
String result2 = String.join(", ", items);
String result3 = items.stream().collect(Collectors.joining(", "));

// NOTE: simple "a" + "b" + "c" is fine — compiler optimizes to StringBuilder

Autoboxing Performance Trap

// WRONG — creates Long object every iteration
Long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
    sum += i;  // unbox sum, add i, rebox result — 1M Long objects!
}

// RIGHT — use primitive
long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
    sum += i;
}

// Also: avoid Map<Integer, Integer> for counted frequency in hot loops
// Use int[] or IntStream.rangeClosed().sum() when possible

Mutable Objects as Map Keys

// NEVER put mutable objects as Map keys
List<String> key = new ArrayList<>(List.of("a", "b"));
Map<List<String>, Integer> map = new HashMap<>();
map.put(key, 1);

key.add("c");              // mutate the key
map.get(key);              // null — hashCode changed, bucket is wrong!
map.get(List.of("a","b")); // null — original hash no longer correct

// Use immutable keys: String, Integer, record, List.of(), Set.of()

Returning null from Collections Methods

// HashMap.get() returns null for missing keys
Map<String, List<String>> map = new HashMap<>();
List<String> items = map.get("missing");
items.add("x");   // NullPointerException!

// Safe patterns
List<String> items = map.getOrDefault("key", new ArrayList<>());
List<String> items = map.computeIfAbsent("key", k -> new ArrayList<>());

// Optional — signal "may not be present" at API level
Optional<User> user = userRepo.findById(id);
user.ifPresent(u -> process(u));
user.orElseThrow(() -> new UserNotFoundException(id));

equals() and hashCode() Contract

// If a.equals(b) then a.hashCode() == b.hashCode() MUST hold
// Violation breaks HashMap, HashSet

// WRONG — override equals but not hashCode
public class Point {
    int x, y;
    @Override public boolean equals(Object o) {
        Point p = (Point) o;
        return x == p.x && y == p.y;
    }
    // No hashCode override!
}
Set<Point> set = new HashSet<>();
set.add(new Point(1,2));
set.contains(new Point(1,2));  // false! Different hashCodes

// RIGHT — override both, or use record (handles automatically)
@Override public int hashCode() {
    return Objects.hash(x, y);  // consistent with equals
}

// IDEs and records generate this correctly — let them

ThreadLocal Leaks in Pools

private static final ThreadLocal<DateFormat> DATE_FORMAT =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// In a request handler:
void handleRequest() {
    DateFormat fmt = DATE_FORMAT.get();
    // ... use fmt
    DATE_FORMAT.remove();  // ALWAYS remove when done in pooled threads!
    // Without remove(), next request on same thread sees old value
}

Optional.get() Without Check

// WRONG
Optional<String> opt = findSomething();
String value = opt.get();   // NoSuchElementException if empty!

// RIGHT
String value = opt.orElse("default");
String value = opt.orElseGet(() -> computeDefault());
String value = opt.orElseThrow(() -> new IllegalStateException("missing value"));
opt.ifPresent(v -> process(v));
Enabling extra checks
Run with -ea flag to enable Java assertions in development: assert value > 0 : "value must be positive"; — throws AssertionError when assertion fails. Assertions are disabled by default at runtime.