Unit 1: Collections, Streams, Filters, and Lambdas
I. Functional Data Processing in Java
Java’s collections framework stores groups of objects, while lambda expressions and streams provide a declarative way to process them. Introduced mainly in Java 8 (2014), these features support concise iteration, filtering, transformation, reduction, and aggregation without manually controlling every loop.
- Collections: Interfaces such as
List,Set, andMaprepresent stored, reusable data structures. - Lambda expression: An anonymous function written as
(parameters) -> expressionor(parameters) -> { statements; }. - Functional interface: An interface with one abstract method, such as
Predicate<T>,Function<T,R>, orConsumer<T>. - Stream: A single-use sequence of elements processed through a pipeline; it does not itself store data.
- Pipeline: A source followed by zero or more intermediate operations and one terminal operation.
- Declarative style: Code states what result is required rather than explicitly describing loop control.
- Non-interference: Stream operations should not modify their source while the pipeline is executing.
- Statelessness: Lambda results should generally not depend on mutable state that changes during processing.
II. Object Construction
A. Describing the Builder pattern
The Builder pattern constructs complex objects incrementally while keeping the final object valid and readable.
- Purpose: It avoids constructors containing many positional parameters, such as
new Student("Asha", 20, "CS", true). - Structure:
- The target class usually has a private constructor.
- A static nested
Builderstores temporary field values. - Builder methods return
this, enabling method chaining. build()validates data and creates the final object.
- Immutability: Declaring target fields
private finalprevents modification after construction. - Concrete form:
class Student {
private final String name;
private final int age;
private Student(Builder b) {
name = b.name;
age = b.age;
}
static class Builder {
private String name;
private int age;
Builder name(String value) {
name = value;
return this;
}
Builder age(int value) {
age = value;
return this;
}
Student build() {
if (name == null || age < 0)
throw new IllegalStateException();
return new Student(this);
}
}
}
Student s = new Student.Builder()
.name("Asha")
.age(20)
.build();- Limitation: A builder introduces additional classes and methods, so it is unnecessary for very small objects.
III. Lambdas and Collection Processing
A. Iterating through a collection using lambda syntax
The forEach method accepts a Consumer<T> lambda and performs an action for every collection element.
- Syntax:
collection.forEach(element -> action);useselementas the lambda parameter. - Example:
List<String> names = List.of("Ana", "Ben", "Chen");
names.forEach(name -> System.out.println(name));- Method reference: The equivalent
names.forEach(System.out::println);passes the existingprintlnmethod. - Difference from a loop:
forEachhides index and iterator management; an enhancedforloop remains preferable whenbreakorcontinueis required. - Ordering:
List.forEachfollows encounter order, whereas an unordered collection such asHashSetdoes not guarantee a stable order.
B. Filtering a collection using lambda expressions
Filtering retains only elements for which a Predicate<T> lambda evaluates to true.
- Predicate form:
Predicate<Integer> positive = n -> n > 0;maps an integer to a Boolean result. - Stream filter:
List<Integer> even = List.of(1, 2, 3, 4, 5, 6)
.stream()
.filter(n -> n % 2 == 0)
.toList();- Result: The predicate accepts
2,4, and6; the original list remains unchanged. - Composition: Predicates support
and,or, andnegate, as inadult.and(active).
C. Program to implement Lambda operations
A lambda program commonly defines functional interfaces and supplies behavior without creating named implementation classes.
- Operations: The following program demonstrates calculation, testing, transformation, and consumption.
- Interface mapping:
BinaryOperator<Integer>combines two integers.Predicate<Integer>tests one integer.Function<Integer,Integer>transforms a value.Consumer<Integer>performs an action.
import java.util.function.*;
public class LambdaDemo {
public static void main(String[] args) {
BinaryOperator<Integer> add = (a, b) -> a + b;
Predicate<Integer> even = n -> n % 2 == 0;
Function<Integer, Integer> square = n -> n * n;
Consumer<Integer> print = n -> System.out.println(n);
int total = add.apply(10, 5);
if (even.test(total))
print.accept(square.apply(total));
else
print.accept(total);
}
}- Execution:
totalbecomes15; because it is odd, the program prints15.
IV. Streams and Pipelines
A. Describing the Stream interface
Stream<T> represents a sequence of elements supporting aggregate operations over values of type T.
- Creation: Streams arise from
collection.stream(),Stream.of(...), arrays, files, or generator methods. - No storage: A stream reads from a source such as
List<String>but is not another collection. - Single use: After a terminal operation, reusing the stream causes
IllegalStateException. - Internal iteration: The stream library controls element traversal, enabling optimization and parallel execution.
- Specialized streams:
IntStream,LongStream, andDoubleStreamavoid boxing primitive values.
B. Describing the types of stream operations
Stream operations are classified by whether they build or execute a pipeline.
- Intermediate operations:
filter,map,sorted,distinct, andlimitreturn another stream and are generally lazy. - Terminal operations:
collect,reduce,count,forEach, andfindFirstconsume the stream and produce a result or side effect.
- Stateful distinction:
sortedanddistinctmay need information about previously encountered elements;mapandfilterare normally stateless. - Short-circuiting:
limit,findFirst,anyMatch, and similar operations may finish without processing every element.
C. Chaining multiple methods together
Method chaining passes each operation’s returned stream directly to the next operation.
- Example:
long count = names.stream()
.filter(n -> n.length() >= 4)
.map(String::toUpperCase)
.distinct()
.count();- Data flow: Elements are filtered, converted to uppercase, deduplicated, and counted.
- Readability: Placing one operation per line makes processing stages visible.
- Requirement: The chain must end in a terminal operation such as
count()for execution to begin.
D. Defining pipelines in terms of lambdas and collections
A stream pipeline consists of a collection source, lambda-driven intermediate stages, and a terminal result.
- General form:
result = collection.stream()
.intermediateOperation(lambda)
.terminalOperation();- Source:
collection.stream()supplies elements. - Processing:
filter(x -> condition)andmap(x -> value)express pipeline logic as lambdas. - Termination:
toList(),collect(...), orreduce(...)initiates traversal and produces the result. - Constraint: Lambdas should avoid modifying the source collection because interference can produce errors or unpredictable results.
E. Describing lazy processing
Lazy processing delays intermediate operations until a terminal operation requests elements.
- No immediate work: Calling
stream.filter(predicate)creates a pipeline stage but does not yet test elements. - Element-by-element flow: An element can pass through
filterandmapbefore the next element is read. - Optimization: In
filter(...).findFirst(), processing stops as soon as the first matching element is found. - Observation:
Stream<Integer> s = List.of(1, 2, 3).stream()
.filter(n -> {
System.out.println(n);
return n > 1;
});
// Nothing printed until:
s.findFirst();- Benefit: Laziness supports large or potentially infinite streams when combined with short-circuiting operations.
V. Transformation and Ordering
A. Extracting data from an object using map
map applies a Function<T,R> to each element, transforming a stream of type T into a stream of type R.
- Extraction: For
Studentobjects,map(Student::getName)produces student names. - Example:
List<String> studentNames = students.stream()
.map(Student::getName)
.toList();- Type change: The pipeline changes from
Stream<Student>toStream<String>. - Null handling: Mapping to
nullis legal but risky; filtering nulls or usingOptionalis safer. - Related operation:
flatMaptransforms each element into a stream and then flattens nested streams.
B. Sorting a stream
The sorted operation arranges elements by natural order or a supplied Comparator.
- Natural order:
numbers.stream().sorted()orders integers in ascending order. - Custom order:
List<Student> ordered = students.stream()
.sorted(Comparator.comparingInt(Student::getAge)
.thenComparing(Student::getName))
.toList();- Comparison sequence: Students are ordered first by age and then by name when ages match.
- Descending order: Use
.reversed()on a comparator. - Cost: Sorting is stateful and generally requires all elements, with typical time complexity
O(n log n).
VI. Reduction and Parallel Execution
A. Defining reduction
Reduction combines many stream elements into one summary value by repeatedly applying an associative operation.
- Examples: Summation, multiplication, minimum, maximum, and string concatenation are reductions.
- Identity: A neutral starting value satisfies
combine(identity, x) = x;0is the identity for addition. - Associativity:
(a op b) op cmust equala op (b op c)for reliable parallel reduction. - Non-example: Subtraction is not associative because
(10 - 5) - 2differs from10 - (5 - 2).
B. Calculating a value using reduce
The reduce terminal operation repeatedly combines stream elements using a binary operator.
- Identity overload:
int sum = List.of(2, 4, 6).stream()
.reduce(0, (subtotal, n) -> subtotal + n);- Calculation: Starting with
0, the steps produce2,6, and finally12. - Method reference:
reduce(0, Integer::sum)expresses the same operation. - No-identity overload:
reduce(Integer::max)returnsOptional<Integer>because an empty stream has no maximum. - Three-argument form: Parallel reductions may specify identity, accumulator, and combiner separately.
C. Describing how to make a stream pipeline execute in parallel
A pipeline executes in parallel when created with parallelStream() or converted using parallel().
- Creation:
long total = numbers.parallelStream()
.filter(n -> n > 0)
.mapToLong(Integer::longValue)
.sum();- Execution pool: Parallel streams normally use the common
ForkJoinPool. - Suitable work: Large, splittable datasets and CPU-intensive, independent operations benefit most.
- Risks: Shared mutable state, blocking input/output, small datasets, and ordering requirements can remove performance gains.
- Ordering:
forEachmay appear unordered in parallel;forEachOrderedpreserves encounter order at additional cost.
D. Describing the process for decomposing and then merging work
Parallel streams use divide-and-conquer: divide the source, process partitions concurrently, and combine partial results.
- Decomposition: A
Spliteratordivides data into smaller partitions, such as two ranges of an array. - Local processing: Worker threads independently run the same filter, map, or accumulation logic.
- Merging: A combiner joins partial values, for example
30 + 70 = 100. - Completion: The root task returns the final merged result.
- Correctness condition: The combiner must be associative and compatible with the accumulator.
- Performance condition: Partitioning and merging overhead must be smaller than the time saved through concurrency.
VII. Optional and Result Collection
A. Describing the Optional class
Optional<T> is a container that either holds one non-null value or is empty.
- Creation: Use
Optional.of(value),Optional.ofNullable(value), orOptional.empty(). - Stream usage:
findFirst,min,max, and identity-freereducereturnOptionalbecause no result may exist. - Safe access:
String name = optionalName.orElse("Unknown");
optionalName.ifPresent(System.out::println);- Transformation:
maptransforms a present value;filterretains it only when a predicate succeeds. - Caution: Calling
get()without checking presence can throwNoSuchElementException;orElse,orElseGet, ororElseThrowis clearer.
B. Saving results to a collection using the collect method
collect performs mutable reduction by accumulating stream elements into a result container.
- List collection:
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.toList());- Other destinations:
Collectors.toSet()removes duplicates, whileCollectors.toMap(keyMapper, valueMapper)creates key-value entries. - Modern alternative:
Stream.toList()returns an unmodifiable list, whereas the mutability ofCollectors.toList()is not guaranteed by its specification. - Parallel support: A collector supplies creation, accumulation, and combination behavior for partial containers.
C. Grouping and partitioning data using the Collectors class
Collectors provides predefined reductions for classifying elements into maps.
- Grouping:
groupingByuses a classifier and can create any number of categories.
Map<String, List<Student>> byDepartment =
students.stream()
.collect(Collectors.groupingBy(Student::getDepartment));- Partitioning:
partitioningByuses a predicate and always creates Boolean categories,trueandfalse.
Map<Boolean, List<Student>> byAdultStatus =
students.stream()
.collect(Collectors.partitioningBy(s -> s.getAge() >= 18));- Downstream collector:
groupingBy(Student::getDepartment, Collectors.counting())stores the number of students per department. - Distinction: Grouping classifies by a key such as department; partitioning divides data according to one condition.
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →