Intermediate Stream Operations in Java
filter, map, flatMap, sorted, distinct, limit, skip and peek. Each returns a new stream and none of them runs on its own.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
What makes an operation intermediate
An intermediate operation returns another Stream, so calls can be chained. None of them executes until a terminal operation is added.
| Operation | Purpose | Stateful |
|---|---|---|
filter | Keep matching elements | No |
map | Transform each element | No |
flatMap | Flatten nested structures | No |
peek | Observe elements as they pass | No |
distinct | Remove duplicates | Yes |
sorted | Order elements | Yes |
limit | Take the first n | Short circuiting |
skip | Discard the first n | Stateful |
takeWhile, dropWhile | Take or drop while a condition holds | Short circuiting |
mapMulti | Emit zero or more per element | No |
A stateful operation needs to see more than the current element. sorted must buffer everything before it can emit anything, which is why an infinite stream cannot be sorted.
filter
List<Note> popular = notes.stream()
.filter(Note::published)
.filter(note -> note.views() > 300)
.toList();Two filters or one combined predicate behave the same. Separate filters usually read better, and the cost is negligible because elements flow one at a time.
map
List<String> titles = notes.stream()
.map(Note::title)
.map(String::strip)
.map(String::toUpperCase)
.toList();
List<Integer> lengths = titles.stream().map(String::length).toList();
IntStream primitive = titles.stream().mapToInt(String::length); // avoids boxingmap is one to one: every input produces exactly one output, and the element type may change.
flatMap
List<List<String>> tagsPerNote = List.of(
List.of("java", "oop"),
List.of("java", "streams"),
List.of("sql"));
List<String> allTags = tagsPerNote.stream()
.flatMap(List::stream) // Stream<List<String>> becomes Stream<String>
.distinct()
.sorted()
.toList();
System.out.println(allTags); // [java, oop, sql, streams]// map would give the wrong shape
Stream<Stream<String>> wrong = tagsPerNote.stream().map(List::stream);Usemapwhen each element becomes one value, andflatMapwhen each element becomes a stream of values that should be merged into one flat stream. Splitting text into words is the classic case.
List<String> words = List.of("java streams", "sql joins").stream()
.flatMap(line -> Arrays.stream(line.split(" ")))
.toList(); // [java, streams, sql, joins]distinct
List<String> unique = Stream.of("a", "b", "a", "c", "b")
.distinct()
.toList(); // [a, b, c]Duplicates are decided by equals, so the element type must implement it sensibly. Records do this automatically.
sorted
List<Note> ordered = notes.stream()
.sorted(Comparator.comparingInt(Note::views).reversed()
.thenComparing(Note::title))
.toList();
List<String> natural = titles.stream().sorted().toList(); // needs Comparablelimit and skip
List<Note> topThree = notes.stream()
.sorted(Comparator.comparingInt(Note::views).reversed())
.limit(3)
.toList();
List<Note> page2 = notes.stream()
.skip(20)
.limit(10)
.toList(); // simple paginationlimit short circuits: once it has enough elements the pipeline stops pulling from the source, which is how an infinite stream can be used safely.
takeWhile and dropWhile
List<Integer> values = List.of(1, 3, 5, 8, 9, 11);
List<Integer> leading = values.stream()
.takeWhile(n -> n % 2 == 1)
.toList(); // [1, 3, 5] - stops at the first even value
List<Integer> rest = values.stream()
.dropWhile(n -> n % 2 == 1)
.toList(); // [8, 9, 11] - keeps everything afterThese differ from filter: they stop at the first element that fails, rather than testing every element. Introduced in Java 9.
peek
List<String> result = notes.stream()
.peek(note -> logger.fine("considering " + note.title()))
.filter(Note::published)
.map(Note::title)
.toList();peek is for debugging and logging only. It is not a hook for modifying elements, and the specification allows an implementation to skip it entirely when the result does not depend on it.
Order of operations matters
// Sorts everything, then discards most of it
notes.stream().sorted(byViews).filter(Note::published).limit(5).toList();
// Filters first, so far less is sorted
notes.stream().filter(Note::published).sorted(byViews).limit(5).toList();Put cheap, reducing operations such as filter early, and expensive stateful ones such as sorted late.
Common mistakes
- Using
mapwhereflatMapwas needed, and ending with a stream of streams. - Calling
sortedon an infinite stream. - Using
peekto modify elements or accumulate results. - Placing
sortedbeforefilterand sorting data that is about to be discarded. - Expecting
distinctto work on a class withoutequalsandhashCode. - Confusing
takeWhilewithfilter.
Best practices
- Filter early, sort late.
- Use method references where the lambda only delegates.
- Prefer primitive streams for numeric transformations.
- Keep every lambda free of side effects.
- Break a very long pipeline into named intermediate variables or methods.
Practice
- Flatten a list of lists of tags into one sorted list of unique tags.
- Explain the difference in output between
filterandtakeWhileon the list 1, 3, 5, 8, 9. - Implement pagination for page 3 with 10 items per page.
- Why is
sortedbeforefilterusually the wrong order? - Split a paragraph into unique lower case words in one pipeline.
Conclusion
Intermediate operations build the pipeline without running it. Know which are stateful, use flatMap when each element expands into many, and order operations so the cheap ones reduce the work for the expensive ones.