Java · lesson 28 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Java Streams API
Learn the Java Streams API by running it: laziness, filter, map, flatMap, sorted, reduce, Collectors grouping and partitioning, and primitive streams.
By SkillByExample · Updated
A stream is a pipeline over a source — a collection, an array, a range — made of lazy
intermediate operations and one terminal operation that runs it. Streams do not store data and
do not change their source. Every example is a single file run with the JDK 21 source launcher
(java File.java).
Laziness: nothing runs until the terminal operation
import java.util.List;
import java.util.stream.Stream;
public class LazyDemo {
public static void main(String[] args) {
Stream<Integer> pipeline = List.of(1, 2, 3, 4, 5, 6).stream()
.filter(n -> { System.out.println(" filter " + n); return n % 2 == 0; })
.map(n -> { System.out.println(" map " + n); return n * n; });
System.out.println("pipeline built, nothing printed yet");
System.out.println("first = " + pipeline.findFirst().orElseThrow());
try {
pipeline.count();
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
}
}
}
$ java LazyDemo.java
pipeline built, nothing printed yet
filter 1
filter 2
map 2
first = 4
stream has already been operated upon or closed
Building the pipeline printed nothing. When findFirst ran, elements went through one at a
time — 1 was filtered out, 2 passed and was mapped — and the stream stopped there; 3 to 6
were never examined. A stream is single-use, so the second terminal operation throws.
Creating streams
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class CreateDemo {
public static void main(String[] args) {
System.out.println(List.of("a", "b", "c").stream().toList());
System.out.println(Arrays.stream(new int[] {3, 1, 2}).sum());
System.out.println(Stream.of("x", "y", "z").count());
System.out.println(IntStream.range(1, 5).boxed().toList() + " " + IntStream.rangeClosed(1, 5).boxed().toList());
System.out.println(Stream.iterate(1, n -> n * 2).limit(8).toList());
System.out.println(Stream.iterate(1, n -> n < 100, n -> n * 3).toList());
System.out.println(Stream.generate(() -> "hi").limit(3).toList());
}
}
$ java CreateDemo.java
[a, b, c]
6
3
[1, 2, 3, 4] [1, 2, 3, 4, 5]
[1, 2, 4, 8, 16, 32, 64, 128]
[1, 3, 9, 27, 81]
[hi, hi, hi]
range excludes its end and rangeClosed includes it. iterate and generate produce infinite
streams, so they need limit — or the three-argument iterate (Java 9+), whose middle argument
works like a for loop’s condition.
filter, map and flatMap
import java.util.Arrays;
import java.util.List;
public class TransformDemo {
record Person(String name, int age) {}
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
System.out.println(numbers.stream().filter(n -> n > 3).filter(n -> n % 2 != 0).toList());
List<Person> people = List.of(new Person("Alice", 30), new Person("Bob", 25), new Person("Charlie", 35));
System.out.println(people.stream().map(Person::name).toList());
System.out.println(people.stream().map(Person::name).map(String::length).toList());
List<String> sentences = List.of("Hello World", "Java Streams", "are great");
System.out.println(sentences.stream().map(s -> s.split(" ")).toList().size() + " arrays");
System.out.println(sentences.stream().flatMap(s -> Arrays.stream(s.split(" "))).toList());
System.out.println(List.of(List.of(1, 2), List.of(3), List.<Integer>of()).stream().flatMap(List::stream).toList());
}
}
$ java TransformDemo.java
[5, 7, 9]
[Alice, Bob, Charlie]
[5, 3, 7]
3 arrays
[Hello, World, Java, Streams, are, great]
[1, 2, 3]
map is one-in, one-out: mapping three sentences with split gives three arrays. flatMap
lets each element produce a stream of any length — including zero, as the empty list shows — and
concatenates them into a single flat stream.
distinct, sorted, skip, limit, peek
import java.util.Comparator;
import java.util.List;
public class OrderingDemo {
record Person(String name, int age) {}
public static void main(String[] args) {
List<Integer> nums = List.of(5, 3, 1, 4, 1, 5, 9, 2, 6, 5);
System.out.println(nums.stream().distinct().toList());
System.out.println(nums.stream().distinct().sorted().skip(2).limit(3).toList());
System.out.println(nums.stream().sorted(Comparator.reverseOrder()).limit(3).toList());
List<Person> people = List.of(new Person("Bob", 25), new Person("Alice", 30), new Person("Cara", 25));
System.out.println(people.stream()
.sorted(Comparator.comparingInt(Person::age).thenComparing(Person::name))
.map(Person::name).toList());
List<Integer> doubled = nums.stream().filter(n -> n > 5).peek(n -> System.out.println("saw " + n)).map(n -> n * 2).toList();
System.out.println(doubled);
}
}
$ java OrderingDemo.java
[5, 3, 1, 4, 9, 2, 6]
[3, 4, 5]
[9, 6, 5]
[Bob, Cara, Alice]
saw 9
saw 6
[18, 12]
distinct keeps the first occurrence of each value in encounter order. thenComparing breaks
ties: Bob and Cara are both 25, so they are ordered by name. peek lets you watch elements pass
without changing them — handy for debugging, but do not put real logic in it.
Collectors: grouping, counting, partitioning, joining
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class CollectDemo {
public static void main(String[] args) {
List<String> words = List.of("apple", "banana", "cherry", "apricot", "blueberry");
Map<Character, List<String>> byLetter = words.stream()
.collect(Collectors.groupingBy(w -> w.charAt(0), TreeMap::new, Collectors.toList()));
System.out.println(byLetter);
Map<Character, Long> counts = words.stream()
.collect(Collectors.groupingBy(w -> w.charAt(0), TreeMap::new, Collectors.counting()));
System.out.println(counts);
System.out.println(words.stream().collect(Collectors.partitioningBy(w -> w.length() > 5)));
Map<String, Integer> lengths = words.stream().collect(Collectors.toMap(w -> w, String::length, (a, b) -> a, TreeMap::new));
System.out.println(lengths);
System.out.println(words.stream().collect(Collectors.joining(", ", "[", "]")));
try {
words.stream().collect(Collectors.toMap(w -> w.charAt(0), w -> w));
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
}
}
}
$ java CollectDemo.java
{a=[apple, apricot], b=[banana, blueberry], c=[cherry]}
{a=2, b=2, c=1}
{false=[apple], true=[banana, cherry, apricot, blueberry]}
{apple=5, apricot=7, banana=6, blueberry=9, cherry=6}
[apple, banana, cherry, apricot, blueberry]
Duplicate key a (attempted merging values apple and apricot)
groupingBy builds a Map<K, List<V>>; the downstream collector (counting() here) decides what
each group becomes. The TreeMap::new argument sorts the keys — a plain groupingBy returns a
HashMap with no ordering promise. toMap throws on a duplicate key unless you pass a merge
function such as (a, b) -> a.
reduce and matching
import java.util.List;
import java.util.Optional;
public class ReduceDemo {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
System.out.println(numbers.stream().reduce(0, Integer::sum));
System.out.println(numbers.stream().reduce(1, (a, b) -> a * b));
System.out.println(numbers.stream().reduce(Integer::max));
Optional<Integer> none = List.<Integer>of().stream().reduce(Integer::max);
System.out.println(none);
List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");
System.out.println(names.stream().filter(n -> n.length() > 4).count());
System.out.println(names.stream().filter(n -> n.startsWith("C")).findFirst());
System.out.println(names.stream().anyMatch(n -> n.length() > 6) + " "
+ names.stream().allMatch(n -> n.length() < 10) + " "
+ names.stream().noneMatch(String::isEmpty));
System.out.println(List.<String>of().stream().allMatch(n -> n.length() > 100));
}
}
$ java ReduceDemo.java
15
120
Optional[5]
Optional.empty
3
Optional[Charlie]
true true true
true
reduce with an identity (0 for sum, 1 for product) always returns a value; without one it
returns an Optional, which is empty for an empty stream. allMatch on an empty stream is true
— there is no element that fails — a fact that surprises people in validation code.
A realistic pipeline
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class OrderProcessor {
record Order(String id, String customer, double amount, String status) {}
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("O1", "Alice", 150.00, "PAID"),
new Order("O2", "Bob", 50.00, "PENDING"),
new Order("O3", "Alice", 200.00, "PAID"),
new Order("O4", "Charlie", 300.00, "PAID"),
new Order("O5", "Bob", 75.00, "CANCELLED"));
List<Order> paid = orders.stream().filter(o -> o.status().equals("PAID")).toList();
System.out.println("revenue: " + paid.stream().mapToDouble(Order::amount).sum());
Map<String, Double> byCustomer = paid.stream()
.collect(Collectors.groupingBy(Order::customer, TreeMap::new, Collectors.summingDouble(Order::amount)));
System.out.println("by customer: " + byCustomer);
paid.stream()
.sorted(Comparator.comparingDouble(Order::amount).reversed())
.limit(2)
.forEach(o -> System.out.println("top: " + o.id() + " " + o.amount()));
System.out.println("status counts: " + orders.stream()
.collect(Collectors.groupingBy(Order::status, TreeMap::new, Collectors.counting())));
}
}
$ java OrderProcessor.java
revenue: 650.0
by customer: {Alice=350.0, Charlie=300.0}
top: O4 300.0
top: O3 200.0
status counts: {CANCELLED=1, PAID=3, PENDING=1}
Each question is one pipeline over the same immutable list: filter to the paid orders once, then
sum, group, rank or count. The stream never modifies orders, so the pipelines cannot interfere
with each other.
Primitive streams
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.stream.IntStream;
public class PrimitiveDemo {
public static void main(String[] args) {
System.out.println(IntStream.rangeClosed(1, 100).sum());
System.out.println(IntStream.rangeClosed(1, 100).average());
System.out.println(IntStream.empty().max());
IntSummaryStatistics stats = IntStream.of(3, 1, 4, 1, 5, 9, 2, 6).summaryStatistics();
System.out.println(stats);
List<String> words = List.of("stream", "of", "words");
System.out.println(words.stream().mapToInt(String::length).max().getAsInt());
System.out.println(IntStream.range(0, 3).mapToObj(i -> "item" + i).toList());
}
}
$ java PrimitiveDemo.java
5050
OptionalDouble[50.5]
OptionalInt.empty
IntSummaryStatistics{count=8, sum=31, min=1, average=3.875000, max=9}
6
[item0, item1, item2]
IntStream, LongStream and DoubleStream work on unboxed values and add sum, average and
summaryStatistics. average and max return OptionalDouble/OptionalInt because an empty
stream has none. mapToInt enters the primitive world, mapToObj or boxed leaves it.
Practice
- From a list of sentences, produce the distinct words in lowercase, sorted alphabetically.
Solution
import java.util.Arrays;
import java.util.List;
public class DistinctWords {
public static void main(String[] args) {
List<String> sentences = List.of("The quick fox", "the lazy dog", "A quick dog");
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split("\\s+")))
.map(String::toLowerCase)
.distinct()
.sorted()
.toList();
System.out.println(words);
}
}
$ java DistinctWords.java
[a, dog, fox, lazy, quick, the]
- Find the average order amount per customer, sorted by customer name, using
groupingBywithaveragingDouble.
Solution
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class AveragePerCustomer {
record Order(String customer, double amount) {}
public static void main(String[] args) {
List<Order> orders = List.of(new Order("Bob", 50), new Order("Alice", 150),
new Order("Alice", 200), new Order("Bob", 75));
Map<String, Double> averages = orders.stream().collect(
Collectors.groupingBy(Order::customer, TreeMap::new, Collectors.averagingDouble(Order::amount)));
System.out.println(averages);
}
}
$ java AveragePerCustomer.java
{Alice=175.0, Bob=62.5}
- Count how many times each character appears in
"mississippi", keyed by character in sorted order.
Solution
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;
public class CharCount {
public static void main(String[] args) {
var counts = "mississippi".chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(Function.identity(), TreeMap::new, Collectors.counting()));
System.out.println(counts);
}
}
$ java CharCount.java
{i=4, m=1, p=2, s=4}
String.chars() returns an IntStream, so each code is cast back to char before grouping.
- Use
Stream.iterateto print the first 10 Fibonacci numbers.
Solution
import java.util.stream.Stream;
public class Fibonacci {
public static void main(String[] args) {
System.out.println(Stream.iterate(new long[] {0, 1}, f -> new long[] {f[1], f[0] + f[1]})
.limit(10)
.map(f -> f[0])
.toList());
}
}
$ java Fibonacci.java
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Each element is a pair {current, next}; map keeps only the first number.