Unit 1: Collections, Streams, Filters, and Lambdas
1. Object Construction, Immutability, and the Builder Pattern
Modern enterprise Java applications prioritize immutable, thread-safe data structures to eliminate concurrency hazards and side-effects.
Object Creation Paradigms
- Telescoping Constructor Anti-Pattern: Providing overloaded constructors with increasing parameter counts ( parameters). This leads to poor readability, error-prone parameter ordering, and difficulties in scaling optional fields.
- JavaBeans Pattern (Setters): Creating an empty object and calling mutator methods (
setName(),setAge()).- Severe Flaws: Leaves objects in an inconsistent state during multi-step construction; prevents constructing strictly immutable objects.
- The Builder Design Pattern (GoF / Joshua Bloch): Separates the construction of a complex object from its representation, allowing the same construction process to create various representations safely.
javapublic class UserProfile {
private final String username; // Required
private final String email; // Required
private final int age; // Optional
private final boolean premium; // Optional
private UserProfile(Builder builder) {
this.username = builder.username;
this.email = builder.email;
this.age = builder.age;
this.premium = builder.premium;
}
public static class Builder {
private final String username;
private final String email;
private int age = 18;
private boolean premium = false;
public Builder(String username, String email) {
this.username = username;
this.email = email;
}
public Builder age(int age) {
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
this.age = age;
return this;
}
public Builder premium(boolean premium) {
this.premium = premium;
return this;
}
public UserProfile build() {
return new UserProfile(this);
}
}
}
Immutable Collection Factories (Java 9+)
Modern Java provides convenient static factory methods returning unmodifiable, compact collection implementations:
List.of("A", "B", "C"): Returns an immutable, null-hostile list (throwsNullPointerExceptionif null is inserted,UnsupportedOperationExceptionif modified).Set.of(1, 2, 3): Rejects duplicates at instantiation time (IllegalArgumentException).Map.of("k1", "v1", "k2", "v2")andMap.ofEntries(): Immutable key-value mappings.
2. Lambdas, Closures, and Functional Interfaces
Introduced in Java 8, lambda expressions treat functionality as method arguments, enabling functional programming paradigms.
The @FunctionalInterface Annotation
A Functional Interface (Single Abstract Method - SAM interface) contains exactly one abstract method. It may contain any number of default or static methods.
- The
@FunctionalInterfaceannotation informs the compiler to enforce the single-abstract-method constraint.
Standard Functional Interfaces (java.util.function)
Java standardizes core functional interfaces to eliminate boilerplate custom SAM definitions:
| Interface | Signature | Core Method | Primary Use Case |
|---|---|---|---|
Predicate<T> | T -> boolean | boolean test(T t) | Filtering, matching, validation |
Consumer<T> | T -> void | void accept(T t) | Side-effects, logging, printing |
Supplier<T> | () -> T | T get() | Factory instantiation, lazy generation |
Function<T, R> | T -> R | R apply(T t) | Data transformation, field extraction |
UnaryOperator<T> | T -> T | T apply(T t) | In-place transformations |
BinaryOperator<T> | (T, T) -> T | T apply(T t1, T t2) | Reductions, mathematical aggregations |
BiPredicate<T, U> | (T, U) -> boolean | boolean test(T t, U u) | Two-argument conditional evaluation |
BiFunction<T, U, R> | (T, U) -> R | R apply(T t, U u) | Combining two distinct inputs |
Variable Capture and Closures
Lambdas can reference local variables declared outside the lambda body only if those variables are effectively final:
- A variable is effectively final if its value is never modified after initialization.
- Reason: Java captures local variables by value (copying), not by reference, to avoid race conditions and stack lifecycle hazards.
Method References
Method references provide shorthand syntax for lambdas that simply invoke an existing named method (:: operator):
- Static Method Reference:
ClassName::staticMethod(args) -> ClassName.staticMethod(args)(e.g.,Math::max). - Instance Method of an Arbitrary Object:
ClassName::instanceMethod(obj, args) -> obj.instanceMethod(args)(e.g.,String::toUpperCase). - Instance Method of a Specific Object:
instanceRef::instanceMethod(args) -> instanceRef.instanceMethod(args)(e.g.,System.out::println). - Constructor Reference:
ClassName::new(args) -> new ClassName(args)(e.g.,ArrayList::new).
3. The Java Streams API: Architecture and Pipelines
A Stream is a sequence of elements supporting sequential and parallel aggregate operations.
Key Distinctions: Collections vs. Streams
- Data Storage: A Collection is an in-memory data structure holding all its elements. A Stream is a computational pipeline that does not store data; it conveys elements from a source through computational stages.
- Consumption: Collections can be traversed repeatedly. A Stream is traversed only once; calling a terminal operation consumes the stream, and subsequent operations throw
IllegalStateException. - Iteration: Collections use external iteration (
for,Iterator), where client code controls traversal. Streams use internal iteration, allowing the runtime to optimize traversal, pipelining, and parallel execution.
CODE+------------------------------------------------------------------------+ | STREAM PIPELINE ANATOMY | | | | [ Data Source ] ──► List, Set, Array, I/O Channel, Generator | | │ | | ▼ | | [ Intermediate ] ──► filter(), map(), flatMap(), sorted(), distinct() | | [ Operations ] (Lazy, return Stream<T>, chained sequentially) | | │ | | ▼ | | [ Terminal ] ──► collect(), forEach(), reduce(), count(), anyMatch()| | [ Operation ] (Eager, triggers execution, produces result/void)| +------------------------------------------------------------------------+
Lazy Evaluation and Short-Circuiting
- Laziness: Intermediate operations are not executed when invoked. They merely construct a recipe (a pipeline of stages). Data processing begins only when a terminal operation is executed.
- Loop Fusion: Intermediate operations are fused together. An element traverses the entire pipeline of filters and mappers in a single pass, rather than generating intermediate collection buffers.
- Short-Circuiting Operations:
- Intermediate:
limit(n),takeWhile(predicate)truncate processing once conditions are met. - Terminal:
findFirst(),findAny(),anyMatch(),allMatch(),noneMatch()terminate traversal as soon as the result is determined without evaluating remaining elements.
- Intermediate:
4. Intermediate Operations: Filtering and Transformation
1. filter(Predicate<T>)
Evaluates a boolean condition against each stream element, retaining only matching elements.
2. map(Function<T, R>)
Transforms each element of type T into a corresponding element of type R in a ratio.
javaList<String> names = employees.stream() .map(Employee::getName) .toList();
3. flatMap(Function<T, Stream<R>>)
Flattens nested data structures ( mapping). Transforms each element into a stream of values and flattens multiple generated streams into a single unified stream:
javaList<List<String>> nested = List.of(List.of("A", "B"), List.of("C", "D"));
List<String> flat = nested.stream()
.flatMap(Collection::stream)
.toList(); // ["A", "B", "C", "D"]
4. distinct(), sorted(), peek(), skip(), limit()
distinct(): Eliminates duplicates based onequals()andhashCode(). Stateful operation.sorted(): Sorts elements according to natural order or a customComparator. Stateful operation.peek(Consumer<T>): Inspects elements as they flow through the pipeline (primarily for debugging).takeWhile(Predicate<T>)/dropWhile(Predicate<T>)(Java 9): Evaluates ordered streams up to the first non-matching element.
5. Terminal Operations and Reduction
Immutable Reductions (reduce())
The reduce() operation combines all stream elements into a single summary value through repeated application of an associative accumulator function.
- Two-Argument Form:
javaint sum = numbers.stream().reduce(0, Integer::sum); - One-Argument Form:
- Three-Argument Form (Parallel Pipelines):
Mutable Reductions (collect())
Accumulates elements into a mutable result container (such as a List, Set, or StringBuilder) using the Collector interface:
- Supplier:
() -> R(Creates a fresh result container). - Accumulator:
(R, T) -> void(Folds an element into the container). - Combiner:
(R, R) -> R(Merges two result containers in parallel execution).
Standard Collectors (java.util.stream.Collectors)
Collectors.toList()/Collectors.toSet()/Collectors.toCollection(TreeSet::new)Collectors.joining(delimiter, prefix, suffix)Collectors.groupingBy(): Groups elements by a classifier function into aMap<K, List<T>>.- Downstream Collectors:
java
// Group employees by department and count them Map<Department, Long> deptCounts = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.counting())); Collectors.partitioningBy(Predicate): Partitions elements into two groups based on a boolean condition:Map<Boolean, List<T>>.
6. The Optional<T> API
Introduced in Java 8, Optional<T> is a container object that may or may not contain a non-null value, designed to represent the absence of a value without resorting to error-prone null references.
Instantiation
Optional.of(value): Wraps a non-null value (throwsNullPointerExceptionif null).Optional.ofNullable(value): Wraps a value if non-null, or returnsOptional.empty()if null.Optional.empty(): Explicit empty container.
Modern Idiomatic Methods
isPresent()/isEmpty()(Java 11)ifPresent(Consumer<T>): Executes action if value exists.ifPresentOrElse(Consumer<T>, Runnable)(Java 9): Executes consumer if present, runnable if empty.map(Function<T, U>)/flatMap(Function<T, Optional<U>>): Functional transformations.filter(Predicate<T>): Discards value if predicate fails.- Extraction:
orElse(defaultValue): Eager fallback value evaluation.orElseGet(Supplier<T>): Lazy fallback evaluation (executes supplier only when empty).orElseThrow(): ThrowsNoSuchElementExceptionif empty.
7. Parallel Streams and ForkJoinPool
Parallel streams split data across multiple CPU threads automatically using the common ForkJoinPool.
Execution Model: Divide-and-Conquer
- Split (Decomposition): The source
Spliteratorrecursively partitions data into chunks. - Compute: Independent worker threads process sub-streams concurrently.
- Combine (Reduction): The combiner merges intermediate partial results.
Performance Hazards and Best Practices
- Shared Mutable State: Lambdas must be strictly stateless and side-effect-free. Mutating shared data causes race conditions and corrupted results.
- Data Source Splittability:
- Excellent:
ArrayList, primitive arrays,IntStream.range(random-access, known size, efficient splitting). - Poor:
LinkedList, I/O streams,Stream.iterate(sequential pointer chasing, high splitting overhead).
- Excellent:
- NQ Model: Parallelism is beneficial when , where is the element count and is computational work per element.