Java Notes
Complete guide to Java 8 Stream API — creation, intermediate operations, terminal operations, parallel streams, collectors, and real-world data processing pipelines.
What is the Stream API?
The Stream API provides a declarative way to process collections of data. Instead of telling Java *how* to iterate (for loops, iterators), you describe *what* transformations you want — the Stream handles the iteration internally.
// Imperative approach (HOW)
List<String> filtered = new ArrayList<>();
for (String name : names) {
if (name.length() > 3) {
filtered.add(name.toUpperCase());
}
}
Collections.sort(filtered);
// Declarative approach with Streams (WHAT)
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());Key Characteristics
- Not a data structure — doesn't store elements
- Lazy evaluation — intermediate operations execute only when a terminal operation is called
- Possibly unbounded — can process infinite sequences
- Consumable — can only be traversed once
- Non-interfering — doesn't modify the source
Intermediate Operations (Lazy)
These return a new Stream and are not executed until a terminal operation is invoked.
filter — Select elements matching a predicate
map — Transform each element
flatMap — Flatten nested structures
sorted — Sort elements
// Natural order
List<Integer> sorted = numbers.stream().sorted().collect(Collectors.toList());
// Custom comparator
List<String> sortedByLength = names.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
// Multiple criteria
List<Person> sortedPeople = people.stream()
.sorted(Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparingInt(Person::getAge))
.collect(Collectors.toList());
// Reverse order
List<Integer> descending = numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());distinct, limit, skip, peek
Terminal Operations (Trigger Execution)
collect — Accumulate into a collection
[Alice, Bob, Charlie]
reduce — Combine elements into a single result
// Sum
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
int sumWithIdentity = numbers.stream().reduce(0, Integer::sum);
// Max/Min
Optional<Integer> max = numbers.stream().reduce(Integer::max);
// String concatenation
String concat = words.stream().reduce("", (a, b) -> a + " " + b).trim();
// Complex reduction
record Product(String name, double price, int qty) {}
double totalValue = products.stream()
.reduce(0.0, (sum, p) -> sum + p.price() * p.qty(), Double::sum);forEach, count, anyMatch, allMatch, noneMatch
// forEach (terminal - consumes stream)
names.stream().forEach(System.out::println);
// count
long count = names.stream().filter(n -> n.length() > 3).count();
// Short-circuit matching
boolean anyLong = names.stream().anyMatch(n -> n.length() > 10);
boolean allShort = names.stream().allMatch(n -> n.length() < 20);
boolean noneEmpty = names.stream().noneMatch(String::isEmpty);
// findFirst and findAny
Optional<String> first = names.stream()
.filter(n -> n.startsWith("A"))
.findFirst();
Optional<String> any = names.parallelStream()
.filter(n -> n.startsWith("A"))
.findAny(); // May return different results in parallelParallel Streams
// Creating parallel streams
Stream<Integer> parallel = list.parallelStream();
Stream<Integer> parallel2 = list.stream().parallel();
// When to use parallel streams:
// ✅ Large datasets (> 10,000 elements)
// ✅ CPU-intensive operations (computation, not I/O)
// ✅ Stateless, non-interfering operations
// ✅ Unordered results acceptable
// ❌ Don't use when:
// - Small datasets (overhead > benefit)
// - Order matters (use forEachOrdered)
// - Operations have side effects
// - Source is LinkedList or I/O-based
// Performance example
long start = System.nanoTime();
long sum = LongStream.range(0, 100_000_000).parallel().sum();
System.out.println("Parallel: " + (System.nanoTime() - start) / 1_000_000 + "ms");
start = System.nanoTime();
sum = LongStream.range(0, 100_000_000).sum();
System.out.println("Sequential: " + (System.nanoTime() - start) / 1_000_000 + "ms");
// Thread safety with parallel streams
// ❌ NOT SAFE
List<Integer> unsafeList = new ArrayList<>();
IntStream.range(0, 1000).parallel().forEach(unsafeList::add); // ConcurrentModification!
// ✅ SAFE
List<Integer> safeList = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());Parallel: " + (System.nanoTime() - start) / 1_000_000 + "ms Sequential: " + (System.nanoTime() - start) / 1_000_000 + "ms
Advanced Patterns
Custom Collector
// Collect to ImmutableList (Guava)
Collector<String, ?, ImmutableList<String>> toImmutableList =
Collector.of(
ImmutableList::<String>builder, // supplier
ImmutableList.Builder::add, // accumulator
(b1, b2) -> b1.addAll(b2.build()), // combiner
ImmutableList.Builder::build // finisher
);
// Collect to comma-separated string with limit
public static <T> Collector<T, ?, String> joiningWithLimit(int limit, String delimiter) {
return Collectors.collectingAndThen(
Collectors.toList(),
list -> {
String joined = list.stream()
.limit(limit)
.map(Object::toString)
.collect(Collectors.joining(delimiter));
if (list.size() > limit) {
joined += delimiter + "... and " + (list.size() - limit) + " more";
}
return joined;
}
);
}Stream Pipeline for Data Processing
record Transaction(String id, String type, double amount, LocalDate date, String category) {}
public class TransactionAnalyzer {
public static void main(String[] args) {
List<Transaction> transactions = getTransactions();
// Monthly spending summary by category
Map<String, Map<String, Double>> monthlyByCategory = transactions.stream()
.filter(t -> "DEBIT".equals(t.type()))
.collect(Collectors.groupingBy(
t -> t.date().getMonth().toString(),
Collectors.groupingBy(
Transaction::category,
Collectors.summingDouble(Transaction::amount)
)
));
// Top 5 largest transactions
List<Transaction> top5 = transactions.stream()
.sorted(Comparator.comparingDouble(Transaction::amount).reversed())
.limit(5)
.collect(Collectors.toList());
// Running total
List<Double> runningTotal = new ArrayList<>();
transactions.stream()
.mapToDouble(Transaction::amount)
.reduce(0.0, (sum, amount) -> {
double newSum = sum + amount;
runningTotal.add(newSum);
return newSum;
});
}
}Interview Questions
Q1: What is the difference between intermediate and terminal operations?
Answer: Intermediate operations (filter, map, sorted) are lazy — they return a new Stream and don't process data until a terminal operation (collect, forEach, reduce) is invoked. Terminal operations trigger the pipeline execution and produce a result.
Q2: Can a stream be reused after a terminal operation?
Answer: No. Once a terminal operation is called, the stream is consumed and cannot be reused. Attempting to reuse it throws IllegalStateException. Create a new stream from the source instead.
Q3: What's the difference between map and flatMap?
Answer: map transforms each element to exactly one output element (1:1). flatMap transforms each element to zero or more elements by flattening nested structures (1:N). Use flatMap when each element maps to a collection.
Q4: When should you use parallel streams?
Answer: When processing large datasets (>10K elements) with CPU-intensive, stateless operations where order doesn't matter. Avoid for I/O-bound operations, small datasets, or operations with side effects.
Q5: What is lazy evaluation in streams?
Answer: Intermediate operations are not executed immediately. They build a pipeline that executes only when a terminal operation is called. This enables optimizations like short-circuiting (findFirst stops after finding one match).
Q6: How does reduce differ from collect?
Answer: reduce combines elements into a single immutable result through repeated application of a binary operator. collect is a mutable reduction that accumulates elements into a container (List, Map, etc.) and is more efficient for building collections.
Q7: Write a stream pipeline to find the second-highest salary.
Optional<Double> secondHighest = employees.stream()
.map(Employee::getSalary)
.distinct()
.sorted(Comparator.reverseOrder())
.skip(1)
.findFirst();Summary
In this chapter, we learned about Stream API in Java 8 in detail. Here are the key points:
- Basic introduction to the concept and why it is needed
- Syntax and structure with complete examples
- Internal working and best practices
- Real-world applications and use cases
- Common mistakes and how to avoid them
- How this topic is asked in interviews
To practice these concepts, write and run the code in your IDE and verify the output. Modify each example and experiment so that you deeply understand the concept.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Stream API in Java 8.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, features, stream, api
Related Java Master Course Topics