Java Streams
Stream creation Β· filter Β· map Β· flatMap Β· reduce Β· collect Β· sorted Β· distinct Β· Optional Β· parallel
Sheet 3 of 7
Java 8+
Intermediate
Printable
Stream Pipeline β How It Works
Creating a Stream
import java.util.stream.Stream; // From a Collection List<String> list = List.of("a","b","c"); Stream<String> s1 = list.stream(); // From varargs Stream<Integer> s2 = Stream.of(1, 2, 3); // From array String[] arr = {"x", "y"}; Stream<String> s3 = Arrays.stream(arr); // Primitive streams IntStream is = IntStream.range(0, 10); LongStream ls = LongStream.of(1L, 2L); DoubleStream ds = DoubleStream.of(1.5, 2.5);
Pipeline Anatomy
// Source β Intermediate(s) β Terminal List.of(5,3,8,1,9,2) .stream() // source .filter(n -> n > 3) // intermediate .sorted() // intermediate .map(n -> n * 2) // intermediate .forEach(System.out::println); // β 10 16 18 // Streams are LAZY β intermediate ops // run only when terminal op is called
Intermediate vs Terminal
// INTERMEDIATE β return a Stream filter(predicate) map(function) flatMap(function) sorted() / sorted(comparator) distinct() limit(n) / skip(n) peek(consumer) // TERMINAL β end the pipeline forEach() / collect() reduce() / count() findFirst() / findAny() anyMatch() / allMatch() min() / max() / toList()
Streams are single-use: Once a terminal operation is called, the stream is consumed and cannot be reused. Create a new stream from the source each time.
filter()
Basic Filter
List<Integer> nums = List.of(1,2,3,4,5,6); // Keep only even numbers List<Integer> evens = nums.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); // [2, 4, 6] // Chain multiple filters nums.stream() .filter(n -> n > 2) .filter(n -> n % 2 == 0) .forEach(System.out::println); // 4 6
Filter on Objects
List<String> names = List.of("Alice","Bob","Anna","Carol"); // Starts with "A" names.stream() .filter(s -> s.startsWith("A")) .forEach(System.out::println); // Alice Anna // Negate a predicate Predicate<String> startsA = s -> s.startsWith("A"); names.stream() .filter(startsA.negate()) // Bob, Carol .forEach(System.out::println);
map()
Transform Elements
List<String> names = List.of("alice", "bob", "carol"); // String β String names.stream() .map(String::toUpperCase) .forEach(System.out::println); // ALICE BOB CAROL // String β Integer List<Integer> lengths = names.stream() .map(String::length) .collect(Collectors.toList()); // [5, 3, 5]
mapToInt / mapToLong / mapToDouble
// Use primitive streams for performance List<String> words = List.of("hello", "world", "java"); int totalLen = words.stream() .mapToInt(String::length) .sum(); // 15 double avg = words.stream() .mapToInt(String::length) .average() .orElse(0); // 5.0
flatMap()
map vs flatMap
List<List<Integer>> nested = List.of( List.of(1,2), List.of(3,4), List.of(5,6)); // map β Stream<Stream<Integer>> β nested.stream() .map(List::stream); // nested! // flatMap β Stream<Integer> β nested.stream() .flatMap(List::stream) .forEach(System.out::print); // 1 2 3 4 5 6
Split strings into words
List<String> sentences = List.of( "Hello World", "Java Streams"); List<String> words = sentences.stream() .flatMap(s -> Arrays.stream( s.split(" "))) .collect(Collectors.toList()); // ["Hello","World","Java","Streams"]
flatMapToInt β sum of all sublists
List<List<Integer>> groups = List.of( List.of(10,20), List.of(30,40)); int total = groups.stream() .flatMapToInt( l -> l.stream() .mapToInt(Integer::intValue)) .sum(); // 100
Rule of thumb: Use
flatMap whenever your mapping function returns a Stream, Collection, or array and you want a single flat stream instead of nested streams.reduce()
reduce() β 3 forms
List<Integer> nums = List.of(1,2,3,4,5); // 1. With identity (no Optional) int sum = nums.stream() .reduce(0, Integer::sum); // 15 // 2. Without identity β Optional Optional<Integer> max = nums.stream() .reduce(Integer::max); // Optional[5] // Lambda form int product = nums.stream() .reduce(1, (a, b) -> a * b); // 120
Prefer IntStream for numeric ops
// More efficient than reduce for numbers IntStream.rangeClosed(1, 10) .sum(); // 55 IntStream.of(3,1,4,1,5) .average() // OptionalDouble[2.8] .orElse(0); IntStream.of(3,1,4,1,5) .summaryStatistics(); // count=5, sum=14, min=1, max=5, avg=2.8
collect() & Collectors
Collect to Collections
import java.util.stream.Collectors; Stream<String> s = Stream.of("a","b","a","c"); // β List List<String> list = s.collect( Collectors.toList()); // β unmodifiable List (Java 16+) List<String> imm = s.toList(); // β Set (removes duplicates) Set<String> set = s.collect( Collectors.toSet()); // β joining strings String joined = s.collect( Collectors.joining(", ","[","]")); // [a, b, a, c]
groupingBy & partitioningBy
List<String> names = List.of("Alice","Bob","Anna","Carol"); // Group by first letter Map<Character,List<String>> byLetter = names.stream() .collect(Collectors.groupingBy( s -> s.charAt(0))); // {A=[Alice,Anna], B=[Bob], C=[Carol]} // Partition into true/false Map<Boolean,List<String>> parts = names.stream() .collect(Collectors.partitioningBy( s -> s.length() > 3)); // {false=[Bob], true=[Alice,Anna,Carol]}
counting, mapping, toMap
// Count per group Map<Character,Long> counts = names.stream() .collect(Collectors.groupingBy( s -> s.charAt(0), Collectors.counting())); // name β length map Map<String,Integer> nameLens = names.stream() .collect(Collectors.toMap( s -> s, String::length)); // {Alice=5, Bob=3, Anna=4, Carol=5}
sorted Β· distinct Β· limit Β· skip
sorted()
List<Integer> nums = List.of(5,3,8,1); // Natural order (ascending) nums.stream().sorted() .toList(); // [1,3,5,8] // Descending nums.stream() .sorted(Comparator.reverseOrder()) .toList(); // [8,5,3,1] // Sort strings by length List.of("banana","fig","apple") .stream() .sorted(Comparator.comparingInt( String::length)) .toList(); // [fig, apple, banana]
distinct Β· limit Β· skip
List<Integer> data = List.of(1,2,2,3,3,4,5); // Remove duplicates data.stream().distinct().toList(); // [1, 2, 3, 4, 5] // First 3 elements data.stream().limit(3).toList(); // [1, 2, 2] // Skip first 2, take next 3 data.stream().skip(2).limit(3) .toList(); // [2, 3, 3] // Pagination pattern int page = 1, size = 10; data.stream().skip((long) page * size) .limit(size);
Matching & Finding
anyMatch Β· allMatch Β· noneMatch
List<Integer> nums = List.of(1,2,3,4,5); // Is any element > 4? nums.stream().anyMatch(n -> n > 4); // true // Are all elements > 0? nums.stream().allMatch(n -> n > 0); // true // Are none negative? nums.stream().noneMatch(n -> n < 0); // true // Count matching long count = nums.stream() .filter(n -> n % 2 == 0) .count(); // 2
findFirst Β· findAny Β· min Β· max
// findFirst β returns Optional Optional<Integer> first = nums.stream() .filter(n -> n > 3) .findFirst(); // Optional[4] // findAny β better for parallel Optional<Integer> any = nums.stream() .filter(n -> n > 3) .findAny(); // Optional[4] // min / max Optional<Integer> min = nums.stream() .min(Integer::compareTo); // Optional[1] Optional<Integer> max = nums.stream() .max(Integer::compareTo); // Optional[5]
Optional<T>
Creating an Optional
// Wrap a value (nullable) Optional<String> opt1 = Optional.of("hello"); // May be null β use ofNullable String val = null; Optional<String> opt2 = Optional.ofNullable(val); // empty // Explicitly empty Optional<String> empty = Optional.empty();
Checking & Extracting
Optional<String> opt = Optional.of("hello"); opt.isPresent(); // true opt.isEmpty(); // false (Java 11+) opt.get(); // "hello" (throws if empty) // Safe extraction β prefer these opt.orElse("default"); // value or default opt.orElseGet(() -> computeDefault()); opt.orElseThrow(() -> new RuntimeException("missing"));
Optional in pipelines
Optional<String> name = Optional.of(" alice "); // map β transform if present name.map(String::trim) .map(String::toUpperCase) .orElse("unknown"); // "ALICE" // filter β keep if condition met name.filter(s -> s.length() > 3) .orElse("short"); // ifPresent β execute if value exists name.ifPresent(System.out::println);
Never call opt.get() directly without checking
isPresent() first β it throws NoSuchElementException on empty. Prefer orElse(), orElseGet(), or orElseThrow() always.Parallel Streams
Enable & Use Parallel Streams
// Convert to parallel List.of(1,2,3,4,5) .parallelStream() .filter(n -> n % 2 == 0) .forEach(System.out::println); // Or convert mid-pipeline List.of(1,2,3) .stream() .parallel() // enables parallel .map(n -> n * 2) .sequential() // back to sequential .toList();
peek() β debug intermediate steps
List.of(1,2,3,4,5) .stream() .filter(n -> n > 2) .peek(n -> System.out.println("after filter: " + n)) .map(n -> n * 10) .peek(n -> System.out.println("after map: " + n)) .toList(); // Useful for debugging β remove in prod
Parallel caution: Parallel streams use the common ForkJoinPool. They help for large datasets with CPU-intensive, stateless operations. Avoid for I/O, small collections, or when order matters β the overhead can make things slower.
Method References
4 Types of Method References
// 1. Static method // Class::staticMethod Stream.of("1","2","3") .map(Integer::parseInt) // s -> Integer.parseInt(s) .toList(); // 2. Instance method on parameter // Class::instanceMethod Stream.of("a","b") .map(String::toUpperCase) // s -> s.toUpperCase() .toList(); // 3. Instance method on specific object String prefix = "Hello"; Stream.of("World") .map(prefix::concat) // s -> prefix.concat(s) .toList(); // 4. Constructor // Class::new Stream.of("Alice","Bob") .map(StringBuilder::new) // s -> new StringBuilder(s) .toList();
Use method references whenever a lambda just calls a single method β they're more readable and slightly faster. If the lambda has logic (
n -> n * 2 + 1), keep the lambda.Stream Operations β Quick Reference
| Operation | Type | Input | Returns | Example |
|---|---|---|---|---|
| filter() | Intermediate | Predicate<T> | Stream<T> | .filter(n -> n > 0) |
| map() | Intermediate | Function<T,R> | Stream<R> | .map(String::toUpperCase) |
| flatMap() | Intermediate | Function<T,Stream<R>> | Stream<R> | .flatMap(List::stream) |
| sorted() | Intermediate | Comparator (opt) | Stream<T> | .sorted(Comparator.reverseOrder()) |
| distinct() | Intermediate | β | Stream<T> | .distinct() |
| limit(n) | Intermediate | long | Stream<T> | .limit(10) |
| skip(n) | Intermediate | long | Stream<T> | .skip(5) |
| peek() | Intermediate | Consumer<T> | Stream<T> | .peek(System.out::println) |
| forEach() | Terminal | Consumer<T> | void | .forEach(System.out::println) |
| collect() | Terminal | Collector | R | .collect(Collectors.toList()) |
| toList() | Terminal | β | List<T> | .toList() (Java 16+) |
| reduce() | Terminal | BinaryOperator | Optional<T> / T | .reduce(0, Integer::sum) |
| count() | Terminal | β | long | .count() |
| findFirst() | Terminal | β | Optional<T> | .findFirst() |
| anyMatch() | Terminal | Predicate<T> | boolean | .anyMatch(n -> n > 0) |
| allMatch() | Terminal | Predicate<T> | boolean | .allMatch(n -> n > 0) |
| min() / max() | Terminal | Comparator | Optional<T> | .min(Integer::compareTo) |
Streams Mastery Checklist
| Pipeline Skills | Key point |
|---|---|
| Create a stream from List / array | .stream() / Arrays.stream() |
| Explain lazy evaluation | runs only at terminal op |
| Know intermediate vs terminal | returns Stream vs value |
| Avoid reusing a consumed stream | create new stream each time |
| Transform & Collect | Key point |
|---|---|
| filter + map + collect | core pipeline pattern |
| flatMap nested collections | Function β Stream<R> |
| groupingBy / partitioningBy | Map<K, List<V>> |
| joining strings with delimiter | Collectors.joining(", ") |
| Optional & Safety | Key point |
|---|---|
| Wrap nullable with ofNullable | not Optional.of(null) |
| Extract with orElse / orElseGet | never raw .get() |
| Use 4 method reference types | Class::method / obj::method |
| Know when to use parallel | large + CPU + stateless |
Next up β Sheet 4: Java OOP Β·
classes Β· objects Β· inheritance Β· polymorphism Β· abstract classes Β· interfaces Β· encapsulation β the pillars of object-oriented design in Java.