Java Modern FeaturesRecords · Sealed · Pattern Match · Generics · Serialization · Optional · JDK 8–21
Unnamed Classes & Patterns
Unnamed Classes (JDK 21)
No class declaration needed — top-level fields & void main() just work
Launch protocol: JVM looks for public static void main(String[] args) first, then falls back to void main()
Cannot be referenced by name — goes into the unnamed package / unnamed module
Implicitly final — no inheritance, can't implement interfaces, can't extend anything (except Object)
Default zero-param constructor auto-generated (no custom constructors possible)
No static method calls by class name (it has none), but this works
// Minimal runnable Java file — no class, no psvm
String greeting = "Hello, World!";
void main() {
    System.out.println(greeting);
}
Unnamed Variables & Patterns ( _ )
Use _ for declared-but-never-used variables — signals intentional ignore
Works in catch blocks, instanceof destructuring, switch patterns
// Old: forced to name x, y, z even if only x needed
if (obj instanceof Location(int x, int y, int z)) { ... }

// New: underscore for ignored components
if (obj instanceof Location(int x, _, _)) {
    System.out.println("X is " + x);
}

// Also valid in catch
try { ... } catch (Exception _) { ... }
Regex
Email Validation Pattern
// Basic email regex
email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");
Pattern Breakdown
PartMeaning
^Start of string
[A-Za-z0-9+_.-]+Local part — allowed chars, 1 or more
@Mandatory separator
[A-Za-z0-9.-]+Domain — allowed chars, 1 or more
$End of string
This is a basic pattern — doesn't cover all RFC 5321 edge cases.
Method References
What It Is
Shorthand for a lambda that just calls a single existing method
Syntax: ClassName::methodName or instance::methodName
Four Kinds
KindSyntaxExample
Static methodClass::staticMethodSystem.out::println
Instance (specific obj)obj::methodmyComparator::compare
Instance (arbitrary obj)Class::instanceMethodInteger::compareTo
ConstructorClass::newTreeSet::new
Usage Examples
// Static — forEach
list.forEach(System.out::println);

// Specific instance
list.stream().sorted(customComparator::compare);

// Arbitrary instance
list.stream().sorted(Integer::compareTo);

// Constructor ref
Supplier<TreeSet> s = TreeSet::new;
Functional Interfaces
What It Is
Interfaces with a single abstract method (SAM)
Annotate with @FunctionalInterface — compiler enforces exactly one SAM
Can also have any number of private (utility), static (common utility), or default (backward compat) methods
Built-in Functional Interfaces
NameAboutMethod
PredicateTakes one input & gives a Boolean outputtest
SupplierTakes no args, gives one outputget
ConsumerTakes input and performs action, returns nothingaccept
FunctionTakes one input & gives one outputapply
BiFunctionOperates on two distinct types (T, U → R)apply
BinaryOperatorOperates on a single type (T, T → T)apply
Records
What You Get for Free
Private final instance fields per component
Public accessor methods with same name as field (not getX — just x())
Canonical constructor, toString(), equals(), hashCode()
public record User(String name, int age) {}
Rules & Traits
Implicitly final — cannot be extended
Can implement interfaces, be generic, be annotated
Can be declared locally — implicitly static, so can't access enclosing method vars
Can override accessor, equals, hashCode, toString
Shallow Immutability
Fields are final, no setters — but mutable fields (e.g. Map) can still be mutated externally
Records don't do defensive copies by default → true immutability requires a compact constructor
⚠️ map.put(1,2) after passing map to record will corrupt the record's state
Constructors
Canonical: auto-generated, initializes all fields
Compact constructor: no param list — validates/transforms, JVM adds this.field = field at end
public record User(String username) {
    public User {  // compact — no () needed
        if (username == null)
            throw new IllegalArgumentException("null!");
        // this.username = username ← auto-added
    }
}
Sealed Classes
Declaration
public sealed class Shape
    permits Circle, Rectangle, Square {}
If permitted subclasses are in same file, permits clause can be omitted
All permitted classes must be in the same module and must directly extend the sealed class
Permitted Subclass Modifiers
ModifierEffect
finalCannot be extended further
sealedCan only extend to its own permits list
non-sealedOpen to any subclass — sealed can't prevent this
Sealed Interfaces & Records
Works the same: sealed interface Inter permits A, B, C {}
A record can be named in permits (records are implicitly final)
Narrowing & Casting Rules
Narrowing works between parent interface and child class even if unrelated — a new subclass could implement both
But if the class is final, it and a disjoint interface are incompatible → cast fails at compile time
Reflection
Class::permittedSubclasses()ClassDesc[]
Class::isSealed()boolean
Pattern Matching
instanceof Pattern
// Old way
if (s instanceof Rectangle) {
    Rectangle r = (Rectangle) s;
    return 2 * r.length() + 2 * r.width();
}

// New — binding var in instanceof
if (s instanceof Rectangle r) {
    return 2 * r.length() + 2 * r.width();
}
Switch Expression (type matching)
return switch (obj) {
    case Integer i  -> String.format("int %d", i);
    case Long l     -> String.format("long %d", l);
    case Double d   -> String.format("double %f", d);
    case String s   -> String.format("String %s", s);
    case null       -> "It is null";
    default         -> obj.toString();
};
Guarded Patterns (when clause)
switch (obj) {
    case String s when s.length() == 1
               -> System.out.println("Short: " + s);
    case String s -> System.out.println(s);
    default       -> System.out.println("Not a string");
}
Switch vs if-else (Shape Example)
// Verbose if-else chain
if (s instanceof Rectangle r)
    return 2*r.length() + 2*r.width();
else if (s instanceof Circle c)
    return 2*c.radius()*Math.PI;
else throw new IllegalArgumentException(...);

// Clean switch expression
return switch (s) {
    case Rectangle r -> 2*r.length() + 2*r.width();
    case Circle c    -> 2*c.radius()*Math.PI;
    default          -> throw new IllegalArgumentException(...);
};
Rules & Gotchas
Labels are tested in order — broader patterns must come after narrower ones
Compiler errors if a label can never match (dominated by a prior label)
Switch with pattern/null labels must be exhaustive — use default to cover the rest
Exhaustive at compile-time but not at runtime → MatchException thrown
null case can only be combined with default: case null, default -> ...
No null case + null value → NullPointerException as usual
Can use enum constants as case labels for enum-typed switch vars
null Handling
switch (obj) {
    case String s       -> println("String: " + s);
    case null, default  -> println("null or other");
}
Generics & Wildcards
Basics
Type params convention: T type · E element · K key · V value · N number
Generics are invariantList<String> is NOT a subtype of List<Object>
Cannot create array of generic type: new T[n] illegal. Can make array of wildcard.
Static members can't use type params — statics are shared across all instantiations
// Generic class
class Test<T, I> { T obj; I obj2; }

// Generic method
class Abc { <T> void func(T elem) { ... } }
Bounded Types
<T extends Number> — T must be Number or subclass (resolved at compile time)
Multiple bounds: one class + multiple interfaces → <T extends Number & Comparable<T>>
No T super Number — type erasure resolves to upper bound at compile time; upper bound would be Object (pointless)
Variance
VarianceMeaningJava
InvariantNo subtype relationPlain generics List<T>
CovariantSubtype acceptedArrays, ? extends T
ContravariantSupertype accepted? super T
⚠️ Arrays covariant at runtime → possible ArrayStoreException. Generics invariant → safer at compile time.
PECS Rule
Producer → Extends
Reading from collection
List<? extends T>
get ✓ · add ✗ (except null)
Consumer → Super
Writing to collection
List<? super T>
add ✓ · get returns Object
Wildcards
WildcardSyntaxUse when
UnboundedList<?>Type irrelevant to logic
Upper boundedList<? extends N>Read-only, any subtype of N
Lower boundedList<? super I>Write ops, any supertype of I
Can't use ? as a method return type — unknown type is unusable by caller
Unlike T, multiple params can each be a different unknown type with wildcards
Type Erasure
Compiler replaces generics with Object (or upper bound) in bytecode
Exists for backward compat with Java 1.4 — JVM never knew generics
Cannot do instanceof List<String> — at runtime it's just List
Cannot overload with List<String> vs List<Integer> — same signature after erasure → compile error
Bridge methods: compiler auto-generates synthetic methods in subclasses to preserve correct overriding semantics after erasure
Raw types (List list = new ArrayList()) disable compile-time checks — avoid
⚠️ Raw type + add: list.add(10) compiles but is unsafe at runtime
When to Use Generic vs Wildcard
UseWhen
<T> genericNeed type consistency, adding to collection, specific return type
? wildcardOnly reading, type doesn't need to be named/reused
Tricky Overriding Edge Case
Parent: method(List<String>) · Child: method(List<Integer>)
Overriding check happens before erasure → not treated as override
Child inherits parent's method → overloading checked after erasure → both become method(List) → compile error
Serialization
Setup
Implement Serializable (marker interface — no methods)
Declare private static final long serialVersionUID = 1L; — used to detect class version mismatch on deserialization
Serializing
ObjectOutputStream oos = new ObjectOutputStream(
    new FileOutputStream("data.bin"));
oos.writeObject(myObj);
JVM collects: class name, serialVersionUID, field metadata + values → binary
static fields not serialized (belong to class, not instance)
Deserializing
ObjectInputStream ois = new ObjectInputStream(
    new FileInputStream("data.bin"));
MyClass obj = (MyClass) ois.readObject();
JVM reads metadata → finds class in classpath → allocates object without calling constructor → restores fields
transient fields restored to default values (0, null, false)
Key Rules
transientField skipped during serialization
staticNot serialized — gets assigned value on class load
UUID mismatchDeserialization fails with InvalidClassException
No UUID declaredAuto-generated from class structure — any change breaks deserialization
Custom Serialization
private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject(); // write normal fields
    // write custom data...
}
private void readObject(ObjectInputStream in) throws ... {
    in.defaultReadObject();
    // restore custom data...
}
⚠️ Modern Java avoids built-in serialization: security vulnerabilities, perf overhead, tight coupling. Prefer Jackson (JSON) or Apache Avro.
Optional
What & Why
A container that may or may not hold a non-null value — explicit signal that absence is possible
Designed as a method return type only — not for fields, parameters, or collections
Eliminates null checks and makes absence part of the API contract
Creation
// Empty optional
Optional<String> empty = Optional.empty();

// From a non-null value (throws NPE if null)
Optional<String> present = Optional.of("hello");

// From a possibly-null value — safe
Optional<String> maybe = Optional.ofNullable(getValue());
Retrieving the Value
// get() — throws NoSuchElementException if empty; avoid raw get()
String val = opt.get();

// orElse — always evaluates the fallback expression
String val = opt.orElse("default");

// orElseGet — lazy; fallback only called if empty (prefer this)
String val = opt.orElseGet(() -> computeDefault());

// orElseThrow — throws if empty; no-arg = NoSuchElementException
String val = opt.orElseThrow();
String val = opt.orElseThrow(() -> new IllegalStateException("missing"));

// or — returns another Optional if empty (Java 9+)
Optional<String> result = opt.or(() -> Optional.of("fallback"));
Checking Presence
// isPresent / isEmpty (Java 11+)
if (opt.isPresent()) { ... }
if (opt.isEmpty())   { ... }

// ifPresent — runs action only if value exists
opt.ifPresent(System.out::println);

// ifPresentOrElse (Java 9+) — branch on presence
opt.ifPresentOrElse(
    val  -> System.out.println("Got: " + val),
    ()   -> System.out.println("Empty")
);
Transforming the Value
// map — transforms value if present, returns Optional of result
Optional<Integer> len = opt.map(String::length);

// flatMap — use when mapper itself returns an Optional (avoids Optional<Optional<T>>)
Optional<String> city = userOpt.flatMap(u -> u.getAddress())
                                .flatMap(a -> a.getCity());

// filter — keeps value only if predicate matches, else empty
Optional<String> long_ = opt.filter(s -> s.length() > 5);
Method Quick Reference
MethodReturnsNotes
of(v)Optional<T>NPE if v is null
ofNullable(v)Optional<T>Safe — empty if null
empty()Optional<T>Singleton empty
get()TThrows if empty — avoid
orElse(v)TAlways evaluates v
orElseGet(s)TLazy — prefer over orElse
orElseThrow()TThrows if empty
or(s)Optional<T>Java 9+ — fallback Optional
map(f)Optional<R>Transform if present
flatMap(f)Optional<R>f returns Optional — no nesting
filter(p)Optional<T>Empty if predicate false
isPresent()booleanTrue if value present
isEmpty()booleanJava 11+ — inverse of isPresent
ifPresent(c)voidRun consumer if present
ifPresentOrElse(c,r)voidJava 9+ — branch on presence
stream()Stream<T>Java 9+ — 0 or 1 element
Stream Integration (Java 9+)
// stream() (Java 9+) — 0 or 1 element stream; great for flatMap
List<String> names = optionals.stream()
    .flatMap(Optional::stream)   // unwrap all present values
    .toList();
Anti-patterns & Best Practices
// ❌ Antipattern — just use if-null check instead
if (opt.isPresent()) {
    return opt.get();
} else {
    return "default";
}

// ✅ Use orElse / orElseGet
return opt.orElse("default");

// ❌ Never use Optional as a field or method parameter
// — it's designed as a return type only
class User { Optional<String> nickname; } // bad

// ❌ Don't wrap primitives — use OptionalInt, OptionalLong, OptionalDouble
Optional<Integer> count = ...; // allocates — use OptionalInt instead
Use OptionalInt, OptionalLong, OptionalDouble for primitives — avoids boxing overhead.
Java 17 / 21 Highlights
Java 17
Sealed classes (final) JDK 17
Pattern matching for switch (preview) Preview
JEP 356: new RandomGenerator interface + impls (SplittableRandom etc.)
static String formatter(Object obj) {
    return switch (obj) {
        case String s  -> "String: " + s;
        case Integer i -> "Integer: " + i;
        case null      -> "null";
        default        -> "Unknown";
    };
}
Java 21
Virtual threads (Project Loom) Final
Pattern matching for switch Final
Record patterns Final
Unnamed classes & void main() Preview
Unnamed variables _ Preview
SequencedCollection / SequencedMap interfaces added — uniform API for ordered collections (addFirst, getLast, reversed())
String templates Preview
Quick Version Map
FeatureVersion
Lambdas, Streams, OptionalJava 8
var (local type inference)Java 10
Records, Sealed (preview)Java 14/15
Pattern instanceofJava 16
Records, Sealed (final)Java 16/17
Switch pattern match (final)Java 21
Virtual threads (final)Java 21
SequencedCollectionJava 21
JAVA MODERN FEATURES · JDK 8 – 21 · QUICK REFERENCE