# Streams and Lambdas

## 1. Lambdas: Anonymous Implementations of a Single-Method Interface

A lambda expression is shorthand syntax for implementing a **functional interface** — any interface with exactly one abstract method. Before Java 8, the same thing required an anonymous inner class.

```java
// pre-lambda: anonymous inner class implementing Comparator<String>
java.util.Comparator<String> byLength = new java.util.Comparator<String>() {
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
};

// lambda: same functional interface, far less ceremony
java.util.Comparator<String> byLengthLambda = (a, b) -> a.length() - b.length();
```

The compiler infers which functional interface a lambda implements from the assignment/parameter context (**target typing**) — a lambda has no type of its own in isolation.

## 2. The Core Functional Interfaces

`java.util.function` supplies the functional interfaces almost every stream operation is built on:

```java
java.util.function.Function<String, Integer> length = s -> s.length();   // T -> R
java.util.function.Predicate<String> isEmpty = s -> s.isEmpty();          // T -> boolean
java.util.function.Consumer<String> printer = s -> System.out.println(s); // T -> void
java.util.function.Supplier<String> greeting = () -> "hello";             // () -> T
java.util.function.BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; // (T,U) -> R
```

## 3. Method References: a Lambda That Just Calls One Existing Method

When a lambda's entire body is "call this one existing method," a method reference (`::`) says the same thing more directly:

```java
java.util.List<String> names = java.util.List.of("charlie", "alice", "bob");

names.forEach(s -> System.out.println(s));   // lambda
names.forEach(System.out::println);           // equivalent method reference

names.stream().map(s -> s.toUpperCase());      // lambda calling an instance method on the parameter
names.stream().map(String::toUpperCase);        // equivalent — "unbound" instance method reference
```

Four forms exist: `Type::staticMethod`, `instance::instanceMethod`, `Type::instanceMethod` (unbound — the first stream element becomes the receiver), and `Type::new` (constructor reference).

## 4. The Stream Pipeline: Source, Intermediate, Terminal

A stream pipeline always has exactly this shape: one **source**, zero or more **intermediate** operations, one **terminal** operation.

```java
java.util.List<String> names = java.util.List.of("charlie", "alice", "bob", "dave");

java.util.List<String> result = names.stream()      // source
    .filter(s -> s.length() > 3)                      // intermediate
    .map(String::toUpperCase)                          // intermediate
    .sorted()                                            // intermediate
    .collect(java.util.stream.Collectors.toList());       // terminal

// result: ["ALICE", "CHARLIE", "DAVE"]
```

A `Stream` is not a data structure — it doesn't store elements. It's a description of a computation over a source, and nothing runs until a terminal operation is invoked.

## 5. `filter`, `map`, `reduce`

```java
java.util.List<Integer> nums = java.util.List.of(1, 2, 3, 4, 5, 6);

java.util.List<Integer> evenSquares = nums.stream()
    .filter(n -> n % 2 == 0)        // keep elements matching a Predicate
    .map(n -> n * n)                 // transform each element via a Function
    .collect(java.util.stream.Collectors.toList());
// [4, 16, 36]

int sum = nums.stream()
    .reduce(0, (acc, n) -> acc + n); // fold to a single value: (identity, accumulator)
// 21
```

`reduce`'s first argument is the identity/seed value; the accumulator combines the running result with each element. An overload with no identity returns `Optional<T>`, since an empty stream has no result to produce.

## 6. `Collectors`: Turning a Stream Back Into Something Concrete

```java
java.util.List<String> names = java.util.List.of("alice", "bob", "charlie", "amy");

java.util.List<String> asList = names.stream().collect(java.util.stream.Collectors.toList());

String joined = names.stream().collect(java.util.stream.Collectors.joining(", "));
// "alice, bob, charlie, amy"

java.util.Map<Character, java.util.List<String>> byFirstLetter = names.stream()
    .collect(java.util.stream.Collectors.groupingBy(s -> s.charAt(0)));
// {a=[alice, amy], b=[bob], c=[charlie]}

long count = names.stream().filter(s -> s.length() > 3).count();
```

`groupingBy` is the workhorse for anything resembling a SQL `GROUP BY` — the classifier function (`s -> s.charAt(0)`) produces the map's keys, and every element mapping to the same key is collected into a `List` under that key by default.

## 7. Laziness of Intermediate Operations

Intermediate operations (`filter`, `map`, `sorted`, ...) are **lazy** — building the pipeline does no work. Only calling a terminal operation (`collect`, `count`, `forEach`, `reduce`, ...) actually pulls elements through the pipeline.

```java
java.util.List<String> names = java.util.List.of("alice", "bob", "charlie");

java.util.stream.Stream<String> pipeline = names.stream()
    .filter(s -> {
        System.out.println("filtering " + s); // nothing printed yet — no terminal op called
        return s.length() > 3;
    });

System.out.println("pipeline built, nothing has run yet");
long count = pipeline.count(); // NOW "filtering alice", "filtering bob", "filtering charlie" print
```

Laziness also enables **short-circuiting**: `.filter(...).findFirst()` can stop pulling elements the moment a match is found, without processing the rest of the source, because `filter` and `findFirst` are fused into a single per-element pass rather than run as separate full sweeps.

## 8. Lambda Variable Capture: Effectively Final

A lambda can reference a local variable from its enclosing scope only if that variable is **effectively final** — never reassigned after initialization, even if not literally marked `final`.

```java
int threshold = 3; // never reassigned anywhere below — effectively final

java.util.List<Integer> nums = java.util.List.of(1, 2, 3, 4, 5);
long countAbove = nums.stream().filter(n -> n > threshold).count(); // legal: threshold is effectively final

int total = 0;
nums.forEach(n -> {
    // total += n; // COMPILE ERROR: total is reassigned elsewhere, not effectively final
});
```

## Under the Hood: `invokedynamic`, Not Anonymous Classes

Lambdas are **not** compiled to anonymous inner classes, despite behaving similarly at the source level — this was a deliberate design choice for Java 8. Each lambda body is compiled into a private synthetic method on the enclosing class, and the call site that "creates" the lambda is compiled to an `invokedynamic` bytecode instruction rather than a `new AnonymousClass()` allocation.

At first use of a given lambda call site, `invokedynamic` invokes a **bootstrap method** (`LambdaMetafactory`, part of the JDK) which generates the actual implementing class **at runtime**, using low-level `MethodHandle`s pointing at the synthetic method holding the lambda body, then caches that generated class for all future calls to the same call site. This means: no `.class` file exists on disk per lambda (unlike an anonymous inner class, which does produce one), lambda class generation is deferred until the JIT/runtime actually needs it, and two syntactically identical lambdas at two different source locations become two independent generated classes, because each `invokedynamic` call site gets its own bootstrap invocation and its own cached generated class — they are never deduplicated into one.

This is also why capturing a variable in a lambda differs mechanically from an anonymous class capturing one: an anonymous class captures by copying the value into a `final` field of the generated class at construction time; a lambda's captured variables are instead passed as extra arguments to the generated implementation method by the `LambdaMetafactory`-generated glue code — functionally equivalent from a language standpoint (still requires effective finality, since the value is captured once, not re-read live), but a different mechanism than the desugaring anonymous classes use.

![A stream pipeline pulls one element at a time through filter and map only when the terminal operation asks, short-circuiting at findFirst; each lambda is an invokedynamic call site](diagrams/01-stream-pipeline.svg)

## Common Pitfalls

1. **Calling a stream's terminal operation twice** — a `Stream` can only be consumed once; calling `.collect(...)` and then `.count()` on the *same* stream instance throws `IllegalStateException: stream has already been operated upon or closed`. Build a fresh stream (e.g. call `.stream()` again on the source) for each traversal.
2. **Assuming intermediate operations run immediately** — side effects inside `.map()`/`.filter()` lambdas won't happen until a terminal operation runs the pipeline; debugging by adding a `System.out.println` inside `.map()` with no terminal op afterward prints nothing.
3. **Trying to mutate a captured local variable from inside a lambda** — violates the effectively-final rule and fails to compile; use an `AtomicInteger`/array-of-one-element workaround or, better, restructure with `reduce`/`collect` instead of external mutable state.
4. **Using `reduce` for something `collect` does better** — `reduce` is meant for combining immutable values into a new immutable result; building up a mutable collection (like a `List`) with `reduce` works but is awkward and inefficient compared to `collect`, which is designed exactly for that mutable-accumulation case.
5. **Overusing streams for simple loops** — a `for` loop over 3 elements with a single `if` is often clearer than a stream pipeline; streams pay off with multi-step transformations or when parallelism (`.parallelStream()`) is genuinely needed, not as a blanket style rule.

## Quick Reference

```java
list.stream()                        // source (or Stream.of(...), Arrays.stream(arr))
    .filter(x -> ...)                  // intermediate: keep matching (Predicate)
    .map(x -> ...)                      // intermediate: transform (Function)
    .sorted() / .sorted(comparator)      // intermediate
    .collect(Collectors.toList())         // terminal: materialize back into a List
    .collect(Collectors.joining(", "))    // terminal: join Strings
    .collect(Collectors.groupingBy(fn))   // terminal: Map<K, List<T>>
    .reduce(identity, accumulator)          // terminal: fold to one value
    .count() / .findFirst() / .anyMatch()    // terminal

Type::staticMethod   instance::instanceMethod
Type::instanceMethod (unbound)   Type::new

Lazy: intermediate ops build a plan; nothing runs until a terminal op
Effectively final: captured locals must never be reassigned after init
A Stream is single-use — build a new one per traversal
```
