← All cheat sheets ·

Modern Java (8 to 21)

Language and API features by Java version, with syntax, gotchas and stream recipes.

Features by version

Version LTS Features
8 yes lambdas, functional interfaces, method refs, Stream API, Optional, default/static interface methods, java.time, CompletableFuture
9 no modules (JPMS), List/Set/Map.of, private interface methods, Optional.ifPresentOrElse/or/stream, Stream.takeWhile/dropWhile/iterate(3-arg), ProcessHandle
10 no var for locals, List.copyOf, Collectors.toUnmodifiable*, Optional.orElseThrow()
11 yes var in lambda params, String.isBlank/strip/lines/repeat, Files.readString/writeString, standard HttpClient, single-file launch java Foo.java
12-13 no switch expressions (preview), text blocks (preview), Collectors.teeing (12)
14 no switch expressions final, helpful NPE messages (on by default in 15)
15 no text blocks final
16 no records final, instanceof pattern final, Stream.toList(), Stream.mapMulti
17 yes sealed classes final, strong encapsulation of JDK internals, RandomGenerator, pattern switch (preview)
18 no UTF-8 default charset, simple web server jwebserver
19-20 no virtual threads (preview), record patterns (preview)
21 yes virtual threads, pattern matching for switch, record patterns, sequenced collections: all final; generational ZGC; string templates and unnamed patterns (preview)
  • Spring Boot 3.x needs Java 17 minimum; Hibernate 6 needs Java 11+.
  • Java 22: unnamed variables _ final; Java 25 (LTS): scoped values, flexible constructor bodies final.

var (10+)

  • Local variables only (also for and try-with-resources); not for fields, params, return types.
  • Needs an initializer: var x; and var x = null; do not compile. Diamond with var infers ArrayList<Object>.
  • Type is still static: var list = new ArrayList<String>();.
  • Lambda needs a target type: var f = () -> 1; fails.

Records (16)

public record Point(int x, int y) {
    public Point {                       // compact canonical constructor
        if (x < 0) throw new IllegalArgumentException();
    }
    static Point origin() { return new Point(0, 0); }
}
  • Implicitly final; fields private final; accessors x(), not getX(); equals/hashCode/toString generated.
  • Cannot extend a class or declare instance fields; can implement interfaces and be nested/local.
  • Shallowly immutable: a List component can still be mutated; copy in the compact constructor (List.copyOf).
  • Jackson 2.12+ and Spring Boot 3 bind records; JPA entities cannot be records (need no-arg constructor, non-final), but records work as projections/DTOs.

Sealed types (17)

public sealed interface Shape permits Circle, Square, Rect {}
public record Circle(double r) implements Shape {}
public record Square(double s) implements Shape {}
public final class Rect implements Shape { ... }
  • Subtypes must be final, sealed, or non-sealed; same module (or same package if unnamed module).
  • permits can be omitted when subclasses are in the same file.
  • Enables exhaustive switch without default; adding a subtype breaks compile in all switches (a feature).

Switch expressions (14) and pattern matching

int days = switch (month) {
    case FEB -> 28;
    case APR, JUN, SEP, NOV -> 30;
    default -> { log.debug("x"); yield 31; }
};

String d = switch (obj) {                         // Java 21
    case null -> "null";
    case Integer i when i > 10 -> "big " + i;
    case Integer i -> "int " + i;
    case String s -> s.toUpperCase();
    case Circle(double r) -> "circle " + r;       // record pattern
    default -> "other";
};
if (o instanceof String s && !s.isEmpty()) { ... }   // Java 16
  • Arrow form does not fall through; colon form (case X:) with yield still can.
  • Switch expression must be exhaustive: enums covered or default; sealed hierarchies covered.
  • Pattern dominance: more specific case must come before the general one (compile error otherwise).
  • Old switch on a null selector throws NPE unless case null exists (21).

Text blocks (15)

String sql = """
    SELECT id, name
      FROM users
     WHERE active = true
    """;
  • Opening """ must be followed by a newline; indentation stripped by the least-indented line (closing delimiter position counts).
  • Trailing spaces stripped; \ at line end joins lines; \s keeps a space; " needs no escaping.
  • "...%s...".formatted(x) (15) for interpolation.

Optional

Do Avoid
opt.map(User::name).orElse("n/a") opt.get() without check (use orElseThrow(), 10+)
orElseGet(() -> expensive()) (lazy) orElse(expensive()) (always evaluated)
opt.ifPresentOrElse(use, fallback) (9) Optional fields, params, collections elements
opt.or(() -> other) (9), opt.stream() (9), opt.flatMap Optional.of(nullable) (NPE; use ofNullable)
return type for "might be absent" serializing it, or Optional<List<T>> (return empty list)

Streams

Kind Operations
Intermediate (lazy) filter, map, flatMap, distinct, sorted, peek, limit, skip, takeWhile, dropWhile, mapMulti (16)
Terminal collect, toList (16), forEach, reduce, count, min/max, anyMatch/allMatch/noneMatch, findFirst/findAny
Primitive IntStream.range/rangeClosed, mapToInt(..).sum(), summaryStatistics(), boxed()
  • A stream is single-use; nothing runs until a terminal op; short-circuit ops (limit, findFirst, anyMatch) stop early.
  • Lambdas must be non-interfering and stateless; do not mutate the source; avoid side effects in map/filter.
  • parallel() only for large CPU-bound, associative, stateless work; uses the common ForkJoinPool.
  • Collectors.toMap throws IllegalStateException on duplicate key; pass a merge function. It also NPEs on null values.
  • Collectors.groupingBy returns HashMap; pass a map factory for ordering.

Stream recipes

var byDept   = emps.stream().collect(groupingBy(Emp::dept));
var count    = emps.stream().collect(groupingBy(Emp::dept, counting()));
var avgSal   = emps.stream().collect(groupingBy(Emp::dept, averagingDouble(Emp::salary)));
var parts    = emps.stream().collect(partitioningBy(e -> e.salary() > 100_000));
var byId     = emps.stream().collect(toMap(Emp::id, identity(), (a, b) -> a, LinkedHashMap::new));
var names    = emps.stream().map(Emp::name).collect(joining(", ", "[", "]"));
var top3     = emps.stream().sorted(comparingDouble(Emp::salary).reversed()).limit(3).toList();
var maxByDept= emps.stream().collect(groupingBy(Emp::dept, maxBy(comparingDouble(Emp::salary))));
var flat     = lists.stream().flatMap(List::stream).toList();
var freq     = Arrays.stream(text.split("\\s+")).collect(groupingBy(identity(), counting()));
var firstDup = words.stream().filter(w -> !seen.add(w)).findFirst();   // seen: Set (stateful, sequential only)
var minMax   = nums.stream().collect(teeing(minBy(naturalOrder()), maxBy(naturalOrder()), List::of));
int sum      = IntStream.rangeClosed(1, 100).sum();

(Static imports from java.util.stream.Collectors, java.util.Comparator, java.util.function.Function.)

Other API additions worth knowing

  • java.time: Instant, LocalDate, ZonedDateTime, Duration, Period; immutable, thread-safe; avoid Date/Calendar.
  • Helpful NPE messages (JEP 358): "Cannot invoke String.length() because s is null".
  • String: strip (Unicode-aware) vs trim; chars(), formatted, stripIndent, translateEscapes; Java 21 indexOf(ch, from, to), splitWithDelimiters.
  • Objects.requireNonNullElse, Objects.checkIndex; Math.clamp (21); StringBuilder.repeat (21).
  • Sequenced collections (21): list.getFirst(), list.getLast(), list.reversed(), LinkedHashMap.firstEntry()/pollFirstEntry()/sequencedKeySet().
  • Garbage collectors: G1 default since 9; ZGC production in 15; generational ZGC in 21 (-XX:+UseZGC -XX:+ZGenerational, default mode from 23); Shenandoah production in 15 (not in Oracle builds).
  • Removed: Nashorn (15), Security Manager deprecated for removal (17), finalization deprecated for removal (18); use Cleaner/try-with-resources.