Progress
🌍
Why does this topic matter?
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.

🔗
How it connects: Lecture 3 introduced 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

📌
What is a Lambda? A lambda is an anonymous function — a function without a name, class, or explicit return type declaration. Java 8 introduced them to enable functional programming patterns and eliminate boilerplate anonymous class syntax.
☕ Java · Evolution: Anonymous Class → Lambda → Method Reference
// 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

☕ Java · 4 Types of 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

🎯
Streams Pipeline: SOURCE → [intermediate ops] → TERMINAL. Streams are lazy — intermediate ops do nothing until a terminal op triggers evaluation. This means you can chain many operations with only one pass through the data.
☕ Java · Stream Pipeline
// 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)

☕ Java · All Intermediate Ops
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)

☕ Java · All Terminal Ops
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)

☕ Java · Key Collectors
// 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

⚠️
The Billion Dollar Mistake: Tony Hoare introduced null in 1965 and called it his "billion dollar mistake." Java's Optional<T> forces you to acknowledge that a value might be absent, eliminating silent NullPointerExceptions.
☕ Java · Optional Patterns
// 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

☕ Java · Default Methods (Java 8+)
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

Problem 01 · Streams
Filter Students by GPA
EasyAmazonStream filter+sort
Problem 02 · Collectors
Group Words by First Letter
MediumGooglegroupingBy
Problem 03 · Optional
Safe User Lookup Chain
MediumLinkedInOptional flatMap
Problem 04 · reduce() Terminal Op
Sum and Product Using Stream reduce()
EasyAmazonStream reduce
Problem 05 · Method References
Rewrite Lambda as Method Reference
EasyGoogleMethod Reference
Problem 06 · Multi-Key Sort
Sort Transactions by Amount then Date
MediumStripeComparator.comparing
Problem 07 · flatMap
Flatten a List of Lists with Stream
MediumAmazonflatMap

📝 Assignment

📋
Lecture 4 Assignment
15+ problems covering Streams, Optional, lambda sorting, and functional interface composition.

📄 Open Assignment →

✅ Lecture Completion Checklist

I can rewrite any anonymous Comparator as a lambda and then as a method reference
I know all 7 core functional interfaces and their method signatures
I can build a complete stream pipeline: filter → map → sorted → collect
I know the difference between intermediate (lazy) and terminal (eager) operations
I can use groupingBy, partitioningBy, and joining collectors
I understand why flatMap is needed for nested structures
I never use Optional.get() without isPresent() (or I use orElse/orElseThrow instead)
I know when to use Optional.map() vs Optional.flatMap()
I can write a custom @FunctionalInterface with a valid SAM
I understand why default methods were added to interfaces in Java 8
🧠
You're ready for Topic 5: Recursion & Backtracking
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.
← Topic 3: OOP & CollectionsTopic 5: Recursion & Backtracking →