Java Refresher
Core language + Modern Java 17+ features — quick reference for experienced developers
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)
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
-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
| Tool | Config file | Wrapper | Strengths |
|---|---|---|---|
| Maven | pom.xml | mvnw | Strict conventions, widely adopted in enterprise, large plugin ecosystem |
| Gradle | build.gradle / build.gradle.kts | gradlew | Faster incremental builds, flexible DSL, preferred in Android |
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.
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!
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.
| Term | What it is | Includes |
|---|---|---|
| JDK | Java Development Kit | javac, java, javadoc, jar, JRE |
| JRE | Java Runtime Environment | JVM + standard class libraries |
| JVM | Java Virtual Machine | Class loader, bytecode verifier, JIT compiler, GC |
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
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
| Type | Size | Range / Notes | Default |
|---|---|---|---|
byte | 8-bit | -128 to 127 | 0 |
short | 16-bit | -32,768 to 32,767 | 0 |
int | 32-bit | -2^31 to 2^31-1 | 0 |
long | 64-bit | -2^63 to 2^63-1 (suffix: L) | 0L |
float | 32-bit IEEE 754 | ~7 decimal digits (suffix: f) | 0.0f |
double | 64-bit IEEE 754 | ~15 decimal digits | 0.0d |
char | 16-bit Unicode | '\u0000' to '\uffff' | '\u0000' |
boolean | JVM-defined | true / false | false |
// 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()
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 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
| StringBuilder | StringBuffer | |
|---|---|---|
| Thread-safe | No | Yes (synchronized) |
| Performance | Faster | Slower (lock overhead) |
| Use when | Single 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
"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
| Need | Use | Why |
|---|---|---|
| Ordered list, fast random access | ArrayList | O(1) get, O(n) insert/remove middle |
| Frequent insert/remove from ends | LinkedList (as Deque) | O(1) head/tail ops, poor cache locality |
| Fast membership test, no order needed | HashSet | O(1) avg add/contains/remove |
| Sorted unique elements | TreeSet | O(log n), natural or custom order |
| Insertion-ordered set | LinkedHashSet | O(1) ops + predictable iteration |
| Key-value, fast lookup | HashMap | O(1) avg, unordered |
| Sorted map by key | TreeMap | O(log n), NavigableMap API |
| Insertion-ordered map | LinkedHashMap | Iteration in insertion order |
| Thread-safe map | ConcurrentHashMap | Segment locks, no full lock on reads |
| FIFO queue | ArrayDeque | Faster than LinkedList for queues |
| Priority ordering | PriorityQueue | Min-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();
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
| Interface | Method | Signature | Example |
|---|---|---|---|
Function<T,R> | apply | T -> R | transform a value |
BiFunction<T,U,R> | apply | (T,U) -> R | combine two values |
Predicate<T> | test | T -> boolean | filter condition |
BiPredicate<T,U> | test | (T,U) -> boolean | two-arg filter |
Consumer<T> | accept | T -> void | side-effect action |
BiConsumer<T,U> | accept | (T,U) -> void | two-arg side effect |
Supplier<T> | get | () -> T | lazy factory |
UnaryOperator<T> | apply | T -> T | transform same type |
BinaryOperator<T> | apply | (T,T) -> T | reduce 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()));
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);
});
}
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
| Checked | Unchecked (RuntimeException) | |
|---|---|---|
| Must handle/declare | Yes — compiler enforces | No |
| When to use | Recoverable conditions (file not found, network error) | Programming bugs, invalid state |
| Examples | IOException, SQLException, ParseException | NPE, 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
- Never catch
ExceptionorThrowableunless 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()andPreconditions
// 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);
}
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
| Area | What lives there | GC'd |
|---|---|---|
| Heap | All objects (new), class instances | Yes |
| Stack | Method frames, local variables, references | No (LIFO, auto) |
| Metaspace | Class metadata, method bytecode, String pool (Java 8+) | Limited |
| Native | JNI code, NIO direct buffers | Manual/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
| GC | Tuning goal | Pause | Use when |
|---|---|---|---|
Serial -XX:+UseSerialGC | Minimal footprint | Stop-the-world | Single-core, small heaps |
Parallel -XX:+UseParallelGC | Throughput | Stop-the-world (parallel) | Batch jobs, throughput priority |
G1 -XX:+UseG1GC | Balanced | < 200ms target | Default in Java 9+, most apps |
ZGC -XX:+UseZGC | Ultra-low latency | < 1ms | Latency-sensitive, large heaps |
| Shenandoah | Low latency | < 10ms | Similar 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
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
| Maven | Gradle | |
|---|---|---|
| Config format | XML (pom.xml) | Kotlin DSL (build.gradle.kts) or Groovy |
| Build model | Fixed lifecycle phases | DAG of tasks (flexible) |
| Performance | Slower (no incremental default) | Faster (incremental, build cache) |
| Flexibility | Convention over config | Highly customizable |
| Learning curve | Lower | Higher |
| Ecosystem | Huge, mature | Large, growing |
| IDE support | Excellent | Excellent |
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
| Scope | Compile | Test | Runtime | Transitively exported |
|---|---|---|---|---|
compile | Yes | Yes | Yes | Yes (default) |
provided | Yes | Yes | No | No |
runtime | No | Yes | Yes | No |
test | No | Yes | No | No |
optional | Yes | Yes | Yes | No |
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()) { ... }
"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));
-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.