Java 8+ Modern Features
"Lambdas didn't change what Java can do. They changed how beautifully you can do it."
Java 8 was the most impactful Java release in a decade. Lambdas, Streams, and Optional transformed how Java code looks and reads — and you will write all three in interviews. When a FAANG interviewer asks you to sort a list of objects, filter a collection, or process data in a pipeline, the cleanest answer uses Java 8 features. Beyond interviews, every modern Java codebase you will maintain or build uses these daily.
📖 Before We Start — The Big Picture
Before Java 8, passing behavior around required you to write
an entire anonymous class — a full class definition just to say "sort
by length." Java 8 introduced lambdas to fix this. A
lambda is just a function without a name. Instead of writing 5 lines,
you write (a, b) -> a.length() - b.length().
Streams are a pipeline for processing collections — think of it as a conveyor belt: your data flows through a series of operations (filter, map, sort) and exits at the end as a result. The key insight is that streams are lazy — nothing runs until you demand a final result, eliminating wasted work.
Comparator and Comparable. This lecture
shows how lambdas replace the verbose anonymous class syntax you
would have used in Lecture 3. Lecture 5 (Recursion) does not use
Java 8 features directly, but the functional thinking (input →
output, no side effects) is the same mindset recursion requires.
λ Lambda Expressions
// JAVA 7: Anonymous class (verbose, but explicit) Comparator<String> c = new Comparator<String>() { @Override public int compare(String a, String b) { return a.length() - b.length(); } }; // JAVA 8: Lambda (concise) Comparator<String> c = (a, b) -> a.length() - b.length(); // JAVA 8: Method reference (most concise when applicable) Comparator<String> c = Comparator.comparingInt(String::length); // Lambda syntax forms: () -> expression // no params, expression body x -> x * 2 // one param (no parens needed) (x, y) -> x + y // two params (int x, int y) -> { return x + y; } // explicit types + block body
1.1 — Built-in Functional Interfaces
| Interface | Method Signature | Use Case | Example Lambda |
|---|---|---|---|
Predicate<T> |
boolean test(T t) |
Filter / condition check | x -> x > 0 |
Function<T,R> |
R apply(T t) |
Transform / map | s -> s.length() |
Consumer<T> |
void accept(T t) |
Side effect (print, save) | s -> System.out.println(s) |
Supplier<T> |
T get() |
Factory / lazy init | () -> new ArrayList<>() |
BiFunction<T,U,R> |
R apply(T t, U u) |
Two-input transform | (a,b) -> a + b |
UnaryOperator<T> |
T apply(T t) |
Same-type transform | s -> s.toUpperCase() |
BinaryOperator<T> |
T apply(T t1, T t2) |
Two same-type → one | (a,b) -> Math.max(a,b) |
1.2 — Method References
// 1. Static method reference Function<String,Integer> parse = Integer::parseInt; // Class::staticMethod // 2. Instance method reference (specific instance) String prefix = "Hello"; Predicate<String> starts = prefix::startsWith; // instance::method // 3. Instance method reference (arbitrary instance of class) Function<String,Integer> len = String::length; // Class::instanceMethod // 4. Constructor reference Supplier<ArrayList<String>> factory = ArrayList::new; // Class::new // Practical example — sort strings by length then alpha: list.sort(Comparator.comparingInt(String::length).thenComparing(String::compareTo));
🌊 Streams API
// Classic stream pipeline: List<String> result = employees.stream() // SOURCE .filter(e -> e.salary > 50000) // INTERMEDIATE: filter .map(Employee::getName) // INTERMEDIATE: transform .sorted() // INTERMEDIATE: sort .distinct() // INTERMEDIATE: deduplicate .limit(10) // INTERMEDIATE: take first 10 .collect(Collectors.toList()); // TERMINAL: materialise // Key insight: the filter/map/sorted DO NOTHING until collect() is called! // Lazy evaluation = no wasted work
2.1 — Intermediate Operations (lazy)
stream.filter(x -> x > 0) // keep elements matching predicate stream.map(x -> x * 2) // transform each element stream.flatMap(x -> x.stream()) // flatten nested streams (List<List<T>> → Stream<T>) stream.distinct() // remove duplicates (uses equals/hashCode) stream.sorted() // sort by natural order stream.sorted(Comparator.reverseOrder()) // sort descending stream.limit(n) // take first n elements stream.skip(n) // skip first n stream.peek(x -> log(x)) // side effect without consuming (debugging!) stream.mapToInt(String::length) // boxed → primitive stream (no autoboxing)
2.2 — Terminal Operations (eager)
stream.collect(toList()) // → List stream.collect(toSet()) // → Set stream.collect(joining(", ", "[", "]")) // → "[a, b, c]" stream.count() // → long stream.reduce(0, Integer::sum) // → single value stream.forEach(System.out::println) // side effect on each stream.findFirst() // → Optional<T> stream.anyMatch(x -> x > 0) // → boolean (short-circuits) stream.allMatch(x -> x > 0) // → boolean stream.noneMatch(x -> x < 0) // → boolean stream.min(Comparator.naturalOrder()) // → Optional<T> stream.toArray() // → Object[]
2.3 — Collectors (the power tools)
// groupingBy — partition into Map<K, List<V>> Map<String, List<Employee>> byDept = employees.stream().collect(Collectors.groupingBy(e -> e.department)); // groupingBy + counting Map<String, Long> countByDept = employees.stream().collect(Collectors.groupingBy(e -> e.dept, Collectors.counting())); // partitioningBy — Map<Boolean, List<T>> Map<Boolean, List<Employee>> seniors = employees.stream().collect(Collectors.partitioningBy(e -> e.salary > 100000)); seniors.get(true); // high earners // joining String names = employees.stream().map(e -> e.name).collect(Collectors.joining(", ")); // toMap Map<Integer, Employee> byId = employees.stream().collect(Collectors.toMap(e -> e.id, e -> e)); // summarizingInt — stats in one pass IntSummaryStatistics stats = employees.stream().collect(Collectors.summarizingInt(e -> e.salary)); stats.getAverage(); stats.getMax(); stats.getMin(); stats.getSum();
📦 Optional<T> — Null Safety
Optional<T> forces you to
acknowledge that a value might be absent, eliminating
silent NullPointerExceptions.
// Creating Optional<String> present = Optional.of("hello"); // throws NPE if null! Optional<String> maybe = Optional.ofNullable(str); // safe: null → empty Optional<String> empty = Optional.empty(); // Accessing (NEVER use get() without isPresent check!) present.isPresent() // → true present.get() // → "hello" ← DANGEROUS if empty maybe.orElse("default") // value or default maybe.orElseGet(() -> compute()) // lazy default (only called if empty) maybe.orElseThrow(() -> new RuntimeException("missing!")) // throw if empty maybe.ifPresent(val -> process(val)) // run only if present // Transforming (functional style) Optional<Integer> len = maybe.map(String::length); // transform if present maybe.filter(s -> s.length() > 5); // keep only if condition met // Common pattern: chain of Optional operations String city = findUser(id) .flatMap(User::getAddress) .map(Address::getCity) .orElse("Unknown");
🔧 Default & Static Interface Methods
interface Printable { String content(); // abstract — must implement default void print() { // default — can override System.out.println(content()); } static Printable of(String s) { // static — called on interface return () -> s; // lambda as Printable impl (SAM) } } // Why default methods? Backward compatibility! // Java 8 added forEach, stream, removeIf to Collection without breaking existing implementations // Collection.forEach() is a default method calling Iterator internally
💪 Practice Problems
📝 Assignment
15+ problems covering Streams, Optional, lambda sorting, and functional interface composition.
📄 Open Assignment →
✅ Lecture Completion Checklist
You've conquered Java 8+ — lambdas, streams, and functional interfaces. Recursion is the foundational algorithm pattern that unlocks trees, graphs, DP, and backtracking. Every FAANG interview has it. Build the mental model now.