
The Java Stream API is much easier to learn when you can run one operation at a time and watch what it prints. That’s the whole idea of my StreamAPIInJava repository: every Stream operation lives in its own file, with its own main method, its own small list of data and comments that explain what’s about to happen. Open a file, press run, read the output.
- 22 runnable examples, one operation each.
- Plain Java — no Maven, no Gradle, no libraries. Any IDE with a JDK opens it.
- Java 8 or newer, since that’s when streams arrived.
Run your first example
git clone https://github.com/amirh3sam/StreamAPIInJava.git
cd StreamAPIInJava/src
javac Stream_Filter.java && java Stream_Filter
Each file is independent, so there’s no order to follow. Start with Stream_Filter, Stream_Map and Stream_Count — those three cover most of what you’ll use day to day.
Two kinds of operations
Intermediate operations return another stream, so you can chain them — and nothing runs until the chain ends. Terminal operations end the chain and produce a result; after one runs, the stream is used up.
| Intermediate (chainable) | What it does | Terminal (ends the chain) | What it does |
|---|---|---|---|
filter() |
Keeps elements that pass a test | count() |
How many are left |
map() |
Turns each element into something else | reduce() |
Folds everything into one value |
flatMap() |
Flattens a list of lists | min() / max() |
Smallest / largest |
distinct() |
Drops duplicates | findFirst() / findAny() |
One element, if there is one |
sorted() |
Sorts, naturally or with a comparator | allMatch() / anyMatch() / noneMatch() |
True or false tests |
limit() / skip() |
Keep the first n / throw away the first n | toArray() |
Back to a plain array |
peek() |
Look at each element as it passes (debugging) | collect() / forEach() |
Into a collection / do something with each |
The repo also has concat() and parallelStream(), plus written notes in src/Stream.txt.
filter: keep only what passes
From Stream_Filter.java:
List<Integer> numbers = new ArrayList<Integer>(Arrays.asList(2,1,23,40,56,100,33));
List<Integer> evenNumbers = numbers.stream()
.filter(each -> each % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 40, 56, 100]
// several conditions in one filter: numbers between 2 and 40
numbers.stream()
.filter(e -> e > 2 && e < 40)
.forEach(System.out::println); // 23, 33
forEach(each -> System.out.println(each)) and forEach(System.out::println) do exactly the same thing — the second is a method reference.
map vs flatMap
The question everyone asks. Condensed from Stream_FlatMap.java:
// map: one output for each input
List<Integer> num = new ArrayList<Integer>(Arrays.asList(1, 2, 4));
List<Integer> num2 = num.stream().map(e -> e + 2).collect(Collectors.toList());
System.out.println(num2); // [3, 4, 6]
// flatMap: each input is a whole list, and you want one flat list
List<List<Integer>> allNums = Arrays.asList(
Arrays.asList(1, 2, 3), Arrays.asList(4, 5, 6), Arrays.asList(7, 8, 9));
List<Integer> flat = allNums.stream().flatMap(e -> e.stream()).collect(Collectors.toList());
System.out.println(flat); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
reduce, and the identity trap
From Stream_Reduce.java:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b); // 15
List<Integer> newNumbers = Arrays.asList(10, 5, 8, 20, 15);
Integer max = newNumbers.stream().reduce(Integer.MIN_VALUE, Integer::max); // 20
Integer min = newNumbers.stream().reduce(Integer.MAX_VALUE, Integer::min); // 5
The first argument is the identity. One example in the file starts a sum at 3 instead of 0 — reduce(3, (x, y) -> x + y) over 1 to 6 — to show that the starting value is added in, giving 24 instead of 21. That’s worth understanding, and also worth not doing in real code: the identity is supposed to be the neutral value (0 for adding, 1 for multiplying), because on a parallel stream it can be applied once per chunk, and the answer changes with the number of chunks.
Five things to remember
- Nothing runs until the terminal operation.
filter()andmap()on their own only describe the work — which is whypeek()prints nothing without a terminal operation after it. - A stream can only be used once. A second terminal operation on the same stream throws
IllegalStateException. Make a new stream from the collection. - The original collection never changes. Streams produce a new result; assign it if you want to keep it.
min,max,findFirstandfindAnyreturn anOptional, because there may be nothing left. Prefer.orElse(...)to.get()when you’re not sure.- Parallel isn’t automatically faster. Splitting and merging costs time; on a short list a parallel stream is slower. It pays off on large data with independent work.
Why this matters for testers
Streams are everywhere in Java test code: filtering the rows a page returned, mapping web elements to their text, checking that every value in an API response passes a rule with allMatch(). If you’re learning test automation in Java, these 22 files are a quick way to get fluent — and my Java test automation framework is the place to use them.
Frequently asked questions
There’s no pom.xml — how do I open it?
That’s on purpose: it’s plain Java with no dependencies. Open the folder as a project in your IDE, select a JDK, and run any file.
findFirst or findAny?
On a normal stream they behave the same. On a parallel stream findAny can return whichever result finishes first, so it doesn’t have to wait to work out which element was really first.
Which Java version do I need?
Java 8 or anything newer.
All 22 examples: StreamAPIInJava on GitHub. Learning more than one language? See the same tasks solved in Java, Python, JavaScript and TypeScript.
