Java Streams
The Streams API, added in Java 8, is a declarative way to process sequences of values. A stream pipeline consists of a source, zero or more intermediate operations (filter, map, sorted, distinct...), and a terminal operation (collect, reduce, count, forEach). Streams are lazy — nothing happens until a terminal operation is invoked.
This tutorial covers creating streams, the most common intermediate and terminal operations, the difference between lazy and eager, parallel streams, and the collectors that turn a stream back into a collection.
1. Creating Streams
import java.util.stream.Stream;
import java.util.stream.IntStream;
import java.util.List;
class=class="tok-str">"tok-cmt">// from a collection
Stream<String> s1 = List.of("a","b","c").stream();
class=class="tok-str">"tok-cmt">// from an array
Stream<String> s2 = Stream.of("a","b","c");
Stream<Integer> s3 = java.util.Arrays.stream(new Integer[]{class="tok-num">1,class="tok-num">2,class="tok-num">3});
class=class="tok-str">"tok-cmt">// numeric range
IntStream range = IntStream.range(class="tok-num">0, class="tok-num">100); class=class="tok-str">"tok-cmt">// class="tok-num">0..class="tok-num">99
IntStream closed = IntStream.rangeClosed(class="tok-num">1, class="tok-num">10); class=class="tok-str">"tok-cmt">// class="tok-num">1..class="tok-num">10
class=class="tok-str">"tok-cmt">// generate / iterate
Stream<Double> randoms = Stream.generate(Math::random).limit(class="tok-num">10);
Stream<Integer> naturals = Stream.iterate(class="tok-num">1, n -> n + class="tok-num">1).limit(class="tok-num">100);
class=class="tok-str">"tok-cmt">// from a function (Java class="tok-num">9+ iterate with predicate)
Stream<Integer> finite = Stream.iterate(class="tok-num">1, n -> n <= class="tok-num">100, n -> n + class="tok-num">1);
class=class="tok-str">"tok-cmt">// from lines of a file
try (Stream<String> lines = java.nio.file.Files.lines(java.nio.file.Path.of("data.txt"))) {
lines.forEach(System.out::println);
}
2. The Pipeline Pattern
A typical pipeline: source → filter → map → terminal. Each intermediate operation returns a new stream, so they chain fluently:
import java.util.List;
import java.util.stream.Collectors;
List<String> names = List.of("alice","bob","carol","dave","eve");
List<String> upperLong = names.stream()
.filter(n -> n.length() > class="tok-num">3) class=class="tok-str">"tok-cmt">// keep alice, carol, dave
.map(String::toUpperCase) class=class="tok-str">"tok-cmt">// ALICE, CAROL, DAVE
.sorted() class=class="tok-str">"tok-cmt">// ALICE, CAROL, DAVE
.collect(Collectors.toList()); class=class="tok-str">"tok-cmt">// materialise
class=class="tok-str">"tok-cmt">// the same with .toList() (Java class="tok-num">16+) - returns an immutable list
List<String> result = names.stream()
.filter(n -> n.length() > class="tok-num">3)
.map(String::toUpperCase)
.toList();
Nothing actually runs until you call a terminal operation. Intermediate operations describe what to do; the terminal operation triggers execution. This means you can build a pipeline and only pay for what you actually consume.
3. Filtering and Slicing
List<Integer> nums = List.of(class="tok-num">5, class="tok-num">3, class="tok-num">8, class="tok-num">1, class="tok-num">9, class="tok-num">2, class="tok-num">7, class="tok-num">4);
class=class="tok-str">"tok-cmt">// keep evens
List<Integer> evens = nums.stream()
.filter(n -> n % class="tok-num">2 == class="tok-num">0)
.toList(); class=class="tok-str">"tok-cmt">// [class="tok-num">8, class="tok-num">2, class="tok-num">4]
class=class="tok-str">"tok-cmt">// distinct elements
List<Integer> uniq = List.of(class="tok-num">1, class="tok-num">1, class="tok-num">2, class="tok-num">3, class="tok-num">3, class="tok-num">3, class="tok-num">4).stream()
.distinct()
.toList(); class=class="tok-str">"tok-cmt">// [class="tok-num">1, class="tok-num">2, class="tok-num">3, class="tok-num">4]
class=class="tok-str">"tok-cmt">// take first N matching
List<Integer> first3 = nums.stream()
.filter(n -> n > class="tok-num">4)
.limit(class="tok-num">3)
.toList();
class=class="tok-str">"tok-cmt">// skip and limit together = pagination
List<Integer> page2 = nums.stream()
.skip(class="tok-num">2)
.limit(class="tok-num">2)
.toList(); class=class="tok-str">"tok-cmt">// [class="tok-num">8, class="tok-num">1]
class=class="tok-str">"tok-cmt">// dropWhile / takeWhile (Java class="tok-num">9+)
List<Integer> prefix = nums.stream()
.takeWhile(n -> n < class="tok-num">8)
.toList(); class=class="tok-str">"tok-cmt">// [class="tok-num">5, class="tok-num">3] - stops at class="tok-num">8
4. Mapping and FlatMap
List<String> names = List.of("alice","bob","carol");
class=class="tok-str">"tok-cmt">// map: class="tok-num">1-to-class="tok-num">1 transformation
List<Integer> lengths = names.stream()
.map(String::length)
.toList(); class=class="tok-str">"tok-cmt">// [class="tok-num">5, class="tok-num">3, class="tok-num">5]
class=class="tok-str">"tok-cmt">// map to a different type
List<Person> people = names.stream()
.map(name -> new Person(name, class="tok-num">0))
.toList();
class=class="tok-str">"tok-cmt">// flatMap: class="tok-num">1-to-many
List<List<Integer>> nested = List.of(List.of(class="tok-num">1,class="tok-num">2,class="tok-num">3), List.of(class="tok-num">4,class="tok-num">5), List.of(class="tok-num">6,class="tok-num">7,class="tok-num">8,class="tok-num">9));
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.toList(); class=class="tok-str">"tok-cmt">// [class="tok-num">1,class="tok-num">2,class="tok-num">3,class="tok-num">4,class="tok-num">5,class="tok-num">6,class="tok-num">7,class="tok-num">8,class="tok-num">9]
class=class="tok-str">"tok-cmt">// flatMap on a stream of strings to a stream of chars
List<Character> chars = List.of("hi","bye").stream()
.flatMapToInt(String::chars)
.mapToObj(c -> (char) c)
.toList();
flatMap flattens nested streams — essential when one element produces multiple results.
5. Reductions: reduce, count, sum
class=class="tok-str">"tok-cmt">// reduce to a single value
int sum = IntStream.rangeClosed(class="tok-num">1, class="tok-num">10).reduce(class="tok-num">0, Integer::sum); class=class="tok-str">"tok-cmt">// class="tok-num">55
int product = IntStream.rangeClosed(class="tok-num">1, class="tok-num">5).reduce(class="tok-num">1, (a,b) -> a*b); class=class="tok-str">"tok-cmt">// class="tok-num">120
class=class="tok-str">"tok-cmt">// count, min, max
long count = Stream.of(class="tok-num">1,class="tok-num">2,class="tok-num">3,class="tok-num">4,class="tok-num">5).count(); class=class="tok-str">"tok-cmt">// class="tok-num">5
int max = Stream.of(class="tok-num">3,class="tok-num">1,class="tok-num">4,class="tok-num">1,class="tok-num">5,class="tok-num">9,class="tok-num">2,class="tok-num">6).max(Integer::compareTo).orElse(class="tok-num">0); class=class="tok-str">"tok-cmt">// class="tok-num">9
class=class="tok-str">"tok-cmt">// sum, average on numeric streams
IntStream.range(class="tok-num">1, class="tok-num">11).sum(); class=class="tok-str">"tok-cmt">// class="tok-num">55
IntStream.range(class="tok-num">1, class="tok-num">11).average().orElse(class="tok-num">0); class=class="tok-str">"tok-cmt">// class="tok-num">5.5
IntStream.range(class="tok-num">1, class="tok-num">11).summaryStatistics();
class=class="tok-str">"tok-cmt">// IntSummaryStatistics{count=class="tok-num">10, sum=class="tok-num">55, min=class="tok-num">1, max=class="tok-num">10, average=class="tok-num">5.500000}
class=class="tok-str">"tok-cmt">// anyMatch / allMatch / noneMatch
boolean hasEven = nums.stream().anyMatch(n -> n % class="tok-num">2 == class="tok-num">0);
boolean allPos = nums.stream().allMatch(n -> n > class="tok-num">0);
boolean noNeg = nums.stream().noneMatch(n -> n < class="tok-num">0);
6. Collectors
The terminal collect operation turns a stream into a collection or other value. The Collectors class provides the common collectors:
import java.util.stream.Collectors;
import java.util.Map;
import java.util.List;
class=class="tok-str">"tok-cmt">// to a list
List<String> list = stream.collect(Collectors.toList());
class=class="tok-str">"tok-cmt">// to an unmodifiable list (Java class="tok-num">16+)
List<String> unmod = stream.toList();
class=class="tok-str">"tok-cmt">// to a set
Set<String> set = stream.collect(Collectors.toSet());
class=class="tok-str">"tok-cmt">// to a specific collection
ArrayList<String> arr = stream.collect(Collectors.toCollection(ArrayList::new));
class=class="tok-str">"tok-cmt">// to a map
Map<String, Integer> map = people.stream()
.collect(Collectors.toMap(Person::name, Person::age));
class=class="tok-str">"tok-cmt">// joining strings
String csv = Stream.of("a","b","c").collect(Collectors.joining(", "));
class=class="tok-str">"tok-cmt">// "a, b, c"
class=class="tok-str">"tok-cmt">// grouping
Map<Character, List<String>> byLetter =
words.stream().collect(Collectors.groupingBy(s -> s.charAt(class="tok-num">0)));
class=class="tok-str">"tok-cmt">// partitioning
Map<Boolean, List<Integer>> parts =
nums.stream().collect(Collectors.partitioningBy(n -> n % class="tok-num">2 == class="tok-num">0));
class=class="tok-str">"tok-cmt">// counting by group
Map<Character, Long> counts =
words.stream().collect(Collectors.groupingBy(s -> s.charAt(class="tok-num">0), Collectors.counting()));
7. Optional and Search Operations
Search operations return Optional, a value that may or may not be present:
import java.util.Optional;
class=class="tok-str">"tok-cmt">// findFirst - the first matching element
Optional<Integer> firstEven = nums.stream().filter(n -> n % class="tok-num">2 == class="tok-num">0).findFirst();
int v = firstEven.orElse(-class="tok-num">1); class=class="tok-str">"tok-cmt">// -class="tok-num">1 if absent
int v2 = firstEven.orElseThrow(); class=class="tok-str">"tok-cmt">// throws NoSuchElementException if absent
class=class="tok-str">"tok-cmt">// findAny - any matching element, may be faster on parallel streams
Optional<Integer> anyEven = nums.stream().filter(n -> n % class="tok-num">2 == class="tok-num">0).findAny();
class=class="tok-str">"tok-cmt">// min / max return Optional
Optional<Integer> min = nums.stream().min(Integer::compareTo);
class=class="tok-str">"tok-cmt">// consume if present
firstEven.ifPresent(n -> System.out.println("got " + n));
class=class="tok-str">"tok-cmt">// transform the value inside the optional
Optional<Integer> doubled = firstEven.map(n -> n * class="tok-num">2);
Optional<String> asString = firstEven.map(Object::toString);
Optional.get() without a checkCalling get() on an empty optional throws NoSuchElementException. Prefer orElse, orElseGet, or ifPresent.
8. Numeric Streams
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.DoubleStream;
class=class="tok-str">"tok-cmt">// ranges
IntStream.range(class="tok-num">0, class="tok-num">10).forEach(System.out::println);
IntStream.rangeClosed(class="tok-num">1, class="tok-num">5).sum(); class=class="tok-str">"tok-cmt">// class="tok-num">15
class=class="tok-str">"tok-cmt">// convert object stream to numeric
List<Integer> list = List.of(class="tok-num">1,class="tok-num">2,class="tok-num">3);
int sum = list.stream().mapToInt(Integer::intValue).sum(); class=class="tok-str">"tok-cmt">// class="tok-num">6
double avg = list.stream().mapToInt(Integer::intValue).average().orElse(class="tok-num">0);
class=class="tok-str">"tok-cmt">// convert numeric back to object
List<Integer> boxed = IntStream.range(class="tok-num">0, class="tok-num">5).boxed().toList();
class=class="tok-str">"tok-cmt">// random numbers
import java.util.Random;
new Random().ints(class="tok-num">5, class="tok-num">1, class="tok-num">100).forEach(System.out::println); class=class="tok-str">"tok-cmt">// class="tok-num">5 random ints in [class="tok-num">1, class="tok-num">100)
9. Parallel Streams
Adding .parallel() runs the pipeline on the common ForkJoinPool. The API is the same; only the execution changes:
class=class="tok-str">"tok-cmt">// sequential
long sequential = list.stream().filter(x -> isPrime(x)).count();
class=class="tok-str">"tok-cmt">// parallel - runs on the common ForkJoinPool
long parallel = list.parallelStream().filter(x -> isPrime(x)).count();
class=class="tok-str">"tok-cmt">// or
long parallel2 = list.stream().parallel().filter(x -> isPrime(x)).count();
class=class="tok-str">"tok-cmt">// ordering: forEach with parallel may produce out-of-order output
class=class="tok-str">"tok-cmt">// Use forEachOrdered if order matters.
list.parallelStream().forEachOrdered(System.out::println);
class=class="tok-str">"tok-cmt">// avoid parallel streams with stateful lambdas or shared mutable state
class=class="tok-str">"tok-cmt">// they are NOT a magic "go faster" switch - benchmark before relying on them
The overhead of splitting, scheduling, and merging can exceed the gain on small or fast pipelines. Measure before relying on parallel. Use it for CPU-bound pipelines with at least a few thousand elements, and avoid it when ordering matters.
Exercises
- Filter a list of integers to keep only primes, then sum them.
- Map a list of strings to their lengths and find the maximum.
- Use
Collectors.groupingByto group a list of words by their first letter. - Use
IntStream.rangeClosedto compute the sum 1+2+...+100. - Use
flatMapto flatten a list of lists into one stream.