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 invariant — List<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