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.