Java 8 – Stream API: filter, map, reduce, collect and More
The Stream API (java.util.stream) is the most powerful practical addition of Java 8. It lets you process sequences of data — lists, arrays, sets — using a declarative pipeline: describe what you want (filter, transform, aggregate), and the Stream handles the how. This tutorial covers every important stream operation with real examples.
What is a Stream?
A Stream is a sequence of elements from a source (a collection, array, or generator) that supports sequential or parallel aggregate operations. Key properties:
- Not a data structure — streams don't store data; they process it
- Lazy — intermediate operations are only executed when a terminal operation is called
- Single use — once consumed, a stream cannot be reused
- Can be parallel — swap
.stream()for.parallelStream()to use multiple CPU cores
Stream Pipeline Structure
source → [intermediate operations] → terminal operation
Creating Streams
import java.util.*;
import java.util.stream.*;
// From a List
Stream<String> s1 = List.of("a", "b", "c").stream();
// From an array
Stream<String> s2 = Arrays.stream(new String[]{"x", "y", "z"});
// From values directly
Stream<Integer> s3 = Stream.of(1, 2, 3, 4, 5);
// Primitive streams — no boxing overhead
IntStream ints = IntStream.range(1, 6); // 1,2,3,4,5
LongStream longs = LongStream.rangeClosed(1, 5); // 1,2,3,4,5
DoubleStream doubles = DoubleStream.of(1.1, 2.2, 3.3);
// Infinite stream — must use limit()
Stream<Integer> infinite = Stream.iterate(0, n -> n + 2); // 0,2,4,6,...
Stream<Double> randoms = Stream.generate(Math::random);
Intermediate Operations
filter() – Keep matching elements
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evens); // [2, 4, 6, 8, 10]
map() – Transform each element
List<String> names = Arrays.asList("alice", "bob", "charlie");
List<String> upper = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper); // [ALICE, BOB, CHARLIE]
// Map to different type
List<Integer> lengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(lengths); // [5, 3, 7]
flatMap() – Flatten nested collections
List<List<Integer>> nested = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5),
Arrays.asList(6, 7, 8, 9)
);
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(flat); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
sorted() – Sort elements
List<String> words = Arrays.asList("banana", "apple", "cherry", "date");
// Natural order
words.stream().sorted().forEach(System.out::println);
// apple, banana, cherry, date
// Custom comparator — by length, then alphabetically
words.stream()
.sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()))
.forEach(System.out::println);
// date, apple, banana, cherry
distinct() – Remove duplicates
List<Integer> nums = Arrays.asList(1, 2, 2, 3, 3, 3, 4);
nums.stream().distinct().forEach(System.out::print); // 1234
limit() and skip()
// limit — take first N elements
Stream.iterate(1, n -> n + 1)
.limit(5)
.forEach(System.out::print); // 12345
// skip — skip first N elements
List<String> names = Arrays.asList("A", "B", "C", "D", "E");
names.stream().skip(2).forEach(System.out::print); // CDE
peek() – Debug without changing the stream
List<Integer> result = Arrays.asList(1, 2, 3, 4, 5)
.stream()
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("After filter: " + n))
.map(n -> n * n)
.peek(n -> System.out.println("After map: " + n))
.collect(Collectors.toList());
// After filter: 2 → After map: 4 → After filter: 4 → After map: 16 ...
Terminal Operations
collect() – Gather results into a collection
import java.util.stream.Collectors;
List<String> names = Arrays.asList("Alice", "Bob", "Alice", "Charlie", "Bob", "Alice");
// To List
List<String> list = names.stream().collect(Collectors.toList());
// To Set (removes duplicates)
Set<String> set = names.stream().collect(Collectors.toSet());
// Join to String
String joined = names.stream().collect(Collectors.joining(", "));
System.out.println(joined); // Alice, Bob, Alice, Charlie, Bob, Alice
// Count by value
Map<String, Long> frequency = names.stream()
.collect(Collectors.groupingBy(n -> n, Collectors.counting()));
System.out.println(frequency); // {Alice=3, Bob=2, Charlie=1}
// Group by first letter
Map<Character, List<String>> grouped = names.stream()
.distinct()
.collect(Collectors.groupingBy(n -> n.charAt(0)));
System.out.println(grouped); // {A=[Alice], B=[Bob], C=[Charlie]}
reduce() – Aggregate to a single value
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Sum using reduce
int sum = numbers.stream().reduce(0, Integer::sum);
System.out.println("Sum: " + sum); // Sum: 15
// Product
int product = numbers.stream().reduce(1, (a, b) -> a * b);
System.out.println("Product: " + product); // Product: 120
// Max
Optional<Integer> max = numbers.stream().reduce(Integer::max);
System.out.println("Max: " + max.get()); // Max: 5
count(), sum(), min(), max(), average()
List<Integer> nums = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);
System.out.println("Count: " + nums.stream().count()); // 8
System.out.println("Sum: " + nums.stream().mapToInt(i -> i).sum()); // 31
System.out.println("Min: " + nums.stream().mapToInt(i -> i).min().getAsInt()); // 1
System.out.println("Max: " + nums.stream().mapToInt(i -> i).max().getAsInt()); // 9
System.out.println("Average: " + nums.stream().mapToInt(i -> i).average().getAsDouble()); // 3.875
findFirst(), findAny(), anyMatch(), allMatch(), noneMatch()
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Optional<String> first = names.stream()
.filter(n -> n.startsWith("C"))
.findFirst();
System.out.println(first.orElse("not found")); // Charlie
boolean anyMatch = names.stream().anyMatch(n -> n.length() > 5); // true (Charlie)
boolean allMatch = names.stream().allMatch(n -> n.length() > 2); // true
boolean noneMatch = names.stream().noneMatch(n -> n.isEmpty()); // true
Complete Example – Product Catalog Analysis
import java.util.*;
import java.util.stream.*;
class Product {
String name; String category; double price; int quantity;
Product(String n, String c, double p, int q) {
name=n; category=c; price=p; quantity=q; }
public String toString() { return name + "(₹" + price + ")"; }
}
public class StreamDemo {
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("Laptop", "Electronics", 75000, 10),
new Product("Smartphone", "Electronics", 25000, 50),
new Product("Tablet", "Electronics", 35000, 30),
new Product("Keyboard", "Accessories", 2500, 100),
new Product("Mouse", "Accessories", 1500, 200),
new Product("Monitor", "Electronics", 15000, 20)
);
// 1. Average price of Electronics
OptionalDouble avgPrice = products.stream()
.filter(p -> p.category.equals("Electronics"))
.mapToDouble(p -> p.price)
.average();
System.out.printf("Avg Electronics price: ₹%.0f%n", avgPrice.getAsDouble());
// ₹37500
// 2. Most expensive product
products.stream()
.max(Comparator.comparingDouble(p -> p.price))
.ifPresent(p -> System.out.println("Most expensive: " + p));
// Laptop(₹75000.0)
// 3. Group products by category
Map<String, List<Product>> byCategory = products.stream()
.collect(Collectors.groupingBy(p -> p.category));
byCategory.forEach((cat, list) ->
System.out.println(cat + ": " + list));
// 4. Total inventory value (price × quantity)
double totalValue = products.stream()
.mapToDouble(p -> p.price * p.quantity)
.sum();
System.out.printf("Total inventory value: ₹%.0f%n", totalValue);
// ₹4,025,000
// 5. Names of products under ₹10000, sorted
String cheap = products.stream()
.filter(p -> p.price < 10000)
.sorted(Comparator.comparingDouble(p -> p.price))
.map(p -> p.name)
.collect(Collectors.joining(", "));
System.out.println("Cheap products: " + cheap);
// Mouse, Keyboard
}
}
Parallel Streams
// Sequential (default)
long count1 = IntStream.range(1, 1_000_001).filter(n -> n % 2 == 0).count();
// Parallel — uses all CPU cores automatically
long count2 = IntStream.range(1, 1_000_001).parallel().filter(n -> n % 2 == 0).count();
System.out.println(count1); // 500000
System.out.println(count2); // 500000 (same result, faster on large datasets)
Summary
The Stream API transforms collection processing from imperative loops into declarative pipelines. The structure is always: source → zero or more intermediate operations → one terminal operation. Key intermediate operations: filter(), map(), flatMap(), sorted(), distinct(). Key terminal operations: collect(), reduce(), forEach(), count(), findFirst(). Use parallelStream() for CPU-bound operations on large datasets. Use Collectors.groupingBy() to replace complex for-loop grouping logic with a single line.
Comments