thecodex.expert · The Codex Family of Knowledge
Java

Streams and Lambdas in Java

Write data transformations as readable pipelines — filter, map, collect — without a single explicit loop.

Java 21 LTS OpenJDK Last verified:
Canonical Definition

Java Streams (java.util.stream) provide a declarative, lazy pipeline API — source, intermediate operations (filter, map, sorted), and terminal operations (collect, count, reduce) — backed by lambdas and functional interfaces from java.util.function.

One sentence

Java Streams let you express data transformations as a pipeline of operations — filter, transform, collect — without writing loops, and lambdas give you the concise anonymous functions to plug into that pipeline.

Lambdas and functional interfaces

A lambda is an anonymous function — it has parameters and a body but no name. Java lambdas implement a functional interface: any interface with exactly one abstract method.

Javalambdas.java
// Functional interface — exactly one abstract method
@FunctionalInterface
interface Transformer<T, R> {
    R transform(T input);
}

// Lambda syntax: (params) -> expression
Transformer<String, Integer> length = s -> s.length();
Transformer<Integer, Integer> square = n -> n * n;

// Multi-line lambda — use a block body
Transformer<String, String> shout = s -> {
    String upper = s.toUpperCase();
    return upper + "!";
};

// Built-in functional interfaces from java.util.function
// Predicate<T>    — T -> boolean
// Function<T,R>   — T -> R
// Consumer<T>     — T -> void
// Supplier<T>     — () -> T
// BiFunction<T,U,R> — (T,U) -> R
// UnaryOperator<T> — T -> T (extends Function<T,T>)

import java.util.function.*;

Predicate<String>      isLong    = s -> s.length() > 5;
Function<String, Integer> toLen  = String::length;     // method reference
Consumer<String>       printer   = System.out::println;
Supplier<List<String>> listMaker = ArrayList::new;

isLong.test("Hello");       // false
isLong.test("Hello World"); // true
toLen.apply("Java");        // 4
printer.accept("Hi");       // prints "Hi"

Method references

Javamethod_refs.java
// Method reference — shorthand for a lambda that just calls a method
// Four kinds:

// 1. Static method:   ClassName::staticMethod
Function<String, Integer> parse = Integer::parseInt;

// 2. Instance method on arbitrary instance:  ClassName::instanceMethod
Function<String, String>  upper = String::toUpperCase;
Function<String, Integer> len   = String::length;

// 3. Instance method on a specific instance:  instance::method
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;

// 4. Constructor:  ClassName::new
Supplier<ArrayList<String>>  newList   = ArrayList::new;
Function<Integer, int[]>     newArray  = int[]::new;

// Equivalent lambdas for comparison
// Integer::parseInt  ≡  s -> Integer.parseInt(s)
// String::toUpperCase ≡  s -> s.toUpperCase()
// prefix::concat      ≡  s -> prefix.concat(s)

Stream pipelines

Javastreams.java
import java.util.stream.*;
import java.util.*;

List<String> names = List.of("Priya", "Rahul", "Anita", "Zara", "Priya");

// A stream pipeline: source → intermediate ops → terminal op
List<String> result = names.stream()           // 1. source
    .distinct()                                 // 2. intermediate (lazy)
    .filter(n -> n.length() > 4)               // 2. intermediate (lazy)
    .map(String::toUpperCase)                  // 2. intermediate (lazy)
    .sorted()                                  // 2. intermediate (lazy)
    .collect(Collectors.toList());             // 3. terminal — executes pipeline
// ["ANITA", "PRIYA", "RAHUL"]

// Nothing runs until the terminal operation

// Common terminal operations
long count   = names.stream().filter(n -> n.startsWith("P")).count();
boolean any  = names.stream().anyMatch(n -> n.length() > 5);
boolean all  = names.stream().allMatch(n -> n.length() > 2);
Optional<String> first = names.stream().findFirst();
String joined = names.stream().collect(Collectors.joining(", "));

// reduce — combine all elements into one
int total = IntStream.rangeClosed(1, 100).sum();   // 5050
int product = Stream.of(1, 2, 3, 4, 5)
    .reduce(1, (a, b) -> a * b);   // 120
Javacollectors.java
import java.util.stream.*;
import java.util.*;

record Employee(String name, String dept, double salary) {}

List<Employee> staff = List.of(
    new Employee("Priya",  "Engineering", 85000),
    new Employee("Rahul",  "Marketing",   62000),
    new Employee("Anita",  "Engineering", 92000),
    new Employee("Zara",   "Design",      71000),
    new Employee("Deepa",  "Engineering", 78000)
);

// groupingBy — group into a Map
Map<String, List<Employee>> byDept =
    staff.stream().collect(Collectors.groupingBy(Employee::dept));

// groupingBy with downstream collector
Map<String, Double> avgSalaryByDept = staff.stream()
    .collect(Collectors.groupingBy(
        Employee::dept,
        Collectors.averagingDouble(Employee::salary)
    ));

// partitioningBy — split into true/false
Map<Boolean, List<Employee>> highEarners = staff.stream()
    .collect(Collectors.partitioningBy(e -> e.salary() > 80000));

// toMap — explicit key/value mapping
Map<String, Double> salaryMap = staff.stream()
    .collect(Collectors.toMap(
        Employee::name,
        Employee::salary,
        (existing, replacement) -> existing  // merge fn for duplicates
    ));

// statistics
DoubleSummaryStatistics stats = staff.stream()
    .collect(Collectors.summarizingDouble(Employee::salary));
System.out.println(stats.getAverage());  // mean salary
System.out.println(stats.getMax());

Parallel streams

Javaparallel.java
// parallelStream() — uses ForkJoinPool.commonPool()
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
    .boxed()
    .collect(Collectors.toList());

// Parallel — may be faster for CPU-bound, stateless operations on large data
long sumParallel = numbers.parallelStream()
    .mapToLong(Integer::longValue)
    .sum();

// CAUTION: parallel streams are not always faster
// Overhead: splitting + merging + thread synchronisation
// Good candidates: large data, CPU-bound, stateless, order-independent
// Bad candidates: small data, I/O-bound, stateful operations, ordered output needed

// flatMap — flatten nested structures
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4), List.of(5));
List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());  // [1, 2, 3, 4, 5]

// Primitive streams — avoid boxing overhead
int[]    arr    = {1, 2, 3, 4, 5};
IntStream is    = Arrays.stream(arr);
double   avg    = is.average().orElse(0.0);
IntStream range = IntStream.range(0, 10);    // 0..9
IntStream rangeC = IntStream.rangeClosed(1, 10); // 1..10
Commonly confused
Streams are lazy — nothing runs until a terminal operation. Calling .filter(), .map(), and .sorted() on a stream creates a description of the computation, not the result. The pipeline only executes when you call a terminal operation like .collect(), .count(), or .forEach(). This means a stream with no terminal operation does nothing.
A stream can only be consumed once. Once a terminal operation has been called on a stream, the stream is exhausted. Calling any method on it afterwards throws IllegalStateException. If you need to process the same data twice, call .stream() on the source collection again.
Parallel streams are not always faster. For small collections (<1000 elements), the thread-coordination overhead of parallel streams typically makes them slower than sequential. Profile first. Parallel streams also share ForkJoinPool.commonPool() — blocking operations in a parallel stream starve other parallel work in the JVM.
How this connects

Stream implementation: Spliterator and lazy evaluation

The Stream API is built on Spliterator — a splittable iterator that supports both sequential traversal and splitting into sub-tasks for parallel processing. Each intermediate operation wraps the previous stage in a ReferencePipeline object — a chain of lazily-evaluated stages. When the terminal operation fires, a "sink" chain is constructed end-to-end, and elements flow through it one at a time (for sequential streams). Short-circuit terminal operations (findFirst(), anyMatch()) stop as soon as the condition is met, consuming the minimum number of elements from the source.

Project Valhalla and value types (Java 23+)

A long-standing performance problem with streams: every element in a Stream<Integer> is a boxed Integer object on the heap, not a primitive int. This causes significant GC pressure for numeric streams. IntStream, LongStream, and DoubleStream exist to avoid boxing for the three common primitives. Project Valhalla (targeted for future Java releases) introduces value types — user-defined types with value semantics (no identity, no synchronisation, stack-allocatable) — which will eventually allow fully generic primitive-friendly streams.

Sources

1
Java SE 21 API — java.util.stream. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/package-summary.html.
2
Java SE 21 API — java.util.function. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/function/package-summary.html.
3
Java Language Specification §15.27 — Lambda Expressions. docs.oracle.com/javase/specs/jls/se21/html/jls-15.html#jls-15.27.
4
JEP 169 — Value Objects (Project Valhalla). openjdk.org/jeps/169.
Source confidence: High Last verified: Primary source: Java SE 21 Documentation · docs.oracle.com/en/java/

Sources

1
Java SE 21 — java.util.stream. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/package-summary.html.
2
Java SE 21 — java.util.function. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/function/package-summary.html.
3
JLS §15.27 — Lambda Expressions. docs.oracle.com/javase/specs/jls/se21/html/jls-15.html#jls-15.27.
4
JEP 169 — Value Objects (Valhalla). openjdk.org/jeps/169.