Java Streams API Cheat Sheet
Covers creating streams, intermediate operations like filter and map, terminal operations and collectors, and safe use of parallel streams.
Creating Streams
Common ways to obtain a Stream from collections and ranges.
Stream<String> s1 = Stream.of("a", "b", "c");Stream<Integer> s2 = List.of(1, 2, 3).stream();IntStream s3 = IntStream.range(0, 5); // 0..4IntStream s4 = IntStream.rangeClosed(1, 5); // 1..5Stream<Integer> infinite = Stream.iterate(1, n -> n * 2).limit(5); // 1,2,4,8,16
Intermediate Operations
Lazily chained transformations that build a stream pipeline.
List<String> names = List.of("Alice", "Bob", "Charlie", "Dave");List<String> result = names.stream() .filter(n -> n.length() > 3) // keep matching elements .map(String::toUpperCase) // transform each element .sorted() // natural ordering .distinct() // remove duplicates .limit(2) // take first 2 .collect(Collectors.toList());// flatMap - flattens nested streamsList<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));List<Integer> flat = nested.stream() .flatMap(List::stream) .collect(Collectors.toList()); // [1, 2, 3, 4]
Terminal Operations & Collectors
Operations that trigger the pipeline and produce a final result.
long count = names.stream().filter(n -> n.startsWith("A")).count();boolean any = names.stream().anyMatch(n -> n.equals("Bob"));Optional<String> first = names.stream().findFirst();int total = List.of(1, 2, 3).stream().reduce(0, Integer::sum); // reduce with identityMap<Integer, List<String>> byLength = names.stream() .collect(Collectors.groupingBy(String::length));String joined = names.stream().collect(Collectors.joining(", ", "[", "]"));double avg = names.stream().collect(Collectors.averagingInt(String::length));
Parallel Streams
Run a stream pipeline across multiple threads for large datasets.
long total = List.of(1, 2, 3, 4, 5).parallelStream() .mapToLong(Integer::longValue) .sum();// Use only for CPU-bound, stateless, independent operations on large datasetsIntStream.rangeClosed(1, 1_000_000) .parallel() .filter(n -> n % 7 == 0) .count();
Stream Fundamentals
Behaviors that trip people up the first time they use streams.
- Stream is lazy- Intermediate operations (filter, map) build a pipeline but don't execute until a terminal operation is called
- Streams are single-use- Calling a terminal operation consumes the stream; reusing it throws IllegalStateException
- map vs flatMap- map transforms 1-to-1; flatMap transforms 1-to-many and flattens the result
- Collectors.toList()/toSet()/toMap()- Common terminal collectors for materializing results
- Method references- String::toUpperCase, System.out::println shorthand for simple lambdas
- Primitive streams- IntStream/LongStream/DoubleStream avoid boxing overhead for numeric data
Custom Collectors with Collector.of
Build a reusable collector when the built-in Collectors factories don't fit your reduction.
Collector<String, StringJoiner, String> toCsv = Collector.of( () -> new StringJoiner(","), // supplier: mutable container StringJoiner::add, // accumulator StringJoiner::merge, // combiner (parallel streams) StringJoiner::toString // finisher);String csv = Stream.of("a", "b", "c").collect(toCsv); // "a,b,c"// Collectors.teeing (Java 12+) - run two collectors in one passvar stats = Stream.of(3, 1, 4, 1, 5).collect(Collectors.teeing( Collectors.minBy(Integer::compareTo), Collectors.maxBy(Integer::compareTo), (min, max) -> min.get() + ".." + max.get())); // "1..5"
groupingBy with Downstream Collectors
Combine grouping with a second collector to summarize each group instead of just listing it.
record Order(String customer, double amount) {}List<Order> orders = List.of( new Order("Ann", 120.0), new Order("Ann", 40.0), new Order("Bo", 75.0));Map<String, Double> totalByCustomer = orders.stream() .collect(Collectors.groupingBy(Order::customer, Collectors.summingDouble(Order::amount)));Map<String, Long> countByCustomer = orders.stream() .collect(Collectors.groupingBy(Order::customer, Collectors.counting()));// partitioningBy splits into exactly two groups (true/false keys)Map<Boolean, List<Order>> bigVsSmall = orders.stream() .collect(Collectors.partitioningBy(o -> o.amount() > 100));
Spliterator & Stream Characteristics
Understand what powers parallel splitting and why some sources parallelize poorly.
Spliterator<Integer> sp = List.of(1, 2, 3, 4, 5, 6).spliterator();Spliterator<Integer> half = sp.trySplit(); // splits off a prefix, if possibleSystem.out.println(sp.estimateSize());System.out.println(sp.characteristics() & Spliterator.SIZED); // ArrayList: SIZED, SUBSIZED, ORDERED// LinkedList / Iterator-derived streams split poorly (no random access) -// parallelStream() on them often performs WORSE than sequential due to// splitting overhead outweighing the gain.
Short-Circuiting & Stateful Operations
Some operations can stop the pipeline early; others must buffer the whole stream, breaking parallel benefits.
// Short-circuiting: findFirst/anyMatch/limit stop as soon as they canOptional<Integer> firstEven = Stream.of(1, 3, 5, 6, 7) .filter(n -> n % 2 == 0) .findFirst(); // stops at 6, doesn't touch 7// Stateful intermediate ops (sorted, distinct) must consume the ENTIRE// upstream before producing anything - they can't short-circuit and hurt// parallel performance because they need a full buffer + merge.List<Integer> sortedThenLimited = Stream.of(5, 3, 1, 4, 2) .sorted() // must see all elements first .limit(2) // [1, 2] .toList();
Avoiding Boxing in Numeric Pipelines
Mixing Stream<Integer> with primitive streams silently reintroduces autoboxing overhead.
List<Integer> nums = List.of(1, 2, 3, 4, 5);// BAD: reduce on Stream<Integer> boxes every intermediate sumint slow = nums.stream().reduce(0, Integer::sum);// GOOD: mapToInt converts once, then stays primitive through sum()int fast = nums.stream().mapToInt(Integer::intValue).sum();// IntSummaryStatistics gets min/max/avg/count/sum in a single passIntSummaryStatistics stats = nums.stream() .mapToInt(Integer::intValue) .summaryStatistics();System.out.printf("min=%d max=%d avg=%.1f%n", stats.getMin(), stats.getMax(), stats.getAverage());
Advanced Stream Gotchas
Subtle behaviors that surface once pipelines get more complex.
- Peek is not for side effects- peek() is meant for debugging only; the JIT may skip it entirely if the terminal op doesn't need every element (e.g. findFirst)
- Ordering with unordered sources- HashSet.stream() has no defined encounter order; forEach on it (especially parallel) may not process elements in insertion order
- Collectors.toMap key collisions- toMap(keyFn, valFn) throws IllegalStateException on duplicate keys unless a merge function is supplied as the 3rd argument
- Infinite streams need a limiter- Stream.generate()/iterate() never terminate on their own; always chain limit() or a takeWhile() predicate
- takeWhile vs filter- takeWhile (Java 9+) stops at the first non-matching element (short-circuits); filter scans everything
- Common ForkJoinPool sharing- parallelStream() reuses the shared commonPool by default; a slow blocking task in one parallel stream can starve unrelated parallel streams app-wide
- Collector.of characteristics- Pass CONCURRENT/UNORDERED flags to Collector.of when the accumulator is thread-safe, enabling a faster parallel merge strategy
Avoid mutating shared external state (like adding to an outside List) inside stream lambdas - it breaks under parallelStream() and defeats the purpose of functional-style pipelines; use collect() or reduce() instead.