Java Stream API Cheat Sheet¶
What is a Stream? Why it’s useful¶
A Stream is a sequence of elements supporting functional-style operations (map, filter, reduce) on collections, arrays, files, etc. It helps you write concise, readable, parallelizable code without manual loops and temporary lists.
Quick glossary¶
- Source: Where elements come from (e.g.,
List, array, file). - Intermediate op: Returns a new stream (lazy). Examples:
filter,map,sorted. - Terminal op: Triggers processing, produces a value/collection/side-effect. Examples:
collect,count,forEach. - Lazy: Nothing runs until a terminal operation is called.
- Stateless vs stateful:
mapis stateless;sorted/distinctare stateful (need to see more than one element). - Boxed vs primitive streams:
Stream<T>vsIntStream/LongStream/DoubleStream(no boxing, extra math ops). - Collector: Strategy to accumulate stream results (e.g.,
Collectors.toList()). - Short-circuiting: Stops early (e.g.,
findFirst,anyMatch,limit). - Ordered: Some sources are ordered (
List); some aren’t (HashSet).
Creating instances¶
// From collections & arrays
List<String> names = List.of("Ana","Bob","Anya","Bo");
Stream<String> s1 = names.stream();
Stream<String> s2 = names.parallelStream();
IntStream s3 = Arrays.stream(new int[]{1,2,3});
// Literals / empty
Stream<String> s4 = Stream.of("a","b","c");
Stream<Object> s5 = Stream.empty();
// Ranges / iterate / generate
IntStream s6 = IntStream.range(0, 3); // 0,1,2
IntStream s7 = IntStream.rangeClosed(1, 3); // 1,2,3
Stream<Integer> s8 = Stream.iterate(1, n -> n+1).limit(3); // 1,2,3
Stream<Integer> s9 = Stream.iterate(1, n -> n <= 3, n -> n+1); // Java 9+ bounded
Stream<Double> s10 = Stream.generate(Math::random).limit(2);
// Files, regex, builder
// (wrap in try-with-resources in real code)
try (Stream<String> lines = java.nio.file.Files.lines(java.nio.file.Path.of("data.txt"))) {}
Stream<String> words = java.util.regex.Pattern.compile("\\s+").splitAsStream("hi there");
Stream<String> built = Stream.<String>builder().add("x").add("y").build();
Typical output (examples):
IntStream.range(0,3).boxed().toList()→[0, 1, 2]Stream.of("a","b").toList()(J16+) →["a", "b"]
Reading state / accessors (terminal)¶
List<Integer> nums = List.of(3,1,4,1,5);
long c = nums.stream().count(); // -> 5
int min = nums.stream().mapToInt(i->i).min().orElse(-1); // -> 1
int max = nums.stream().mapToInt(i->i).max().orElse(-1); // -> 5
double avg = nums.stream().mapToInt(i->i).average().orElse(0); // -> 2.8
Optional<Integer> first = nums.stream().findFirst(); // -> Optional[3]
Checking properties¶
List<String> l = List.of("a","bb","ccc");
boolean anyLong = l.stream().anyMatch(s -> s.length() >= 3); // -> true
boolean allShort = l.stream().allMatch(s -> s.length() <= 3); // -> true
boolean noneEmpty = l.stream().noneMatch(String::isEmpty); // -> true
boolean isPar = l.parallelStream().isParallel(); // -> true
Transformations (pure ops, no side effects)¶
List<String> raw = List.of(" a","b "," c ","b");
List<String> cleaned =
raw.stream()
.map(String::trim) // ["a","b","c","b"]
.filter(s -> !s.isEmpty()) // same
.distinct() // ["a","b","c"] (order preserved)
.sorted() // ["a","b","c"]
.toList(); // J16+
// flatMap
List<String> phrases = List.of("a b", "c");
List<String> tokens =
phrases.stream()
.flatMap(p -> Arrays.stream(p.split("\\s+")))
.toList(); // -> ["a","b","c"]
// limit / skip
List<Integer> first2 = IntStream.rangeClosed(1,5).limit(2).boxed().toList(); // [1,2]
List<Integer> skip2 = IntStream.rangeClosed(1,5).skip(2).boxed().toList(); // [3,4,5]
// peek (debug only, don’t mutate state!)
List<Integer> out =
IntStream.range(1,4)
.peek(i -> System.out.println("saw " + i)) // prints 1,2,3 during terminal op
.map(i -> i*i)
.boxed()
.toList(); // -> [1,4,9]
Conversions (to other types)¶
// To collections
List<String> list1 = Stream.of("a","b").toList(); // J16+ (unmodifiable)
List<String> list2 = Stream.of("a","b").collect(Collectors.toList()); // modifiable (usually)
Set<String> set = Stream.of("a","b","a").collect(Collectors.toSet()); // -> ["a","b"] (order undefined)
Map<Integer, String> map = Stream.of("a","bb","ccc")
.collect(Collectors.toMap(String::length, s -> s)); // beware duplicate keys
// To array
String[] arr = Stream.of("x","y").toArray(String[]::new);
// To primitives / from primitives
IntStream ints = Stream.of(1,2,3).mapToInt(Integer::intValue);
Stream<Integer> boxed = IntStream.range(1,3).boxed();
// Joining & reducing
String joined = Stream.of("a","b","c").collect(Collectors.joining(",")); // -> "a,b,c"
int sum = IntStream.of(1,2,3).sum(); // -> 6
int prod = IntStream.of(1,2,3).reduce(1, (a,b) -> a*b); // -> 6
Iteration & comparison¶
// Iterate (with side effects) — prefer terminal ops for side effects, not intermediate
List<String> items = List.of("A","B","C");
items.stream().forEach(System.out::println); // prints in encounter order (or use forEachOrdered)
items.stream().forEachOrdered(System.out::println); // stable order even in parallel streams
// Compare two sequences (content equality)
boolean same = List.of(1,2,3).stream().toList().equals(List.of(1,2,3)); // -> true
// Or compare after materializing (common & simple):
boolean same2 = Arrays.equals(
List.of(1,2,3).stream().mapToInt(Integer::intValue).toArray(),
IntStream.of(1,2,3).toArray()
); // -> true
Common utilities (helpers you’ll reach for)¶
// Comparators
record Person(String name, int age) {}
List<Person> people = List.of(new Person("Ana",30), new Person("Bob",25), new Person("Bo",25));
List<Person> byAgeThenName =
people.stream()
.sorted(Comparator.comparingInt(Person::age).thenComparing(Person::name))
.toList(); // -> [(Bob,25),(Bo,25),(Ana,30)]
// Grouping / partitioning
Map<Integer, List<Person>> byAge =
people.stream().collect(Collectors.groupingBy(Person::age));
// -> {25=[(Bob,25),(Bo,25)], 30=[(Ana,30)]}
Map<Boolean, List<Person>> partition =
people.stream().collect(Collectors.partitioningBy(p -> p.age() >= 30));
// -> {true=[(Ana,30)], false=[(Bob,25),(Bo,25)]}
// Summarizing
IntSummaryStatistics stats =
people.stream().collect(Collectors.summarizingInt(Person::age));
// stats.getCount()=3, getMin()=25, getMax()=30, getAverage()=26.666...
Gotchas / anti-patterns / version notes¶
- Streams are single-use: Once a terminal op runs, the stream is consumed. Create a new stream if you need to run another terminal op.
- Side effects in intermediate ops (
map,filter,peek) are error-prone. Keep them pure. Usepeekonly for debugging/logging. parallelStream(): Only beneficial for CPU-heavy, stateless, large workloads. Avoid for small lists, IO-bound work, or when order matters. Be careful with thread-unsafe code.- Ordering:
HashSet.stream()has no deterministic order; operations likesorted()will impose order at a cost. Collectors.toMapduplicate keys throwIllegalStateException. Provide a merge function:
var m = Stream.of("a","bb","aa")
.collect(Collectors.toMap(String::length, s->s, (a,b)->a)); // keep first
Stream.toList() (Java 16+) returns unmodifiable list; Collectors.toList() usually returns a mutable list (implementation-dependent).
* Files.lines(...) holds file resources—use try-with-resources.
* Boxing overhead: Prefer IntStream/LongStream/DoubleStream for numeric crunching.
* Don’t store streams in fields; build & consume locally.
* Java 9+ goodies: Stream.iterate(seed, hasNext, next), takeWhile, dropWhile (on ordered streams).
Mini reference table¶
| Method | What it does | Example → Output |
|---|---|---|
filter(p) |
Keep elements matching predicate | List.of(1,2,3).stream().filter(i->i%2==1).toList() → [1,3] |
map(f) |
Transform each element | Stream.of("a","bb").map(String::length).toList() → [1,2] |
flatMap(f) |
Flatten nested streams | Stream.of("a b","c").flatMap(s->Arrays.stream(s.split(" "))).toList() → [a,b,c] |
distinct() |
Remove duplicates (keeps first) | Stream.of(1,1,2).distinct().toList() → [1,2] |
sorted() |
Sort using natural order | Stream.of(3,1,2).sorted().toList() → [1,2,3] |
limit(n) / skip(n) |
Truncate / skip | IntStream.range(1,6).limit(3).boxed().toList() → [1,2,3] |
anyMatch/allMatch/noneMatch |
Check membership conditions | Stream.of("a","bb").anyMatch(s->s.length()==2) → true |
findFirst/findAny |
Get an element (Optional) | Stream.of(10,20).findFirst() → Optional[10] |
reduce(id,acc) |
Fold elements | IntStream.of(1,2,3).reduce(0,Integer::sum) → 6 |
collect(...) |
Aggregate via collectors | Stream.of("a","b").collect(Collectors.joining(",")) → "a,b" |
mapToInt/boxed |
Convert between primitive/boxed | Stream.of(1,2).mapToInt(i->i).sum() → 3 |
End-to-end example (typical outputs shown)¶
Task: From a list of people, get the top 2 cities by average age, list residents per city alphabetically, and produce some quick stats.
import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
record Person(String name, String city, int age) {}
public class Demo {
public static void main(String[] args) {
List<Person> people = List.of(
new Person("Ana","Vilnius",30),
new Person("Bob","Kaunas",25),
new Person("Bo","Kaunas",27),
new Person("Anya","Vilnius",34),
new Person("Cara","Klaipeda",22)
);
// Group by city and compute average age
Map<String, Double> avgAgeByCity =
people.stream().collect(groupingBy(Person::city, averagingInt(Person::age)));
System.out.println("avgAgeByCity=" + avgAgeByCity);
// -> avgAgeByCity={Kaunas=26.0, Klaipeda=22.0, Vilnius=32.0}
// Top 2 cities by average age (desc)
List<String> top2Cities =
avgAgeByCity.entrySet().stream()
.sorted(Map.Entry.<String,Double>comparingByValue(Comparator.reverseOrder()))
.limit(2)
.map(Map.Entry::getKey)
.toList();
System.out.println("top2Cities=" + top2Cities);
// -> top2Cities=[Vilnius, Kaunas]
// Residents per city alphabetically (for only top2)
Map<String, List<String>> residents =
people.stream()
.filter(p -> top2Cities.contains(p.city()))
.collect(groupingBy(Person::city,
mapping(Person::name,
collectingAndThen(toList(), l -> {
l.sort(Comparator.naturalOrder());
return l;
}))));
System.out.println("residents=" + residents);
// -> residents={Kaunas=[Bo, Bob], Vilnius=[Ana, Anya]}
// Quick stats overall
IntSummaryStatistics stats = people.stream().collect(summarizingInt(Person::age));
System.out.println("count=" + stats.getCount()
+ ", min=" + stats.getMin()
+ ", max=" + stats.getMax()
+ ", avg=" + String.format("%.2f", stats.getAverage()));
// -> count=5, min=22, max=34, avg=27.60
}
}
Bottom line (best practices)¶
- Think pipelines:
source → (map/filter/…)* → terminal. - Keep intermediate ops pure; put side effects in terminal ops (
forEach, I/O). - Prefer
toList()(J16+) for concise unmodifiable results; useCollectors.toList()when you need a mutable list. - Mind order & cost: Put cheap filters early; avoid unnecessary
sorted/distinct. - Prefer primitive streams for numeric performance.
- Avoid
parallelStream()unless you’ve measured a win and your operations are thread-safe, stateless, and CPU-bound. - Handle
Optionalproperly—useorElse,orElseGet,ifPresent, notget()blindly. - Don’t over-stream: A plain
forloop is fine for very simple, stateful, or performance-critical mutations.
🔑 Wildcards in Stream API¶
? super T → Consumer¶
Use when the API feeds stream elements into something.
forEach(Consumer<? super T>)
Stream<Integer> s = Stream.of(1,2,3);
Consumer<Number> print = n -> System.out.println(n);
s.forEach(print); // OK because Consumer<? super Integer>
allMatch(Predicate<? super T>)
Stream<String> words = Stream.of("a", "bb");
Predicate<Object> notNull = Objects::nonNull; // Predicate<Object> is super of String
boolean all = words.allMatch(notNull);
sorted(Comparator<? super T>)
Stream<String> s = Stream.of("c","a","b");
Comparator<Object> cmp = (o1,o2) -> o1.toString().compareTo(o2.toString());
s.sorted(cmp).forEach(System.out::println);
? extends R → Producer¶
Use when the API returns new elements produced from the stream.
map(Function<? super T, ? extends R>)
Stream<Integer> s = Stream.of(1,2,3);
Function<Number, String> f = n -> "Num:" + n;
Stream<String> out = s.map(f); // R is String
flatMap(Function<? super T, ? extends Stream<? extends R>>)
Stream<String> s = Stream.of("a,b","c");
Stream<String> flat = s.flatMap(str -> Arrays.stream(str.split(",")));
📌 Rule of Thumb (PECS)¶
- Producer → Extends: output side (
map,flatMap) - Consumer → Super: input side (
forEach,allMatch,sorted)
👉 That’s really all you need:
- If the stream gives elements to your function =
? super T. - If your function produces new elements =
? extends R.