Java Cheat Sheet language guide · core API index · Java 8 → 25

Written for someone who already knows how languages work and wants Java’s particular semantics: what the compiler guarantees, what erasure takes away, what the memory model actually promises, and where the sharp edges are. The first half is a guide to the language; the second is a searchable index of the language and the core API. Every API row was checked against a real JDK — type in the filter box to narrow it, hover any entry for the full declaration.

Available since: Java 7 or earlier 8 (lambdas, streams) 9–17 21+ legacy / avoid
Guide synthesised from the Java Language Specification and API documentation, Herbert Schildt’s Java: The Complete Reference, Joshua Bloch’s Effective Java and Goetz et al, Java Concurrency in Practice. API signatures generated from openjdk 21.0.2 2024-01-16 LTS.

The Language

semantics, guarantees and sharp edges — 26 sections

Running Java

javac does remarkably little: it type-checks, desugars and emits a class file whose version number is the only compatibility gate. Everything else — verification, linkage, optimisation, memory — happens in the VM, which is why the guide ends with a section on it. Class files are forward-compatible and never backward: build with --release set to the oldest runtime you must support, or the deploy fails with UnsupportedClassVersionError.

javac Hello.java # compiles to Hello.classjava Hello # runs Hello.main(String[])java Hello.java # source mode: compile in memory, run (11+)jshell # REPL: try expressions without writing a class (9+)

The tools you actually use

ToolWhat it does
javacCompiler. -d out output dir, -cp classpath, --release N compile against N’s API, -Xlint:all every warning
javaLauncher. -cp, -jar app.jar, -Dk=v system property, -X/-XX VM options, --enable-preview
jshellREPL. /list /vars /methods /edit /save /open /exit; /env -class-path x.jar
jarArchive. jar cfe app.jar Main -C out . creates an executable jar with Main-Class
javadocAPI docs from /** … */ comments
jdeps / jlink / jpackageDependency analysis · custom trimmed runtime image · native installer
javapDisassembler. javap java.lang.String prints the real signatures; -c prints bytecode
jcmd / jfrLive JVM control — thread dumps, heap dumps, Flight Recorder

Classpath vs module path

The classpath is a flat search list of directories and jars. The module path (-p, Java 9+) carries named modules that declare what they need and what they expose. Ordinary application code still runs happily on the classpath — it lands in the unnamed module, which reads everything.

-cp "out:lib/*"colon-separated on macOS and Linux, semicolon on Windows-p mods -m app/com.x.Mainmodule path, then module/main-classCLASSPATHenvironment variable; any explicit -cp overrides it

On this Mac

jenv versionslist installed JDKs; * marks the active onejenv global 21.0set the default JDKjenv local 17pin a JDK for one directory (.java-version)/usr/libexec/java_home -Vevery JDK macOS knows aboutecho $JAVA_HOMEwhat tools outside the shims will use

Homebrew installs JDKs under /opt/homebrew/opt/openjdk*; jenv puts shims on PATH ahead of them, so java -version and a build tool can disagree if JAVA_HOME was never exported. jenv enable-plugin export fixes that.

Program Structure & Lexical Basics

A source file is a sequence of Unicode characters holding, classically, one public top-level type whose name matches the file name. Everything executable lives inside a type; there are no free functions. Statements end in ;, blocks are { }, whitespace is insignificant, and names are case-sensitive.

package com.example.app; // 0 or 1, must be firstimport java.util.List; // 0 or moreimport static java.lang.Math.max; // static import of one member public class Hello { // file must be named Hello.java public static void main(String[] args) { System.out.println("Hello, " + String.join(" ", args)); }}

The entry point

The launcher looks for public static void main(String[])String... is the same erasure and works. The exit status comes from System.exit, never from main. Java 25 finalised the alternative the scripting world wanted: an implicitly declared class with an instance void main(), no public, no static, no class header at all.

Comments & documentation

// lineto end of line/* block */does not nest/** doc */javadoc: @param @return @throws @see {@code x} {@link T#m}

Identifiers & naming

An identifier starts with a letter, _ or $ and continues with those plus digits — Unicode letters count, so a variable named δ compiles. A lone _ is a keyword since 9 and the unnamed variable since 21. $ is reserved by convention for generated code.

KindConventionExample
packageall lower, reversed domaincom.example.util
class / interface / record / enumUpperCamelCase, a nounHttpClient
method / field / locallowerCamelCase, verb for methodsparseHeader
constantUPPER_SNAKEMAX_VALUE
type parameterone capital letterT E K V R

Keywords

50 reserved words, plus three reserved literals (true false null). None may be used as an identifier. Contextual keywords — var record sealed permits yield when module requires exports opens uses provides — are keywords only where the grammar expects them, so old code with a variable called record still compiles. All of them are in the index.

goto and const are reserved but unimplemented: they exist only so the compiler can reject them with a useful message.

Primitive Types, Literals & var

Java has exactly eight primitive types. They are not objects, live on the stack or inside an object, are never null, and their sizes are fixed by the specification on every platform — unlike C, there is no implementation-defined width.

TypeBitsRangeDefaultWrapper
byte8−128 … 1270Byte
short16−32,768 … 32,7670Short
int32−2,147,483,648 … 2,147,483,6470Integer
long64±9.22 × 10180LLong
float32IEEE 754, ~7 digits0.0fFloat
double64IEEE 754, ~15 digits0.0dDouble
char16UTF-16 code unit, 0 … 65,535 — unsigned'\0'Character
booleantrue / false onlyfalseBoolean

Defaults apply to fields and array elements only. A local variable has no default: reading one before assignment is a compile error, not garbage.

Literals

42 0b1010 0x1F 017decimal · binary (7+) · hex · octal — a leading zero means octal1_000_000 0xFF_FFunderscores anywhere between digits (7+)42L 3.14f 3.14dsuffix: long, float, double — double is the default1e-9 0x1.8p3exponent · hexadecimal floating point (equals 12.0)'A' '\n' 'é'char: single quotes, exactly one code unit"text" """block"""String: double quotes · text block (15+)

Trap: 017 is 15, not 17. And unicode escapes are processed before tokenising, so a written inside a // comment really does end the line and break the program.

Conversion & promotion

byte → short → int → long → float → doublewidening: automatic, never loses magnitude (may lose precision)char → int → long → float → doublechar widens too; byte and short do not widen to char(int) 3.99narrowing needs a cast; truncates toward zero, giving 3(byte) 200narrowing wraps: gives −56, silently

Every arithmetic operand narrower than int is promoted to int first, so byte a=1, b=2; byte c = a + b; does not compile — the sum is an int. Compound assignment hides an implicit cast: c += 1 compiles where c = c + 1 does not.

var — local type inference (10+)

var list = new ArrayList<String>();inferred as ArrayList<String>for (var e : map.entrySet())the usual win: no Map.Entry<K,V> noisevar x;error — needs an initialiservar y = null;error — null has no typevar f = (Runnable) () -> {};a lambda needs a target type, so cast it

var is not dynamic typing and not a keyword — the type is fixed at compile time. It is legal only for locals, for indices, try-with-resources and lambda parameters; never for fields, parameters or return types.

Operators & Precedence

Highest precedence first. Everything on one row binds equally and associates as shown. When in doubt, parenthesise — the compiler does not reward cleverness.

LevelOperatorsAssoc
postfixx++ x--
unary++x --x +x -x ~x !xright
cast / new(Type) x · new T()right
multiplicative* / %left
additive+ - (and String +)left
shift<< >> >>>left
relational< <= > >= instanceofleft
equality== !=left
bitwise& then ^ then |left
logical&& then || — short-circuitleft
ternaryc ? a : bright
assignment= += -= *= /= %= &= ^= |= <<= >>= >>>=right
lambda-> — lower than everything aboveright

The ones that bite

-7 / 2 is -3 · -7 % 2 is -1integer division truncates toward zero; % takes the dividend’s sign5 / 2 is 2 · 5 / 2.0 is 2.5int/int is integer division. One double operand fixes itInteger.MAX_VALUE + 1wraps silently to MIN_VALUE — use Math.addExact to throw instead-8 >> 1 is -4arithmetic shift: the sign bit is copied in-8 >>> 1 is 2147483644logical shift: zero fills. There is no <<<1 << 32 is 1shift distance is taken mod 32 for int, mod 64 for longa & b vs a && bon booleans both work; only && skips evaluating b0.1 + 0.2 == 0.3false. Compare doubles with a tolerance, or use BigDecimalx == y on objectsreference identity, never content — use equals

instanceof with a pattern (16+)

if (o instanceof String s && s.length() > 3) // s: in scope where the test held System.out.println(s.toUpperCase());

The binding is flow-scoped: it exists exactly where the compiler can prove the test succeeded — including everywhere after if (!(o instanceof String s)) return;.

Control Flow

C’s statements, minus the implicit conversions: a condition must be a genuine boolean, so if (x = 5) and if (list) are compile errors rather than idioms. Definite assignment is also checked — every path must assign a local before it is read.

if (c) { ... } else if (d) { ... } else { ... } while (c) { ... } // test first, may run zero timesdo { ... } while (c); // body runs at least once - note the semicolon for (int i = 0; i < n; i++) { ... } // any part may be empty: for (;;)for (int i = 0, j = n; i < j; i++, j--) // comma: init and update only for (String s : names) { ... } // for-each: arrays and Iterable

Breaking out

breakleave the innermost loop or switchcontinuenext iteration of the innermost loopbreak outer;leave the loop tagged outer: — Java’s answer to gotocontinue outer;next iteration of the labelled loopreturn v;leave the method — finally still runs
outer:for (int[] row : grid) for (int v : row) if (v == target) { found = true; break outer; }

for-each: what it costs you

The enhanced for hides the iterator, so you cannot get the index, cannot remove elements (ConcurrentModificationException — use Iterator.remove() or Collection.removeIf), and cannot walk two collections in lock-step. It works on any array and on anything implementing Iterable, including your own classes.

// wrong - throws ConcurrentModificationExceptionfor (String s : list) if (s.isBlank()) list.remove(s); list.removeIf(String::isBlank); // right (8+)

There is no goto, no comma operator outside for, and no statement-level unless. An empty statement is a bare ;while (c); is a legal infinite loop and a classic typo.

switch — Statement, Expression, Pattern

switch has grown three times. The old statement falls through and needs break. The arrow form (14+) does not fall through. The expression form produces a value and must be exhaustive. Pattern matching (21+) lets the labels be types rather than constants.

1 · Classic statement — fall-through is deliberate

switch (day) { case SATURDAY: case SUNDAY: type = "weekend"; break; // shared label default: type = "weekday"; // a forgotten break is a real bug}

2 · Arrow form — no fall-through, no break

switch (day) { case SATURDAY, SUNDAY -> type = "weekend"; default -> type = "weekday";}

3 · Expression form — produces a value

int len = switch (day) { case MONDAY, FRIDAY -> 6; case SATURDAY -> 0; default -> { int h = base(day); yield h + 2; } // block: yield};

4 · Pattern matching (21+)

String describe(Object o) { return switch (o) { case null -> "nothing"; // only since 21 case Integer i when i > 100 -> "big int " + i; // guard case Integer i -> "int " + i; case String s -> "string of " + s.length(); case int[] a -> "int array of " + a.length; default -> o.getClass().getSimpleName(); };}
RuleDetail
Selector typesClassic: byte short char int and their wrappers, String, enum. Pattern switch: any reference type
LabelsMust be compile-time constants in the classic form — not variables, not null unless you write case null
ExhaustiveRequired for every expression switch and every pattern switch. An enum or sealed selector can be exhaustive with no default
DominanceA broader pattern placed before a narrower one is a compile error — order from specific to general
NullWithout case null a null selector throws NullPointerException — in every form

default alone never matches null; to catch both write case null, default -> ….

Arrays

An array is an object with a fixed length, created at run time, with covariant typing and a bounds check on every access. The length is a final field, not a method: a.length, but s.length() for a String and list.size() for a collection — three spellings, one of Java’s oldest irritations.

int[] a = new int[5];five zeros. The size is fixed foreverint[] a = {1, 2, 3};initialiser — only at the declarationa = new int[]{1, 2, 3};anonymous array — needed anywhere elseint[][] g = new int[3][4];3 rows of 4; g[0] is itself an int[]int[][] j = new int[3][];jagged: rows allocated separately, may differ in lengthString[] names, ages;both are arrays. String names[], age; is not — keep [] on the type

What you get for free

a.lengtha final field, not a methoda[i]bounds-checked, throws ArrayIndexOutOfBoundsExceptiona.clone()shallow copy, one level deepArrays.toString(a)readable text — a.toString() prints [I@1b6d3586Arrays.deepToString(g)the same for nested arraysArrays.sort / binarySearch / fill / equals / copyOfthe workhorsesArrays.asList(a)fixed-size view: set works, add throwsArrays.stream(a)the door into the streams worldSystem.arraycopy(src,i,dst,j,n)the fastest bulk move

Covariance — the hole in the type system

Object[] objs = new String[2]; // legal: arrays are covariantobjs[0] = 42; // compiles, throws ArrayStoreException at run time

Generics were deliberately made invariant to avoid exactly this: List<Object> l = new ArrayList<String>() does not compile. Arrays kept covariance for pre-generics compatibility, and pay for it with a type check on every store.

You cannot create an array of a generic type — new T[n] and new List<String>[10] are errors, because erasure would leave the store check with nothing to check. Use (T[]) new Object[n] with a suppressed warning, or a List.

Strings & Text

String is immutable and final. Every method that appears to change one returns a new object and leaves the original untouched. That is what makes strings safely shareable across threads, usable as map keys, and cacheable.

s.concat(" x"); // result droppedimmutability’s classic beginner bug — assign the resultString a = "hi", b = "hi";a == b is true: literals are interned in the string poolnew String("hi") == "hi"false — a fresh object. Always compare with equalss.intern()force a string into the pool

Building strings

+fine for a few pieces; compiled through invokedynamic since 9StringBuildermutable, not synchronised — the default choice in a loopStringBufferthe synchronised original; you almost never need itString.join(", ", list)delimiter join (8+)stream.collect(joining(", ", "[", "]"))join with prefix and suffix"ab".repeat(3)(11+)

Never build a string with += inside a loop: every pass allocates a new builder, copies everything and throws it away — O(n²). One StringBuilder outside the loop is O(n).

Text blocks (15+)

String json = """ {"name": "%s", "id": %d} """.formatted(name, id);

The opening """ must be followed by a line terminator. Incidental whitespace — the indentation common to every line and to the closing delimiter — is stripped, so move the closing """ to control the left margin. Trailing spaces go too; \s keeps one and a trailing \ suppresses the newline. No escaping of " is needed.

Formatting & parsing

"%-10s|%5.2f|%,d|%08.3f%n"left-pad · width.precision · grouping · zero-fill · platform newlineString.format(fmt, args)or fmt.formatted(args) since 15System.out.printf(fmt, args)same syntax, straight to the streamInteger.parseInt(s)gives an int; NumberFormatException on junkInteger.parseInt(s, 16)radix 2 to 36Integer.toBinaryString(n)also toHexString and toOctalString

char, code unit, code point

A char is one UTF-16 code unit, not a character. Anything above U+FFFF — emoji, many CJK extensions, historic scripts — is a surrogate pair and occupies two of them, so an emoji has length() == 2. Use codePointAt, codePoints() and codePointCount when correctness matters.

Classes, Objects & Constructors

Single inheritance of state, multiple of interface, everything on the heap, everything by reference. The parts worth knowing precisely are the initialisation order, what this may safely do before construction finishes, and the fact that Java has no destructor at all.

public class Account { private final String id; // instance field, set once private long balance; private static int count; // one copy for the whole class public Account(String id, long opening) { // no return type this.id = id; // this. disambiguates field from parameter this.balance = opening; count++; } public Account(String id) { this(id, 0); } // this(): first statement public long balance() { return balance; } public static int count() { return count; } // static: no this}

Construction, in order

#What runs
1Memory allocated, every field set to its default (0 / false / null)
2The superclass constructor — explicitly via super(…), or an implicit no-arg super()
3Instance field initialisers and { } instance blocks, in source order
4The rest of the constructor body

Static fields and static { } blocks run once, in source order, when the class is first initialised — not when it is loaded. Calling an overridable method from a constructor is a classic bug: the subclass override runs before the subclass fields are initialised, and sees nulls.

Modifiers worth knowing

final fieldassign exactly once, in the declaration or every constructorfinal methodcannot be overriddenfinal classcannot be extended (String, Integer, all wrappers)staticbelongs to the class; a static nested class needs no outer instancetransientskipped by Java serializationvolatileevery read sees the latest write — see Concurrencysynchronizedmethod takes the instance’s (or class’s) monitornative / strictfpimplemented in C · strict IEEE FP (a no-op since 17)

Varargs

int sum(int... xs) { int t = 0; for (int x : xs) t += x; return t; } // xs: int[]sum(); sum(1); sum(1, 2, 3); sum(new int[]{1, 2, 3}); // all legal

Only one varargs parameter, and it must be last. Overload resolution prefers a fixed-arity match, so sum(1,2) picks sum(int,int) if it exists. Passing a generic varargs array warns about heap pollution — answer it with @SafeVarargs.

Objects have no destructor

The garbage collector reclaims unreachable objects at a time of its choosing. finalize() is deprecated for removal and must not be used; System.gc() is a hint the JVM may ignore. For anything holding a file handle, socket or lock, implement AutoCloseable and let try-with-resources close it deterministically.

Inheritance & Polymorphism

Dispatch is virtual by default — the opposite of C++ — for every instance method that is not private, static or final. Fields and static methods are the exception: they are hidden, resolved against the static type, and that asymmetry is the source of most inheritance surprises.

abstract class Animal { protected final String name; Animal(String name) { this.name = name; } abstract String speak(); // no body: subclasses must supply one public String toString() { return name + " says " + speak(); }}class Dog extends Animal { Dog(String n) { super(n); } // super(...) first statement @Override String speak() { return "Woof"; }}Animal a = new Dog("Rex"); // upcast: always safea.speak(); // dispatches to Dog.speak - "Woof"

Overriding vs overloading vs hiding

What it isChosen
OverrideSame name, same parameters, in a subclassat run time, by the object’s class
OverloadSame name, different parametersat compile time, by the static types
Hidestatic methods and all fieldsat compile time, by the reference type

An override may widen access, must not narrow it, may return a subtype (covariant return), and may throw fewer or narrower checked exceptions — never more. Always write @Override: it turns a silent typo into a compile error.

super

super(args)call a superclass constructor — first statement onlysuper.method()call the superclass version from inside an overridesuper.fieldreach a hidden fieldIface.super.method()pick one interface’s default method by name

Every class inherits from Object

toString()override it — the default prints Type@hashcodeequals(Object)reflexive, symmetric, transitive, consistent, and false for nullhashCode()equal objects must have equal hash codes, or HashMap loses themgetClass()the run-time Class object; final, cannot be overriddenclone()protected, shallow, needs Cloneable — a copy constructor is betterwait / notify / notifyAllthe classic monitor methods; prefer java.util.concurrent

The equals/hashCode contract is the single most consequential rule in the language: break it and objects vanish from hash-based collections. Let the IDE generate the pair, use Objects.equals and Objects.hash, or use a record, which generates both correctly for you.

Interfaces

An interface is a contract: a set of method signatures a class promises to provide. A class may implement any number of them, which is how Java gets the useful part of multiple inheritance without inheriting state twice.

public interface Shape { double area(); // implicitly public abstract double PRECISION = 1e-9; // implicitly public static final default String describe() { // 8+: a body subclasses inherit return getClass().getSimpleName() + " of " + round(area()); } static Shape unit() { return () -> 1.0; } // 8+: not inherited private double round(double d) { return Math.round(d*100)/100.0; } // 9+}
MemberImplicitlySince
method with no bodypublic abstract1.0
fieldpublic static final — a constant, never state1.0
default methodpublic, inherited, overridable8
static methodpublic, not inherited — call it on the interface8
private / private statichelper for the above, invisible outside9

Why default exists

Adding a method to a published interface used to break every implementation on earth. Java 8 needed to add forEach to Iterable and stream() to Collection, so it gained method bodies in interfaces. The rule when two supertypes supply the same method: a class beats an interface, a more specific interface beats a less specific one, and anything still ambiguous is a compile error you resolve by overriding and calling Iface.super.method().

Interface vs abstract class

InterfaceAbstract class
How manymany per classexactly one
Stateconstants onlyany fields, including mutable
Constructornoneyes (called via super)
Memberspublic (plus private helpers)any access level
Use it fora capability: can be compared, can be closedshared implementation among close relatives

Functional interfaces

An interface with exactly one abstract method is functional and can be the target of a lambda. @FunctionalInterface is optional but makes the compiler enforce it. default, static and Object methods do not count toward the one.

Nested, Inner & Anonymous Classes

Four kinds, and the difference that matters is whether an instance of the outer class is needed.

KindDeclaredHolds outer this
static nestedstatic class N inside a classno — just a namespaced class
innerclass N inside a classyes — needs an outer instance
localinside a method or blockyes, plus captured locals
anonymousnew Iface() { … }yes, plus captured locals
class Outer { private int x = 1; static class Nested { int f() { return 2; } } // new Outer.Nested() class Inner { int f() { return x; } } // outer.new Inner() void go() { int local = 3; // effectively final class Local { int f() { return x + local; } } // local class Runnable r = new Runnable() { // anonymous class public void run() { System.out.println(x + local); } }; }}

Capture: effectively final

A local, anonymous or lambda body may read a local variable only if it is final or effectively final — never reassigned after initialisation. The value is copied into the object; there is no closure over the variable itself, so a later change could not be seen and the compiler refuses rather than lie. Fields are captured through this, so they may change freely.

int n = 0; list.forEach(s -> n++);error — n is not effectively finalint[] n = {0}; list.forEach(s -> n[0]++);compiles, but say what you mean: use a stream or an AtomicIntegerOuter.this.xreach the enclosing instance from an inner class

Anonymous class or lambda?

A lambda is shorter, has no this of its own (this means the enclosing instance) and creates no extra class file. An anonymous class can implement an interface with several methods, can extend a class, and can hold state. Use a lambda for a functional interface; reach for the anonymous class only when a lambda cannot express it.

A non-static inner class keeps a hard reference to its outer instance, which is a real memory-leak source in long-lived listeners and in Runnables handed to an executor. Make it static unless it genuinely needs the outer object.

Enums

An enum is a class whose instances are a fixed, named set created once by the JVM. That makes it type-safe, ordered, serialisable, usable in switch, and the correct way to write a singleton.

public enum Planet { MERCURY(3.303e23, 2.4397e6), // constructor arguments EARTH (5.976e24, 6.37814e6); // semicolon before other members private final double mass, radius; // fields may be per-constant Planet(double m, double r) { mass = m; radius = r; } // always private double gravity() { return 6.673E-11 * mass / (radius * radius); }}

What every enum gets

values()a fresh array of the constants, in declaration ordervalueOf("EARTH")lookup by name; IllegalArgumentException if unknownname()the identifier exactly as written — finalordinal()0-based position. Never persist it: reordering breaks your datacompareToby ordinal, so enums sort in declaration orderEnumMap / EnumSetarray-backed, far faster than HashMap/HashSet for enum keys

Constant-specific bodies

public enum Op { PLUS { public int apply(int a, int b) { return a + b; } }, TIMES { public int apply(int a, int b) { return a * b; } }; public abstract int apply(int a, int b); // each constant supplies one}

Each constant with a body is an anonymous subclass, which is why getClass() may report Op$1 and why an enum with bodies is not final. An enum may implement interfaces; it may never extend a class, because it already extends java.lang.Enum.

Enums make the best singletons: enum Registry { INSTANCE; … } is thread-safe on first use, immune to reflection, and correct across serialization — none of which the double-checked-locking idiom guarantees on its own.

Records & Pattern Matching

A record (16+) is a transparent carrier for immutable data. You declare the state; the compiler writes the constructor, accessors, equals, hashCode and toString. It is the answer to the 60-line data class.

public record Point(int x, int y) { } Point p = new Point(3, 4);p.x(); // accessor is x(), not getX()p.equals(new Point(3, 4)); // true - component-by-componentp.toString(); // Point[x=3, y=4]

Constructors

public record Range(int lo, int hi) { public Range { // compact: validate or normalise here if (lo > hi) throw new IllegalArgumentException("lo > hi"); } // fields are assigned for you afterwards public Range(int hi) { this(0, hi); } // extra ctor must delegate public int span() { return hi - lo; } // extra methods are fine static Range empty() { return new Range(0, 0); }}
A record…
is finaland implicitly extends java.lang.Record — so it cannot extend anything else
may implement interfacesand may be generic, nested, local (16+) or a member of a sealed hierarchy
has no other instance fieldsonly the components. Static fields are allowed
is shallowly immutablea component of type List is still a mutable list — copy it in the compact constructor

Record patterns (21+)

sealed interface Shape permits Circle, Rect { }record Circle(Point c, double r) implements Shape { }record Rect(Point tl, Point br) implements Shape { } double area(Shape s) { return switch (s) { // no default needed case Circle(Point c, double r) -> Math.PI * r * r; case Rect(Point(var x1, var y1), // patterns nest Point(var x2, var y2)) -> Math.abs((x2-x1)*(y2-y1)); };}

Deconstruction gives you the components as named variables in one step, and nests as deep as the data. With a sealed hierarchy the compiler proves the switch is exhaustive and tells you the day you add a fourth shape and forget a case — the one thing a chain of instanceof never did.

Sealed Types

sealed (17+) is controlled inheritance: the type names exactly which types may extend or implement it. That closes the hierarchy, which lets the compiler reason about it — exhaustive switches with no default, and a guarantee no stranger will add a fourth case at run time.

public sealed interface Expr permits Num, Add, Neg { } record Num(double v) implements Expr { } // records are finalrecord Add(Expr l, Expr r) implements Expr { }final class Neg implements Expr { final Expr e; /* ... */ }
Every permitted subtype must be…Meaning
finalthe hierarchy stops here (a record always qualifies)
sealedit names its own permitted subtypes and the closure continues
non-sealeda deliberate escape hatch: from here down, anyone may extend

Permitted subtypes must be in the same module, or in the same package if the code is on the classpath. permits may be omitted entirely when every subtype sits in the same source file — the compiler infers the list.

Sealed + records = algebraic data types

double eval(Expr e) { return switch (e) { // exhaustive: no default clause case Num(double v) -> v; case Add(Expr l, Expr r) -> eval(l) + eval(r); case Neg n -> -eval(n.e); };}

This is the sum type ML and Haskell have had for decades, and it changes how you model data: closed set of shapes, open set of operations — the opposite trade-off from classic polymorphism, and the better one whenever the data is fixed and the operations keep arriving.

Packages, Access & Modules

A package is a namespace and an access boundary; its name maps to a directory path. A module (9+) is a named set of packages that declares what it needs and what it exposes.

ModifierClassPackageSubclassWorld
privateyes
(none) package-privateyesyes
protectedyesyesyes
publicyesyesyesyes

The default is not public — it is package-private, and it is the right default for anything that is not deliberately API. protected also grants package access, which surprises people; and a subclass in another package may use protected members only through a reference of its own type.

Imports

import java.util.List;one typeimport java.util.*;every type in that package — not subpackages, and no cost at run timeimport static java.lang.Math.PI;use PI unqualifiedimport static java.lang.Math.*;every static member

java.lang is imported automatically. Two star-imports offering the same simple name is not an error until you use it; then qualify it or import the type explicitly.

module-info.java (9+)

module com.example.app { requires java.net.http; // I need this module requires transitive com.example.api; // my dependents need it too requires static lombok; // compile time only exports com.example.app.api; // public to everyone exports com.example.app.spi to com.example.impl; // qualified export opens com.example.app.model; // deep reflection (JSON, JPA) uses com.example.spi.Codec; // ServiceLoader consumer provides com.example.spi.Codec with com.example.app.ZipCodec;}

Strong encapsulation is the point: a package that is not exportsed is unreachable from outside the module even if its classes are public, and reflection into a package that is not opensed fails. That is what closed off sun.misc.Unsafe and friends.

java --list-modulesevery module in this runtimejava --describe-module java.basewhat it exportsjdeps --print-module-deps app.jarwhat a jar actually needs — feed it to jlink--add-opens m/pkg=ALL-UNNAMEDthe escape hatch when a library needs deep reflection

Exceptions

An exception is an object thrown up the call stack until a matching catch handles it. Java is nearly alone in having checked exceptions: the compiler insists you either handle them or declare them.

Throwable├── Error unchecked - the VM is in trouble. Do not catch│ ├── OutOfMemoryError StackOverflowError NoClassDefFoundError└── Exception CHECKED - must be caught or declared ├── IOException SQLException InterruptedException ... └── RuntimeException unchecked - a bug in the caller, usually ├── NullPointerException IllegalArgumentException ├── IndexOutOfBoundsException IllegalStateException └── ClassCastException ArithmeticException ...

try / catch / finally

try { risky();} catch (FileNotFoundException | AccessDeniedException e) { // multi (7+) log.warn("cannot read {}", path, e); // e is implicitly final} catch (IOException e) { // subclasses first, or unreachable throw new UncheckedIOException(e); // wrap - keep the cause} finally { // always runs: normal exit, exception, or return}

try-with-resources (7+) — use it for everything closeable

try (var in = Files.newBufferedReader(src); var out = Files.newBufferedWriter(dst)) { // closed in reverse in.transferTo(out);} // no finally needed

Any AutoCloseable works. If the body and close() both throw, the body’s exception propagates and close()’s is attached to it — retrievable with getSuppressed(). The hand-written finally { in.close(); } loses the original exception instead, which is exactly why this syntax exists.

Rules and traps

throws IOExceptiondeclare what you do not handle; part of your APIthrow new IllegalStateException("why", cause)always pass the cause — never swallow a stack tracecatch (Exception e) { }the worst line in Java. It hides bugs, including your own NPEsreturn inside finallydiscards a pending exception. Never do itcatch (InterruptedException e)restore the flag: Thread.currentThread().interrupt()e.printStackTrace()fine in a scratch program, wrong in a service — use the loggerhelpful NullPointerExceptionsince 14 the message names the exact expression that was null

Checked or unchecked? The rule that has aged well: checked when a careful caller can recover (a missing file, a refused connection), unchecked when the call itself was wrong (a null argument, a bad index). Never make a checked exception you expect nobody to catch.

Generics

Generics move a whole class of ClassCastExceptions from run time to compile time. List<String> is a list that can only hold strings, and get returns a String with no cast. The catch is erasure: the type arguments exist for the compiler and are gone in the bytecode.

class Box<T> { // T is a type parameter private T value; void set(T v) { value = v; } T get() { return value; }}Box<String> b = new Box<>(); // diamond: argument inferred (7+) static <T extends Comparable<T>> T max(List<T> xs) // generic method

Bounds and wildcards

<T>any reference type — never a primitive, so no List<int><T extends Number>upper bound: T is a Number, so T has doubleValue()<T extends Number & Comparable<T>>several bounds, class firstList<?>unknown type: you may read Objects, and add nothing but nullList<? extends Number>producer: read Numbers out, cannot addList<? super Integer>consumer: add Integers in, reads give Object

PECSProducer Extends, Consumer Super. A method that reads from a collection takes ? extends T; one that writes into it takes ? super T; one that does both takes plain T. That is the whole of Collections.copy(List<? super T> dst, List<? extends T> src).

Invariance

List<String> is not a List<Object>, even though String is an Object. If it were, you could add an Integer through the wider reference. Arrays chose covariance and pay with a run-time check; generics chose invariance and give you a compile error instead.

Erasure — what it takes away

new T() · new T[n]impossible — pass a Supplier<T> or a Class<T>o instanceof List<String>illegal; only instanceof List<?> isvoid f(List<String>) / f(List<Integer>)same erasure — will not compile as an overload pairT.class · catch (T e)not allowed; a generic class may not extend Throwablestatic T field;no static member may use the class’s type parameterlist.getClass()ArrayList — the element type is not there at run time

A raw type (List with no argument) turns every check off and infects the whole expression — it exists only for pre-2004 code. List<?> is the safe way to say "some list". Generic varargs create an array of a non-reifiable type: the compiler warns about heap pollution, and @SafeVarargs is your promise not to store into it.

Lambdas & Method References

A lambda is a functional-interface instance written as an expression, with parameter types inferred from the target type. It is deliberately not sugar for an anonymous class: the compiler emits an invokedynamic and lets LambdaMetafactory decide the representation, so a non-capturing lambda is created once and there is no class file per occurrence.

() -> 42no parametersx -> x * 2one parameter needs no parentheses or type(String a, String b) -> a + bexplicit types when inference needs help(a, b) -> { int c = a + b; return c; }a block body must return explicitly(var a, var b) -> a + bvar in parameters (11+), so annotations can be attached

Method references

FormExampleEquivalent lambda
Type::staticMethodInteger::parseInts -> Integer.parseInt(s)
object::instanceMethodSystem.out::printlnx -> System.out.println(x)
Type::instanceMethodString::toUpperCases -> s.toUpperCase()
Type::newArrayList::new() -> new ArrayList<>()
Type[]::newString[]::newn -> new String[n]

The interfaces in java.util.function

InterfaceMethodShape
Supplier<T>get() → T
Consumer<T>acceptT → void
Function<T,R>applyT → R
Predicate<T>testT → boolean
UnaryOperator<T>applyT → T
BiFunction<T,U,R>apply(T,U) → R
BinaryOperator<T>apply(T,T) → T
Runnable / Callable<V>run / call() → void · () → V throws

Each has primitive specialisations — IntPredicate, ToLongFunction, ObjIntConsumer and friends — purely to avoid boxing in hot code. Most compose: f.andThen(g), f.compose(g), p.and(q).negate(), Predicate.not(String::isBlank).

Rules

captures effectively final localsreassigning a captured local is a compile errorthismeans the enclosing instance — a lambda has no this of its ownno checked exceptionsunless the target interface declares them — the usual friction with IO in streamsnot serializable by defaultand no identity: two identical lambdas may or may not be the same object

The Collections Framework

Interfaces are the contract, classes the trade-off. The implementation details that actually change behaviour: ArrayList grows by half and copies; HashMap resizes at a 0.75 load factor and converts a bucket to a red-black tree past eight entries, so a hash-collision attack degrades to O(log n) rather than O(n); and every non-concurrent collection is fail-fast — a modCount check that throws ConcurrentModificationException on a best-effort basis, which makes it a bug detector, never a synchronisation mechanism.

Iterable└── Collection ├── List ordered, duplicates ArrayList LinkedList ├── Set no duplicates HashSet LinkedHashSet TreeSet ├── Queue head-first ArrayDeque PriorityQueue └── Deque both ends ArrayDeque LinkedList Map keys to values HashMap LinkedHashMap TreeMap (Map is NOT a Collection: no iterator. Use entrySet/keySet/values)
NeedUseBecause
indexed listArrayListO(1) get, amortised O(1) append. The default
queue / stackArrayDequefaster than LinkedList and than the legacy Stack
set membershipHashSetO(1). LinkedHashSet to keep insertion order
sorted set / mapTreeSet / TreeMapO(log n), plus first/last/headSet/tailSet/floor/ceiling
key to valueHashMapO(1). Keys need equals + hashCode
enum keysEnumMap / EnumSetarray-backed, tiny and fast
shared across threadsConcurrentHashMap, CopyOnWriteArrayListnever Collections.synchronizedX unless you also lock while iterating

Complexity worth remembering

getaddcontainsremove
ArrayListO(1)O(1)*O(n)O(n)
LinkedListO(n)O(1)O(n)O(1) at a known node
HashMap / HashSetO(1)O(1)O(1)O(1)
TreeMap / TreeSetO(log n)O(log n)O(log n)O(log n)

* amortised: appending grows the backing array by half when it fills. HashMap degrades to O(log n) per bucket, not O(n), since 8 — long chains become red-black trees.

Modern conveniences

List.of(a,b,c) Map.of(k,v,...)immutable, null-hostile, cheap (9+)List.copyOf(other)immutable snapshot (10+)map.getOrDefault(k, d)no null dance (8+)map.computeIfAbsent(k, x -> new ArrayList<>()).add(v)the multimap idiommap.merge(k, 1, Integer::sum)counting in one linelist.sort(comparing(P::name).thenComparing(P::age))comparator chaining (8+)list.getFirst() / getLast() / reversed()SequencedCollection (21+)stream.toList()an immutable list from a stream (16+)

Streams & Optional

A stream is a pipeline over a source, not a data structure: it stores nothing, does not modify the source, is lazy until a terminal operation runs, and can be consumed only once. It describes what to compute; the library decides how.

List<String> names = people.stream() // source .filter(p -> p.age() >= 18) // intermediate - lazy .map(Person::name) // intermediate - lazy .sorted() // stateful intermediate .toList(); // terminal - runs everything
StageOperations
Sourcecollection.stream() · Arrays.stream(a) · Stream.of(…) · IntStream.range(0,n) · Files.lines(p) · Stream.iterate / generate (infinite — must be limited)
Intermediatefilter map flatMap mapMulti peek distinct sorted limit skip takeWhile dropWhile
TerminalforEach toList toArray collect reduce count min max anyMatch allMatch noneMatch findFirst findAny
CollectorstoList toSet toMap joining counting summingInt averagingDouble groupingBy partitioningBy mapping teeing

Collectors that earn their keep

import static java.util.stream.Collectors.*; Map<Dept, List<Person>> byDept = staff.stream() .collect(groupingBy(Person::dept));Map<Dept, Long> headcount = staff.stream() .collect(groupingBy(Person::dept, counting()));Map<Boolean, List<Person>> split = staff.stream() .collect(partitioningBy(p -> p.salary() > 50_000));String csv = staff.stream().map(Person::name).collect(joining(", "));

Primitive streams

IntStream.range(0, n)half-open; rangeClosed includes the endstream.mapToInt(String::length).sum()no boxing, and sum/average/max exist only hereintStream.boxed()back to Stream<Integer> when you need a collectorsummaryStatistics()count, sum, min, average, max in one pass

Optional — a return type, not a field type

Optional.of(x) ofNullable(x) empty()of throws on null; ofNullable accepts itorElse(d) orElseGet(sup) orElseThrow()orElse evaluates its argument even when present — orElseGet does notmap flatMap filterchain without ever unwrappingifPresent(c) ifPresentOrElse(c, r)(9+)isPresent() + get()the anti-pattern — you have reinvented the null check

Traps. A stream throws IllegalStateException if reused. peek is for debugging and may be skipped entirely. Lambdas must not mutate shared state — forEach writing into a list is how a parallel stream corrupts data; collect instead. parallelStream() pays off only for large, CPU-bound, side-effect-free work on a splittable source, and it shares one common ForkJoinPool with the rest of the process. Collectors.toMap throws on a duplicate key unless you pass a merge function.

Concurrency & Virtual Threads

Java has had threads since 1.0 and a formal memory model since 5. The model is the part that matters: without a happens-before relationship between a write in one thread and a read in another, there is no guarantee the read ever sees the write — whatever the hardware seems to do in testing.

synchronizedmutual exclusion and a happens-before edge on the same monitorvolatileno exclusion, but every read sees the latest write; kills caching and reorderingfinal fieldsafely visible after construction — if this did not escape the constructorThread.start() / join()everything before start is visible in the thread; everything in it is visible after joinjava.util.concurrentevery class in it establishes the edges for you

Do not manage threads by hand

try (var pool = Executors.newFixedThreadPool(4)) { // closeable, 19+ Future<Integer> f = pool.submit(() -> compute(1)); // Callable pool.execute(() -> log("fire and forget")); // Runnable int v = f.get(); // blocks; wraps in ExecutionException} // shutdown + awaitTermination
ToolFor
ExecutorServicea pool that outlives the tasks. newFixedThreadPool, newCachedThreadPool, newScheduledThreadPool, newVirtualThreadPerTaskExecutor (21+)
CompletableFuturechaining async work: supplyAsync thenApply thenCompose thenCombine exceptionally allOf
ConcurrentHashMapthe default shared map. compute, merge and putIfAbsent are atomic
AtomicInteger / LongAdderlock-free counters; LongAdder wins under heavy contention
ReentrantLock / ReadWriteLockwhen you need tryLock, a timeout, fairness or several conditions
CountDownLatch / Semaphore / CyclicBarrierone-shot gate · permit counting · repeated rendezvous
BlockingQueueproducer/consumer without writing a single wait

Virtual threads (21+)

Thread.startVirtualThread(() -> handle(socket)); // millions are fine try (var ex = Executors.newVirtualThreadPerTaskExecutor()) { for (var req : requests) ex.submit(() -> handle(req)); // one each}

A virtual thread is scheduled by the JVM onto a small pool of carrier threads. Blocking on IO parks it and frees the carrier, so ordinary blocking code scales like async code without the callback rewrite. Do not pool them — create one per task. Their weak point is synchronized blocks that block while held, which pin the carrier; use a ReentrantLock in that hot path instead.

The classics still bite: check-then-act is not atomic; i++ is three operations; a lock taken in different orders deadlocks; Thread.stop and suspend are gone; and InterruptedException means somebody asked you to stop — either propagate it or restore the flag with Thread.currentThread().interrupt().

Files, I/O & Time

Two generations coexist. java.io is streams and readers — still the way bytes and characters actually move. java.nio.file (7+) is Path and Files, and it is what you should be calling to touch the filesystem.

InputStream / OutputStreambytesReader / Writercharacters — wrap a stream with a charsetBuffered*always wrap: unbuffered IO is one syscall per byteFilelegacy; its methods return false instead of saying why. Prefer Path

NIO.2 in practice

Path p = Path.of("data", "in.csv"); // no separators to get wrongString s = Files.readString(p); // 11+, UTF-8 by default since 18List<String> ls = Files.readAllLines(p);Files.writeString(out, s, CREATE, TRUNCATE_EXISTING); try (var lines = Files.lines(p)) { // lazy and closeable long n = lines.filter(l -> !l.isBlank()).count();}try (var walk = Files.walk(dir)) { // recursive; find() takes a matcher walk.filter(Files::isRegularFile).forEach(System.out::println);}
Files.exists / size / isDirectorythe questionsFiles.createDirectories / copy / move / deletethe verbs; deleteIfExists avoids the throwFiles.newBufferedReader(p)UTF-8 by defaultFiles.createTempFile(pre, suf)and deleteOnExit if it must not lingerp.resolve("x") p.getParent() p.toAbsolutePath()path algebra, no string surgery

Console, and other odds

System.out / err / inthe three standard streamsnew Scanner(System.in).nextLine()convenient; slow, and mixes next() with nextLine() badlySystem.getProperty("user.home")also line.separator, file.encoding, java.versionSystem.getenv("PATH")environmentnew ProcessBuilder("ls","-l").start()run a program; inheritIO() to see its output

java.time (8+) — the third and correct date API

LocalDate / LocalTime / LocalDateTimeno zone, no instant — a date on a wall calendarInstanta point on the UTC timeline — what you store and compareZonedDateTime / ZoneId.of("America/New_York")the full picture, DST includedDuration / Periodmachine time (seconds) · human time (months, days)DateTimeFormatter.ISO_LOCAL_DATE / ofPattern("dd MMM yyyy")parse and printdate.plusDays(1).withDayOfMonth(1)every type is immutable: use the result

Everything in java.time is immutable and thread-safe, which is precisely what java.util.Date, Calendar and SimpleDateFormat were not — the last of those is a famous source of production corruption when shared between threads. Treat all three as legacy; convert at the boundary with Date.toInstant().

Idioms & Gotchas

The ones everybody hits

"a" == "a" is true, but do not rely on itliterals are pooled; computed strings are not. Use equalsInteger a = 127, b = 127; a == btrue. At 128 it is false — the Integer cache covers −128..127map.get(missing)null, not an exception. Unboxing that null throws NPE0.1 + 0.2 != 0.3binary floating point. Money belongs in BigDecimal or in long centsnew BigDecimal(0.1)captures the binary error — use BigDecimal.valueOf(0.1) or the String constructorlist.remove(1) vs list.remove(Integer.valueOf(1))by index vs by value — a genuine bug factoryArrays.asList(intArray)a List<int[]> of size 1. Use Arrays.stream or a boxed arraycatch (NullPointerException e)never. Fix the nullequals without hashCodethe object vanishes from every HashMap and HashSetmutable key in a HashMapchange it and the entry is unreachable, still occupying a bucketstatic mutable statethe default global variable, and the default race condition

Write it this way

Objects.equals(a, b)null-safe equalityObjects.requireNonNull(x, "x")fail at the boundary, with a nameList<String> x = new ArrayList<>()declare the interface, instantiate the classprivate final everywhere it fitsimmutable by default; open it only when neededreturn an empty collection, never nullList.of() costs nothing and removes a null check from every callerOptional as a return typenever as a field or a parametervar only where the type is obviousvar x = compute() hides what you gotStringBuilder in loopsand String.join or Collectors.joining outside themenum over int constantstype-safe, printable, switchablerecord for data, sealed for a closed hierarchylet the compiler check exhaustivenesstry-with-resources for anything closeableand never an empty catch block

Stack traces, precisely

The last Caused by is the root cause; frames elided as … 23 more are shared with the trace above it. A trace can also lie by omission: the JIT drops frames it inlined, and after enough throws from one site HotSpot replaces the exception with a preallocated one that has no stack trace at all-XX:-OmitStackTraceInFastThrow turns that off when a production NPE arrives naked. Since 14 the helpful NullPointerException names the exact sub-expression, which is why -g (or at least -g:lines) matters in a deployed build.

The JVM at Run Time

The compiler is the smaller half of Java. Nearly everything that determines how a program actually behaves — when a class initialises, when a method gets compiled, whether an object is allocated at all — happens in the VM, at run time, and can be observed.

Loading, linking, initialising

loada ClassLoader finds the bytes; parents are asked first, so the platform wins name clashesverifythe bytecode verifier proves type safety and stack discipline before anything runspreparestatic fields get their default values, not their initialisersresolvesymbolic constant-pool references become direct ones, lazilyinitialisestatic initialisers and static field assignments run, once, thread-safely

Initialisation is triggered by first active use — new, a static method call, a non-constant static field read — and not by Class.forName(x, false, cl) or by reading a static final compile-time constant, which was inlined into the caller and does not touch the class at all. That inlining is a genuine deployment hazard: recompile the constant’s owner alone and callers keep the old value.

invokedynamic

A lambda is not an anonymous class. javac emits an invokedynamic whose bootstrap (LambdaMetafactory) spins the implementing class on first execution, so non-capturing lambdas are allocated once and reused, and there is no class file per lambda. String concatenation with + compiles the same way since 9 (StringConcatFactory) rather than to a StringBuilder chain — which is why the old “concatenation is always slow” advice now applies only inside loops.

Compilation

interpreter → C1 → C2tiered: profile cheaply, then compile hot methods with the profile~10k invocationsthe rough C2 threshold; loops trigger on-stack replacement of a running framedeoptimisationa speculation fails (a second implementation loads, a branch finally runs) and the frame falls backescape analysisa non-escaping object may be scalar-replaced and never allocated; locks on it are elided-XX:+PrintCompilationwatch it happen; JFR and async-profiler are the real tools

This is why a Java microbenchmark that does not warm up measures the interpreter, and why one that does may measure a loop the compiler deleted. Use JMH, which exists because getting this right by hand is genuinely hard.

Memory

heapall objects; generational, because most die youngmetaspaceclass metadata, off-heap and native — a leak here is a classloader leakthread stacksframes, locals, operand stack; -Xss per threadG1 (default)region-based, concurrent marking, pause target -XX:MaxGCPauseMillisZGC / Shenandoahconcurrent compaction, sub-millisecond pauses, larger footprintParallel / Serialthroughput and small heaps; still the right answer for batch work

Reachability has four strengths: strong, soft (cleared under memory pressure — a cache), weak (cleared at the next GC — WeakHashMap) and phantom (post-mortem notification, the supported replacement for finalisers, and what Cleaner uses). None of them is a destructor: nothing guarantees a reference is ever cleared, so anything scarce still needs close().

The classic production leaks are all reachability, not allocation: a static collection that only grows, a ThreadLocal never removed on a pooled thread, a listener never unregistered, an inner class pinning its outer instance, and a redeployed application whose old ClassLoader stays reachable through one stray thread.

Java by Version

Since 9, a feature release every six months, and a long-term-support release every two years — 8, 11, 17, 21, 25. Nearly all production Java sits on an LTS. A preview feature needs --enable-preview at both compile and run time and may change or vanish in the next release.

VersionYearWhat it brought
52004Generics, enums, annotations, autoboxing, for-each, varargs
72011try-with-resources, multi-catch, diamond, strings in switch, underscores in literals, NIO.2
8 LTS2014Lambdas, streams, method references, default methods, Optional, java.time
92017Modules (JPMS), jshell, List.of, private interface methods
102018var for locals
11 LTS2018Standard HttpClient, String.strip/isBlank/lines/repeat, Files.readString, single-file source launch
142020Switch expressions final, helpful NullPointerException
15–162020–21Text blocks final · records and instanceof patterns final, Stream.toList
17 LTS2021Sealed classes final, pattern matching groundwork, new pseudo-random generators
182022UTF-8 as the default charset, everywhere
21 LTS2023Virtual threads, pattern matching for switch, record patterns, sequenced collections
222024Unnamed variables and patterns (_), Foreign Function & Memory API final
23–242024–25Markdown javadoc, generational ZGC by default · stream gatherers and the Class-File API final
25 LTS2025Compact source files and instance main, module import declarations, scoped values, flexible constructor bodies

Which one am I on?

java --version · javac --versionruntime and compiler can differ — check bothSystem.getProperty("java.version")from inside the programjavac --release 17 X.javacompile against 17’s API on a newer JDK — safer than -source/-targetjava --enable-preview -jar app.jarrequired at run time too, and only on the exact version it was compiled with

Class files are forward-compatible, never backward: a JDK 21 build will not load on a JDK 17 runtime (UnsupportedClassVersionError). Compile with --release set to the oldest runtime you must support.

Index

keywords, operators and the core API — signatures read from the installed JDK

Keywords & Modifiers

72

Reserved words

abstractclass or method with no implementation here
assertassert cond : msg; — disabled unless the JVM runs with -ea
booleantrue or false; no numeric conversion in either direction
breakleave the innermost loop or switch, or a labelled one
byte8-bit signed integer, -128..127
casea switch label; several may be comma-separated since 14
catchhandle a thrown exception; multi-catch with |
char16-bit unsigned UTF-16 code unit
classdeclare a class
constreserved, never implemented — use final
continueskip to the next iteration of a loop
defaultswitch fallback label; also a method body in an interface
dodo { } while (c); — body runs at least once
double64-bit IEEE 754 floating point; the default for decimals
elsethe alternative branch of an if
enuma class with a fixed set of named instances
extendsinherit from one class, or bound a type parameter
finalassign once / cannot be overridden / cannot be extended
finallyblock that runs whatever happens in the try
float32-bit floating point; needs the f suffix on literals
forcounted loop, or the for-each over an array or Iterable
gotoreserved, never implemented — use a labelled break
ifcondition must be a boolean, not a number or reference
implementspromise to provide an interface
importrefer to a type by its simple name; static imports members
instanceofrun-time type test; binds a pattern variable since 16
int32-bit signed integer — the type all narrower arithmetic promotes to
interfacea contract; @interface declares an annotation type
long64-bit signed integer; literals take the L suffix
nativemethod implemented in another language through JNI
newallocate an object or an array
packagenamespace declaration; must be the first statement
privatevisible only inside this class
protectedthis class, its package, and subclasses anywhere
publicvisible everywhere the module and package allow
returnleave the method, optionally with a value
short16-bit signed integer, -32768..32767
staticbelongs to the class, not to an instance
strictfpstrict IEEE arithmetic — a no-op since 17, always strict now
supersuperclass constructor, member, or a wildcard lower bound
switchmulti-way branch; also an expression since 14
synchronizedtake the object monitor for the block or method
thisthe current instance; this(...) chains to another constructor
throwraise an exception object
throwsdeclare the checked exceptions a method may propagate
transientexclude a field from Java serialization
tryguard a block; try (r = ...) closes resources automatically
voidno return value
volatileevery read sees the most recent write; no reordering
whiletest-first loop

Contextual keywords

varinferred type for a local variable, only where an initialiser exists
recordtransparent immutable data carrier
sealedrestrict who may extend or implement this type
non-sealedreopen a branch of a sealed hierarchy
permitsname the subtypes a sealed type admits; omittable in a single file
yieldproduce the value of a switch expression block
whenguard on a switch pattern label
_unnamed variable or pattern — a value you deliberately ignore
moduledeclare a named module in module-info.java
requiresthis module needs another; transitive re-exports it
exportsmake a package public outside the module; to for a qualified export
opensallow deep reflection into a package at run time
uses / providesServiceLoader consumer / implementation declaration

Literals

true false nullreserved literals, not keywords
0b1010binary literal
017octal — a leading zero, and a classic bug
0x1Fhexadecimal
1_000_000underscores are ignored between digits
100L 1.5f 1.5dlong, float, double suffixes
0x1.8p3hexadecimal floating point, exponent in powers of two
'\n' '\t' '\\' '\''char escapes; also \b \f \r \0
"""..."""text block; incidental indentation is stripped

Operators

22

Arithmetic & assignment

+ - * / %int/int truncates; % takes the sign of the dividend
++ --prefix returns the new value, postfix the old
+also string concatenation when either operand is a String
= += -= *= /= %=compound forms hide an implicit narrowing cast

Comparison & logic

== !=primitives by value, references by identity — never content
< <= > >=numeric and char only
&& ||short-circuit: the right operand may not be evaluated
& | ^on booleans: no short circuit. On integers: bitwise
!boolean negation
c ? a : bternary; both arms are promoted to a common type
instanceof T ttest and bind in one step, flow-scoped

Bitwise & shift

~one's complement
<<shift left, zero fill; distance is taken mod 32 or mod 64
>>arithmetic shift right — the sign bit is copied in
>>>logical shift right, zero fill. There is no <<<
&= |= ^= <<= >>= >>>=compound bitwise assignment

Other

->lambda, and the non-falling-through switch arrow
::method reference: Type::method, obj::method, Type::new
...varargs — last parameter only, arrives as an array
@annotation
? extends superwildcards in a generic type argument
(Type) xcast; on references it is checked at run time

Object, Objects & Class

30

java.lang.Object

toString()default is Type@hex-hashcode — override it in every class you print
equals(Object)reference identity by default; override with hashCode or neither
hashCode()equal objects must return equal codes; unequal ones need not differ
getClass()the run-time Class; final, so it never lies
clone()protected and shallow; a copy constructor is almost always better
wait()release the monitor and block until notified — inside synchronized only
notify()wake one waiter; notifyAll wakes all — prefer java.util.concurrent
notifyAll()wake every thread waiting on this monitor

java.util.Objects

equals(Object,Object)null-safe equality — the correct body of most equals overrides
hash(Object...)hash of several fields at once; use with equals
hashCode(Object)null-safe: 0 for null
requireNonNull(T)fail fast at a method boundary
requireNonNull(T,String)the same, with a message naming the argument
requireNonNullElse(T,T)value, or the fallback if it is null
toString(Object,String)null-safe toString with a default
isNull(Object)exists mostly to be used as a method reference
nonNull(Object)the readable filter: stream.filter(Objects::nonNull)
checkIndex(int,int)bounds check that throws IndexOutOfBoundsException

java.lang.Class & reflection

getName()binary name — java.lang.String, or [I for an int array
getSimpleName()the name without the package
getSuperclass()null for Object, interfaces and primitives
getInterfaces()the directly implemented interfaces
isInstance(Object)the dynamic form of instanceof
isAssignableFrom(Class<?>)can a reference of this type hold one of that type
getDeclaredFields()every field declared here, private included
getDeclaredMethods()every method declared here, inherited ones excluded
getRecordComponents()the components of a record, in order
isSealed()true for a sealed class or interface
getPermittedSubclasses()what a sealed type permits
forName(String)load a class by name — throws ClassNotFoundException

String

45

Query

length()UTF-16 code units, not characters — an emoji counts 2
isEmpty()length() == 0
isBlank()empty or only whitespace
charAt(int)one code unit; StringIndexOutOfBoundsException past the end
codePointAt(int)the full code point at that index — surrogate-aware
indexOf(String)first occurrence, or -1
lastIndexOf(String)last occurrence, or -1
contains(CharSequence)substring test, no regex
startsWith(String)prefix test
endsWith(String)suffix test
equals(Object)content equality — this is the one you want, not ==
equalsIgnoreCase(String)content equality, case folded
compareTo(String)lexicographic by code unit; negative, zero or positive
compareToIgnoreCase(String)case-insensitive ordering
matches(String)whole-string regex match
hashCode()cached after the first call — one reason Strings make good keys

Transform

substring(int)from the index to the end
substring(int,int)half-open [begin, end) — end is exclusive
toUpperCase()uses the default locale — pass a Locale in a server
toLowerCase()same locale caveat
trim()strips only characters <= U+0020
strip()Unicode-aware trim; stripLeading and stripTrailing too
replace(char,char)every occurrence, no regex
replace(…)literal substring replacement
replaceAll(String,String)regex replacement — $1 refers to a capture group
replaceFirst(String,String)regex, first match only
split(String)by regex; trailing empty strings are dropped
split(String,int)limit < 0 keeps the trailing empties
concat(String)the method behind + for two strings
repeat(int)n copies
indent(int)add or remove leading spaces on every line
formatted(Object...)instance form of String.format — pairs well with text blocks
intern()the pooled instance of this content
chars()IntStream of code units
codePoints()IntStream of real code points
lines()Stream<String> split on line terminators
transform(…)apply a Function to this string — chains without nesting

Static & conversion

valueOf(Object)null-safe: returns "null" rather than throwing
valueOf(char[])a string from a char array
format(String,Object...)printf-style formatting
join(…)delimiter join over a collection
toCharArray()a fresh char[] copy
getBytes(Charset)encode; always name the charset
new String(byte[],Charset)decode bytes with a known charset
CASE_INSENSITIVE_ORDERready-made Comparator<String>

StringBuilder, Character & Regex

45

java.lang.StringBuilder

append(String)overloaded for every type; returns this, so calls chain
insert(int,String)insert at an offset
delete(int,int)remove the half-open range
deleteCharAt(int)remove one character
replace(int,int,String)swap a range for a string
reverse()in place — surrogate pairs are kept intact
setLength(int)truncate, or pad with NUL
setCharAt(int,char)overwrite one position
indexOf(String)search inside the buffer
capacity()allocated room, distinct from length()
toString()the finished immutable String

java.lang.Character

isDigit(char)0-9 and every Unicode decimal digit
isLetter(char)Unicode letter
isLetterOrDigit(char)the usual identifier test
isWhitespace(char)Unicode whitespace, tabs and newlines included
isUpperCase(char)also isLowerCase
toUpperCase(char)single character case fold
getNumericValue(char)the digit value; -1 when it is not numeric
isSurrogate(char)half of a pair — the sign that char is not a character
MIN_VALUE'\u0000' — char is unsigned, so this is zero

java.util.regex.Pattern

compile(String)compile once, reuse — patterns are immutable and thread-safe
compile(String,int)flags: CASE_INSENSITIVE, MULTILINE, DOTALL, COMMENTS
matcher(CharSequence)bind the pattern to an input; the Matcher holds the state
split(CharSequence)the reusable form of String.split
quote(String)escape a literal so no character is special
matches(String,CharSequence)one-shot whole-input match
splitAsStream(CharSequence)lazy split into a Stream<String>

java.util.regex.Matcher

matches()the whole input must match
find()next match anywhere — call it in a while loop
lookingAt()match anchored at the start, need not reach the end
group()the text of the whole match
group(int)capture group n, counted by opening parenthesis
group(String)named group from (?<name>...)
start()index of the match; end() is exclusive
replaceAll(String)substitute every match; $1 refers to a group
results()Stream<MatchResult> of every match

Regex syntax

. \d \w \sany char (not newline) · digit · word char · whitespace
\D \W \Sthe negations
[a-z] [^a-z]class · negated class
* + ? {n,m}greedy quantifiers
*? +? ??reluctant — match as little as possible
^ $ \bstart · end · word boundary
(x) (?:x) (?<n>x)capture · group without capturing · named group
(?=x) (?!x)positive · negative lookahead
\\ in Java sourceevery backslash is doubled: "\\d" is the regex \d

Math & Numbers

50

java.lang.Math

abs(int)note: abs(Integer.MIN_VALUE) is negative — it cannot be represented
max(int,int)overloaded for int, long, float, double
min(int,int)likewise
pow(double,double)double result even for integral arguments
sqrt(double)NaN for a negative argument
cbrt(double)cube root, correct for negatives
hypot(double,double)sqrt(x²+y²) without intermediate overflow
round(double)to long, half up; round(float) gives an int
floor(double)toward negative infinity; ceil toward positive
floorDiv(int,int)division that floors — floorMod is always non-negative
floorMod(int,int)the modulo you want for wrapping an index
addExact(int,int)throws ArithmeticException instead of wrapping
multiplyExact(int,int)same guard for multiplication
toIntExact(long)narrow a long, throwing if it does not fit
random()a double in [0,1) — for anything serious use Random
log(double)natural log; log10 and exp are here too
toRadians(double)degrees to radians — the trig functions take radians
PI3.141592653589793; E is here too

Wrappers

parseInt(String)to int; NumberFormatException on anything else
parseInt(String,int)with a radix from 2 to 36
valueOf(int)boxes; caches -128..127, which is why == can surprise
toBinaryString(int)unsigned binary text; toHexString and toOctalString too
toString(int,int)in any radix
compare(int,int)the safe comparison — no subtraction overflow
MAX_VALUE2147483647; MIN_VALUE is -2147483648, not its negation
bitCount(int)population count
divideUnsigned(int,int)treat both operands as unsigned

Double, Long & friends

Double.parseDouble(s)text to double
Double.compare(a,b)total order — handles NaN and -0.0 correctly
Double.isNaN(d)the only reliable test: NaN != NaN
Double.MAX_VALUElargest finite; MIN_VALUE is the smallest positive, not the most negative
Long.parseLong(s)text to long
Boolean.parseBoolean(s)true only for "true", ignoring case
Numberthe abstract base: intValue, doubleValue, longValue
Integer.MAX_VALUE + 1wraps to MIN_VALUE — guard with Math.addExact

java.math.BigDecimal

valueOf(double)the right entry point — new BigDecimal(0.1) keeps the binary error
new BigDecimal(String)exact: the scale comes from the text
add(BigDecimal)immutable, like every operation here — use the result
multiply(BigDecimal)scales add together
divide(…)plain divide throws on a non-terminating quotient
setScale(int,RoundingMode)HALF_UP is what accountants mean by rounding
compareTo(BigDecimal)compares value only; equals also compares scale, so 2.0 != 2.00
stripTrailingZeros()normalise before printing with toPlainString

java.math.BigInteger

valueOf(long)from a long; there is no literal syntax
add(BigInteger)arbitrary precision, immutable
multiply(BigInteger)no overflow, ever
mod(BigInteger)always non-negative, unlike remainder
modPow(BigInteger,BigInteger)the workhorse of textbook RSA
gcd(BigInteger)greatest common divisor
isProbablePrime(int)Miller-Rabin with the certainty you name

Arrays & System

36

java.util.Arrays

toString(int[])readable text — the array’s own toString is a type tag and a hash
deepToString(Object[])the same, recursing into nested arrays
sort(int[])dual-pivot quicksort for primitives, TimSort for objects
sort(T[],Comparator<?super T>)stable; the comparator must be consistent or it throws
binarySearch(int[],int)the array must already be sorted
fill(int[],int)set every element
copyOf(int[],int)resize into a new array, padding with the default
copyOfRange(int[],int,int)a slice, end exclusive
equals(int[],int[])element-wise; == on arrays compares references
deepEquals(Object[],Object[])element-wise through nested arrays
hashCode(int[])content hash — Object.hashCode on an array is identity
asList(T...)a fixed-size view backed by the array: set works, add throws
stream(int[])into an IntStream
setAll(int[],IntUnaryOperator)fill by index — Arrays.setAll(a, i -> i * i)
compare(int[],int[])lexicographic array ordering
mismatch(int[],int[])index of the first difference, or -1

java.lang.System

outthe standard output PrintStream: println, printf, print
errstandard error — unbuffered, so it can interleave ahead of out
instandard input, raw bytes; wrap it in a Scanner or a Reader
arraycopy(…)the fastest bulk element move; handles overlap correctly
currentTimeMillis()wall clock, and it can jump — never time anything with it
nanoTime()monotonic, for elapsed time only; the origin is arbitrary
getProperty(String)user.home, user.dir, os.name, java.version, line.separator
getenv(String)environment variable, read-only
lineSeparator()the platform newline
exit(int)terminate the JVM with a status; shutdown hooks still run
identityHashCode(Object)the hash Object would have given, whatever the class overrode

java.lang.Runtime & ProcessBuilder

Runtime.getRuntime()the singleton
availableProcessors()the sizing input for a pool
totalMemory(…)heap now, free now, and the -Xmx ceiling
addShutdownHook(Thread)run on exit; not on a kill -9
new ProcessBuilder("ls","-l")the supported way to run a program
.directory(…)working directory and environment
.inheritIO()send the child’s output to this console
.redirectErrorStream(true)merge stderr into stdout
process.waitFor()block for the exit status; onExit() gives a CompletableFuture

Exceptions

31

java.lang.Throwable

getMessage()the detail text passed to the constructor
getCause()the wrapped exception — the last one is usually the real fault
printStackTrace()to System.err; wrong in a service, use the logger
getStackTrace()the frames as an array, innermost first
addSuppressed(Throwable)what try-with-resources does when close() also throws
getSuppressed()those secondary exceptions
initCause(Throwable)set the cause when the constructor could not

Unchecked — a bug, usually

NullPointerExceptionsince 14 the message names the exact expression that was null
IllegalArgumentExceptionthe caller passed something impossible
IllegalStateExceptionright call, wrong time
IndexOutOfBoundsExceptionArrayIndexOutOfBounds and StringIndexOutOfBounds extend it
ClassCastExceptiona cast the run-time type does not support
ArithmeticExceptioninteger division by zero — floating point gives Infinity instead
NumberFormatExceptiona subclass of IllegalArgumentException; from parseInt and friends
ConcurrentModificationExceptionthe collection changed underneath an iterator
UnsupportedOperationExceptionwhat an immutable collection throws from add or remove
ArrayStoreExceptionarray covariance caught at run time
NoSuchElementExceptionfrom Iterator.next, Optional.get and Scanner

Checked — declare or handle

IOExceptionthe root of the file and network family
FileNotFoundExceptionno such file, or it is a directory, or permission was refused
InterruptedExceptionsomeone asked this thread to stop — restore the flag if you swallow it
ClassNotFoundExceptionreflection could not find the class
CloneNotSupportedExceptionclone() without Cloneable
SQLExceptionthe JDBC family
ParseException / DateTimeParseExceptionlegacy text parsing · java.time parsing (unchecked)

Error — do not catch

OutOfMemoryErrorthe heap is exhausted, or GC is thrashing
StackOverflowErrorrunaway recursion; the trace repeats
NoClassDefFoundErrorit compiled, but the class is missing at run time — a classpath fault
UnsupportedClassVersionErrorcompiled by a newer JDK than this runtime
ExceptionInInitializerErrora static initialiser threw
AssertionErrora failed assert, with -ea enabled

Collection, List, Set, Deque

38

java.util.Collection

size()no size() on Iterator, and none on a Stream
isEmpty()cheaper than size() == 0 on a ConcurrentSkipListSet
contains(Object)O(n) on a List, O(1) on a HashSet — the whole reason to pick one
add(E)false from a Set that already held it; true from a List always
remove(Object)by value, using equals
addAll(Collection<?extends E>)bulk union
removeAll(Collection<?>)set difference; retainAll is intersection
removeIf(Predicate<?super E>)the safe in-place filter — no ConcurrentModificationException
iterator()the only way to remove during traversal
stream()sequential pipeline; parallelStream() forks into the common pool
toArray(IntFunction<T[]>)toArray(String[]::new) — the modern form

java.util.List

get(int)O(1) on ArrayList, O(n) on LinkedList
set(int,E)replace at an index; returns the old element
add(int,E)insert, shifting the tail right
remove(int)by index — remove(Integer.valueOf(1)) is by value
indexOf(Object)first occurrence by equals, or -1
subList(int,int)a live view: writes pass through, and structural change invalidates it
sort(Comparator<?super E>)in place, stable; null Comparator means natural order
replaceAll(UnaryOperator<E>)map in place
of(E...)immutable, null-hostile, and rejects duplicates in Set.of
copyOf(Collection<?extends E>)immutable snapshot of any collection
getFirst()SequencedCollection; also getLast, addFirst, removeLast
reversed()a reversed view, not a copy

java.util.TreeSet & NavigableSet

first()lowest element; last() is the highest
floor(E)greatest element <= e, or null
ceiling(E)least element >= e
higher(E)strictly greater; lower is strictly less
headSet(E,boolean)a view of everything below, inclusivity your choice
subSet(E,boolean,E,boolean)a bounded live view
descendingSet()reverse-order view
pollFirst()remove and return the lowest

java.util.Deque

addFirst(E)throws when full; offerFirst returns false instead
addLast(E)the queue-tail insert
pollFirst()remove head, null when empty; removeFirst throws
peekFirst()inspect head without removing
push(E)stack discipline — equals addFirst. Use ArrayDeque, never Stack
pop()equals removeFirst; NoSuchElementException when empty
descendingIterator()tail to head

Map

42

java.util.Map

get(Object)null both for absent and for a stored null — containsKey disambiguates
getOrDefault(Object,V)the null dance, done properly
put(K,V)returns the previous value, or null
putIfAbsent(K,V)atomic on ConcurrentHashMap, plain on HashMap
computeIfAbsent(…)the multimap idiom: m.computeIfAbsent(k, x -> new ArrayList<>()).add(v)
computeIfPresent(…)update only an existing entry; null result removes it
compute(…)remap from the current value, present or not
merge(…)counting in one line: m.merge(k, 1, Integer::sum)
remove(Object)returns the old value; the two-argument form is conditional
containsKey(Object)by equals and hashCode of the key
keySet()a live view — removing from it removes the entry
values()live view, duplicates included, no removal by value
entrySet()the only efficient way to iterate both halves
forEach(…)BiConsumer over the entries
of(K,V)immutable, at most 10 pairs; ofEntries for more
entry(K,V)a standalone immutable Map.Entry

Implementations

HashMapO(1) expected. Iteration order unspecified and it does change between releases
LinkedHashMapinsertion order; the accessOrder constructor plus removeEldestEntry gives an LRU
TreeMapred-black, O(log n), NavigableMap: floorKey, ceilingKey, headMap, subMap
EnumMapan array indexed by ordinal — the fastest map that exists for enum keys
IdentityHashMapcompares with ==; for canonicalisation and cycle detection
WeakHashMapkeys held weakly — a cache that does not leak, entries vanish at GC
ConcurrentHashMaplock-striped, no null keys or values; compute and merge are atomic
Hashtable / Vector / Stacksynchronised on every method, and superseded since 1.2

java.util.Collections

sort(List<T>)delegates to List.sort; TimSort, stable, O(n log n)
reverse(List<?>)in place
shuffle(List<?>)the Fisher-Yates you would otherwise write
max(Collection<?extends T>)by natural order, or with a Comparator
unmodifiableList(…)a read-only view — the backing list can still change under it
emptyList()a shared immutable instance; return this, never null
singletonList(T)a one-element immutable list
nCopies(int,T)n references to the same object, lazily
frequency(…)how many elements equal this one
disjoint(…)true when two collections share nothing
synchronizedList(List<T>)wraps every method — you still must lock manually to iterate

java.util.Comparator

comparing(…)the key extractor form: comparing(Person::name)
comparingInt(…)no boxing for an int key
thenComparing(…)tie-breakers, chained left to right
reversed()flip an existing comparator
naturalOrder()for anything Comparable
nullsFirst(…)wrap a comparator to tolerate nulls
compare(T,T)the contract: sgn(compare(a,b)) == -sgn(compare(b,a)), and transitive

Streams & Collectors

53

java.util.stream.Stream — sources

of(T...)a stream over the arguments
empty()zero elements
iterate(T,UnaryOperator<T>)infinite; the 3-argument form takes a predicate and terminates
generate(Supplier<?extends T>)infinite from a Supplier — always follow with limit
concat(…)two streams end to end
ofNullable(T)zero or one element

Intermediate — lazy

filter(Predicate<?super T>)keep what matches; stateless
map(…)one to one
flatMap(…)one to many, flattened — the sub-streams are closed for you
mapMulti(…)push-style flatMap, cheaper when the fan-out is small
distinct()by equals; stateful, so it buffers
sorted()natural order; fully buffers, and is illegal on an infinite stream
limit(long)short-circuits — this is what tames iterate and generate
skip(long)discard a prefix
takeWhile(Predicate<?super T>)stop at the first failure; dropWhile is the complement
peek(Consumer<?super T>)debugging only — it may be elided when the result is not needed
mapToInt(…)to a primitive stream: no boxing, and sum/average appear

Terminal

forEach(Consumer<?super T>)no encounter order in parallel — forEachOrdered if that matters
toList()unmodifiable, and it accepts nulls, unlike Collectors.toUnmodifiableList
toArray(IntFunction<A[]>)String[]::new
collect(…)the general reduction
reduce(BinaryOperator<T>)Optional result; the identity form always returns a value
count()may skip the pipeline entirely when the size is known
anyMatch(Predicate<?super T>)short-circuits; allMatch and noneMatch are true for an empty stream
findFirst()encounter order; findAny is free to pick anything
min(Comparator<?super T>)Optional, by Comparator; max is the twin

java.util.stream.Collectors

toList()mutable ArrayList, unlike Stream.toList()
toSet()a HashSet — no order guarantee
toMap(…)throws IllegalStateException on a duplicate key
toMap(…)the merge function you almost always need
joining(…)delimiter, prefix, suffix
groupingBy(…)to Map<K, List<T>>; the 2-arg form takes a downstream collector
partitioningBy(…)Map<Boolean, List<T>> — both keys always present
counting()as a downstream: groupingBy(f, counting())
summingInt(…)also averagingDouble and summarizingInt
mapping(…)transform before the downstream collector
flatMapping(…)flatten before it
filtering(…)filter inside grouping, keeping empty groups
reducing(T,BinaryOperator<T>)a reduction as a downstream collector
teeing(…)feed one stream to two collectors and merge the results

java.util.Optional

of(T)throws on null — say what you mean
ofNullable(T)the boundary adapter for a legacy API
empty()the singleton absent value
isPresent()with get(), the anti-pattern; prefer map or ifPresent
isEmpty()reads better than !isPresent()
orElse(T)evaluates its argument even when a value is present
orElseGet(…)lazy — the one to use for anything expensive
orElseThrow()NoSuchElementException; the Supplier form names your own
map(…)Optional-in, Optional-out
flatMap(…)when the function itself returns an Optional
filter(Predicate<?super T>)present becomes empty when the predicate fails
ifPresentOrElse(…)the two-branch consumer
stream()zero or one element — flatMaps an Optional-valued map cleanly

Functional Interfaces

22

java.util.function — core

Supplier<T>T get() — deferred creation; the argument to orElseGet
Consumer<T>void accept(T) — andThen chains two
BiConsumer<T,U>void accept(T,U) — Map.forEach takes one
Function<T,R>R apply(T) — compose runs before, andThen after
BiFunction<T,U,R>R apply(T,U) — Map.merge and compute take one
UnaryOperator<T>Function<T,T>; List.replaceAll takes one
BinaryOperator<T>BiFunction<T,T,T>; minBy and maxBy are static here
Predicate<T>boolean test(T) — and, or, negate, Predicate.not
BiPredicate<T,U>boolean test(T,U)

Primitive specialisations

IntSupplier / IntConsumerand the Long and Double forms — no boxing
IntFunction<R>int in, object out; the toArray generator
ToIntFunction<T>object in, int out; comparingInt takes one
IntPredicateint in, boolean out
IntUnaryOperatorint to int; Arrays.setAll takes one
IntBinaryOperator(int,int) to int; IntStream.reduce takes one
ObjIntConsumer<T>(T,int); the accumulator of a primitive collect

Outside java.util.function

Runnablevoid run(), no checked exceptions — Thread and execute take one
Callable<V>V call() throws Exception — submit takes one, Future carries the throw
Comparator<T>int compare(T,T) — functional, despite all the default methods
Iterable<T>Iterator<T> iterator() — enough to be usable in a for-each
AutoCloseablevoid close() throws Exception — enough for try-with-resources
@FunctionalInterfaceoptional; makes the single-abstract-method rule a compile error

Concurrency

41

java.lang.Thread

start()schedules run() on a new thread; twice is IllegalThreadStateException
join()wait for termination, establishing a happens-before edge
sleep(long)does not release any monitor you hold
currentThread()the running Thread — the handle for interrupt()
interrupt()sets the flag, and wakes a thread blocked in sleep, wait or join
isInterrupted()read the flag; the static interrupted() reads and clears it
setDaemon(boolean)daemon threads do not keep the JVM alive; set it before start
ofVirtual()Thread.ofVirtual().start(r) — a JVM-scheduled thread
startVirtualThread(Runnable)one per task, never pooled
setUncaughtExceptionHandler(…)otherwise the stack trace goes to stderr and is lost

Executors & futures

submit(Callable<T>)returns a Future; an exception surfaces from get(), not before
invokeAll(…)run every task, block until all finish
invokeAny(…)the first successful result; the rest are cancelled
shutdown()no new tasks, finish the queue; shutdownNow interrupts
awaitTermination(…)the wait that shutdown does not do for you
close()AutoCloseable: shutdown then await — use try-with-resources

The rest of java.util.concurrent

executor.execute(Runnable)fire and forget, from Executor; an exception reaches the thread’s handler
Executors.newFixedThreadPool(…)bounded pool, unbounded queue — the queue is where memory goes
Executors.newVirtualThreadPerTaskExecutor(…)a thread per task, no pooling
Executors.newScheduledThreadPool(…)scheduleAtFixedRate, scheduleWithFixedDelay
CompletableFuture.supplyAsync(…)thenApply, thenCompose, thenCombine, exceptionally, allOf
CountDownLatch(n)one-shot gate: await() until countDown() reaches zero
Semaphore(n)acquire/release; permits, not exclusion
CyclicBarrier(n)a rendezvous that resets and can be used again
BlockingQueueput/take block; ArrayBlockingQueue is bounded, Linked... optionally
ReentrantLocklock/unlock in a finally; tryLock with a timeout, and fairness
ReentrantReadWriteLockmany readers or one writer; StampedLock adds optimistic reads
AtomicIntegerincrementAndGet, compareAndSet — a CAS loop without the loop
AtomicReferenceupdateAndGet(UnaryOperator) for lock-free state machines
LongAdderbeats AtomicLong under contention; read it once with sum()
ThreadLocalRandom.current()the Random to use in a concurrent program
TimeUnit.SECONDS.sleep(2)readable, and the unit argument every timed method takes
ForkJoinPool.commonPool()what every parallel stream shares — sized to CPUs minus one

Memory model

happens-beforethe only guarantee of visibility between threads; everything else is folklore
volatile write / readestablishes the edge, and forbids reordering across it
synchronizedexclusion plus the edge, on the same monitor object
final fieldvisible after construction, provided this did not escape the constructor
safe publicationvia a volatile field, a final field, a static initialiser or a concurrent collection
double-checked lockingcorrect only when the field is volatile — otherwise a classic data race
word tearingdoes not occur: no field or array element ever sees a half-written value
64-bit non-volatilelong and double may be read as two halves unless declared volatile

Files, IO & Time

49

java.nio.file.Files

readString(Path)the whole file, UTF-8; the default charset became UTF-8 in 18
writeString(…)CREATE and TRUNCATE_EXISTING by default
readAllLines(Path)eager List<String>; lines() is the lazy one
lines(Path)a lazy Stream — close it, so use try-with-resources
newBufferedReader(Path)UTF-8; the Charset overload for anything else
newInputStream(…)raw bytes; newOutputStream for writing
exists(Path,LinkOption...)and notExists — both can be false when access is denied
size(Path)bytes
isDirectory(…)follows symlinks unless you pass NOFOLLOW_LINKS
createDirectories(…)the whole chain, and no error when it already exists
createTempFile(…)in the system temp directory
copy(Path,Path,CopyOption...)REPLACE_EXISTING, COPY_ATTRIBUTES
move(Path,Path,CopyOption...)ATOMIC_MOVE when the filesystem supports it
deleteIfExists(Path)delete() throws NoSuchFileException instead
walk(Path,FileVisitOption...)lazy depth-first stream; close it
find(…)walk plus a BiPredicate on path and attributes
list(Path)one level only, and lazy
probeContentType(Path)MIME type by extension and content

java.nio.file.Path

of(String,String...)the modern constructor — Paths.get is the old spelling
resolve(String)append a child; an absolute argument replaces the whole path
relativize(Path)the path from this one to that one
normalize()collapse . and .. textually, without touching the disk
toAbsolutePath()against the current working directory
toRealPath(LinkOption...)resolves symlinks, and throws if it does not exist
getFileName()the last element; getParent is everything before
startsWith(Path)element-wise, not the string prefix
toFile()back to the legacy java.io.File

Streams & readers

InputStream / OutputStreambytes. transferTo(out) copies the lot in one call (9+)
Reader / Writercharacters — a stream plus a charset
BufferedReader.readLine()null at end of input; lines() gives a Stream
InputStreamReader(in, UTF_8)the bridge; always name the charset explicitly
PrintWriter / PrintStreamprint, println, printf — they swallow IOException
DataInputStreambig-endian primitives; readUTF is modified UTF-8, not UTF-8
ByteArrayOutputStreaman in-memory sink, toByteArray at the end
ObjectOutputStreamJava serialization: fragile, and a deserialisation attack surface
Scanner(System.in)convenient; slow, and mixing next() with nextLine() misfires
System.console()null when there is no terminal; readPassword does not echo

java.time

LocalDate.now() / of(y,m,d)no zone, no instant — a calendar date
LocalDateTimedate and time, still no zone; not a point on the timeline
Instant.now()UTC timeline point — what you store and compare
ZonedDateTime.of(ldt, zone)the full picture, DST transitions included
ZoneId.of("America/New_York")a region, not an offset — offsets do not know about DST
Duration.ofSeconds(n)machine time; toMillis, plus, between
Period.ofMonths(n)human time — months and days, and it is calendar-aware
ChronoUnit.DAYS.between(a,b)elapsed whole units
DateTimeFormatter.ISO_LOCAL_DATEor ofPattern("dd MMM yyyy"); immutable and thread-safe
date.plusDays(…)every type is immutable — use the result
TemporalAdjusters.lastDayOfMonth(…)and firstInMonth, next(DayOfWeek)
Date / Calendar / SimpleDateFormatmutable and not thread-safe; convert at the boundary

Tooling & JVM Flags

45

javac

-d outwhere the class files go, package directories included
-cp / --class-pathsearch path; a trailing /* means every jar in that directory
-p / --module-pathmodule path
--release Ncompile against release N’s API — safer than -source with -target
-Xlint:allevery warning; -Xlint:-serial to silence one
-Werrormake warnings fatal
-gfull debug info; -g:none strips line numbers from stack traces
-parameterskeep parameter names for reflection — frameworks want this
--enable-previewplus --release N, and N must be exactly this JDK
-proc:noneskip annotation processing

java

-cp path Mainrun a class
-jar app.jarthe jar’s Main-Class; note that -cp is then ignored
-m module/Mainrun from the module path
Main.javasource-file mode: compile in memory and run (11+)
-D key=valuea system property, readable with System.getProperty
-eaenable assertions — off by default, which is why assert is rare
-Xmx2g / -Xms512mmaximum and initial heap
-Xss1mstack size per thread — the StackOverflowError dial
-XX:+UseZGCor UseG1GC (the default), UseParallelGC, UseSerialGC
-XX:+HeapDumpOnOutOfMemoryErrorwith -XX:HeapDumpPath=/tmp
-verbose:classwhat was loaded, and from where — the classpath debugger
-XX:+PrintFlagsFinal -versionevery VM flag and its effective value
--add-opens m/pkg=ALL-UNNAMEDreopen a package for deep reflection
-agentlib:jdwp=transport=dt_socket,server=y,address=5005wait for a debugger

jar, jshell & friends

jar cfe app.jar Main -C out .create, set the entry point, from a directory
jar tf app.jarlist; xf extracts, uf updates
jar --describe-modulethe module descriptor of a modular jar
jshell --enable-previewREPL; /list /vars /edit /save /open /env /exit
javap -p -c Classprivate members and bytecode — how the compiler desugared it
javadoc -d docs srcAPI documentation
jdeps --print-module-deps app.jarthe module set to feed jlink
jlink --add-modules m --output rta trimmed runtime image, no full JDK needed
jpackage --input . --main-jar app.jara native installer
jcmd <pid> Thread.printa thread dump; jcmd -l lists the JVMs
jcmd <pid> GC.heap_infoheap state without attaching a profiler
jfr print recording.jfrread a Flight Recorder file

Annotations

@Overridecompile error when nothing is actually overridden — always write it
@Deprecatedsince and forRemoval since 9
@SuppressWarnings("unchecked")the narrowest scope you can manage
@FunctionalInterfaceenforce exactly one abstract method
@SafeVarargsyour promise not to pollute a generic varargs array
@Retention(RUNTIME)SOURCE, CLASS or RUNTIME — only RUNTIME is reflectable
@Target(METHOD)TYPE, FIELD, PARAMETER, TYPE_USE and the rest
@Repeatablethe same annotation more than once on one element
@interface Name { String value(…)declare one; value() may be given positionally