Streams
By the Syncfusion DevTutorials team · Last updated
Process collections with streams: create them, chain filter, map, sorted and more, finish with collect, reduce, count and match operations, and use parallel streams.
Think of a factory conveyor belt sorting fruit.
Apples roll onto the belt. One station removes the bruised ones. The next washes each apple. Another sorts them by size. At the end, a worker packs the good apples into a box.
Each station does one simple job, and the apples flow from one step to the next.
A Java stream works the same way. Your data flows through a chain of simple steps: remove what you do not want, transform each item, sort them, then collect the result.
In simple words: a stream lets you process data step by step by describing what to do, instead of writing the loop yourself.
What You Will Learn
In this lesson, you will learn:
- What a stream is and how it differs from a collection.
- How to create streams from lists, arrays, values and ranges.
- The intermediate operations
filter,map,sorted,distinct,limitandskip. - The terminal operations
collect,forEach,reduce,count,findFirst,anyMatchandallMatch. - How to build a stream pipeline from multiple steps.
- What parallel streams are and when to use them.
Start With a Working Example
Press Run.
Loop vs stream for the same job
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> marks = List.of(72, 35, 88, 28, 64, 91);
List<Integer> loopResult = new ArrayList<>();
for (int m : marks) {
if (m >= 40) {
loopResult.add(m + 5);
}
}
System.out.println("With a loop: " + loopResult);
List<Integer> streamResult = marks.stream()
.filter(m -> m >= 40)
.map(m -> m + 5)
.toList();
System.out.println("With a stream: " + streamResult);
}
}
Expected output:
With a loop: [77, 93, 69, 96]
With a stream: [77, 93, 69, 96]
Both give the same answer: keep the passing marks, then add 5 to each.
The loop says how: make a list, loop, check, add. The stream says what: filter, then map, then turn into a list. It reads almost like a sentence.
Try This
Add .sorted() between .map(...) and .toList(). The stream result comes out in order: [69, 77, 93, 96].
Introduction to Streams
A stream is a sequence of elements that you process with a chain of operations. Streams live in the java.util.stream package and were added in Java 8, together with lambdas.
A stream is not a collection:
| Collection | Stream | |
|---|---|---|
| Purpose | Stores data | Processes data |
| Holds elements? | Yes | No, they flow through it |
| Can be used again? | Yes, as often as you like | No, only once |
| Changes the source? | Can be changed with add and remove | Never changes its source |
| Style | You write the loop | You describe the steps |
Every stream has three parts:
- A source: where the elements come from, such as a list.
- Zero or more intermediate operations: steps that transform the stream, such as
filterandmap. - One terminal operation: the final step that produces a result, such as
toListorcount.
source → filter → map → sorted → collect
(list) \______ intermediate ______/ (terminal)
Streams Are Lazy
Intermediate operations do nothing on their own. They only describe the work. The work happens when a terminal operation runs.
This lets Java be clever. For example, if you only want the first match, it stops as soon as it finds one instead of processing every element.
Creating Streams
| Source | How | Example |
|---|---|---|
| A list or set | collection.stream() | names.stream() |
| Some values | Stream.of(...) | Stream.of("A", "B", "C") |
| An array | Arrays.stream(array) | Arrays.stream(prices) |
| A range of numbers | IntStream.range(a, b) | IntStream.rangeClosed(1, 5) |
| A rule that repeats | Stream.iterate(start, rule) | Stream.iterate(1, n -> n * 2) |
IntStream, LongStream and DoubleStream are special streams for primitive numbers. They have extra methods like sum() and average().
Five ways to create a stream
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
List<String> cities = List.of("Pune", "Goa", "Ooty");
System.out.println("From a list: " + cities.stream().count() + " cities");
System.out.println("From values: " + Stream.of("red", "green", "blue").toList());
int[] prices = {120, 45, 300};
System.out.println("From an array, total: " + Arrays.stream(prices).sum());
System.out.println("From a range: " + IntStream.rangeClosed(1, 5).boxed().toList());
System.out.println("From a rule: " + Stream.iterate(1, n -> n * 2).limit(6).toList());
}
}
Expected output:
From a list: 3 cities
From values: [red, green, blue]
From an array, total: 465
From a range: [1, 2, 3, 4, 5]
From a rule: [1, 2, 4, 8, 16, 32]
rangeClosed(1, 5) includes 5. range(1, 5) stops at 4.
boxed() turns an IntStream of int values into a normal stream of Integer objects so it can become a List.
Stream.iterate would go on forever, so limit(6) stops it after six numbers.
Intermediate Operations
An intermediate operation takes a stream and returns a new stream. Because each one returns a stream, you can chain them one after another.
filter()
filter(rule) keeps only the elements where the rule is true. The rule is a Predicate.
map()
map(rule) changes each element into something else. The rule is a Function. The new stream has the same number of elements, but they can be a different type.
filter keeps some, map changes all
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("asha", "ravi", "meena", "arjun", "divya");
List<String> startsWithA = names.stream()
.filter(n -> n.startsWith("a"))
.toList();
System.out.println("filter: " + startsWithA);
List<String> capitals = names.stream()
.map(String::toUpperCase)
.toList();
System.out.println("map to upper case: " + capitals);
List<Integer> lengths = names.stream()
.map(String::length)
.toList();
System.out.println("map to lengths: " + lengths);
}
}
Expected output:
filter: [asha, arjun]
map to upper case: [ASHA, RAVI, MEENA, ARJUN, DIVYA]
map to lengths: [4, 4, 5, 5, 5]
filter returned 2 of the 5 names. map returned all 5, each changed.
The last map turned a stream of String into a stream of Integer.
sorted()
sorted() puts the elements in natural order. sorted(comparator) uses your own rule.
distinct()
distinct() removes duplicates, keeping the first of each.
limit()
limit(n) keeps only the first n elements.
skip()
skip(n) throws away the first n elements and keeps the rest.
Together, skip and limit are handy for pages: skip(10).limit(10) gives the second page of ten items.
sorted, distinct, limit and skip
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> scores = List.of(64, 91, 72, 91, 55, 88, 72, 40);
System.out.println("sorted: " + scores.stream().sorted().toList());
System.out.println("highest first: " + scores.stream().sorted(Comparator.reverseOrder()).toList());
System.out.println("distinct: " + scores.stream().distinct().toList());
System.out.println("top 3: " + scores.stream()
.sorted(Comparator.reverseOrder())
.limit(3)
.toList());
System.out.println("skip first 5: " + scores.stream().skip(5).toList());
}
}
Expected output:
sorted: [40, 55, 64, 72, 72, 88, 91, 91]
highest first: [91, 91, 88, 72, 72, 64, 55, 40]
distinct: [64, 91, 72, 55, 88, 40]
top 3: [91, 91, 88]
skip first 5: [88, 72, 40]
distinct kept the first 91 and the first 72 in their original positions.
"Top 3" is a classic pipeline: sort from high to low, then keep the first three.
| Operation | What it does | Number of elements out |
|---|---|---|
filter(rule) | Keeps elements that pass the rule | Same or fewer |
map(rule) | Changes every element | Same |
sorted() | Puts elements in order | Same |
distinct() | Removes duplicates | Same or fewer |
limit(n) | Keeps the first n | At most n |
skip(n) | Drops the first n | Fewer |
Terminal Operations
A terminal operation ends the stream and produces a result: a collection, a number, a boolean, or an action like printing.
After a terminal operation, the stream is used up. Using it again throws an IllegalStateException.
collect()
collect(collector) gathers the elements into a collection or other result. The Collectors class provides ready-made collectors:
| Collector | Gives you |
|---|---|
Collectors.toList() | A List |
Collectors.toSet() | A Set |
Collectors.joining(", ") | One String, with a separator between elements |
Collectors.groupingBy(rule) | A Map from each group to its elements |
Collectors.counting() | The number of elements (used inside groupingBy) |
Since Java 16, stream.toList() is a shorter way to collect into a list that cannot be changed.
forEach()
forEach(action) runs an action on every element. It returns nothing. It is usually used for printing. Read the code, then look at the output below it.
collect into a list, a String and groups
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> fruits = List.of("apple", "banana", "avocado", "cherry", "blueberry", "apricot");
List<String> longOnes = fruits.stream()
.filter(f -> f.length() > 6)
.collect(Collectors.toList());
System.out.println("Long names: " + longOnes);
String line = fruits.stream()
.map(String::toUpperCase)
.collect(Collectors.joining(" | "));
System.out.println("Joined: " + line);
Map<Character, Long> byLetter = fruits.stream()
.collect(Collectors.groupingBy(f -> f.charAt(0), TreeMap::new, Collectors.counting()));
System.out.println("Count by first letter: " + byLetter);
fruits.stream()
.filter(f -> f.startsWith("b"))
.forEach(f -> System.out.println("B fruit: " + f));
}
}
Output:
Long names: [avocado, blueberry, apricot]
Joined: APPLE | BANANA | AVOCADO | CHERRY | BLUEBERRY | APRICOT
Count by first letter: {a=3, b=2, c=1}
B fruit: banana
B fruit: blueberry
groupingBy sorted the fruits into groups by their first letter, then counted each group. Passing TreeMap::new keeps the letters in sorted order.
reduce()
reduce combines all the elements into one value, two at a time.
reduce(startValue, (total, element) -> newTotal)
It starts with the start value, then combines it with each element in turn. Use it for totals, products, maximums and joining. Read the code, then look at the output below it.
reduce folds many values into one
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> prices = List.of(120, 45, 300, 80);
int total = prices.stream().reduce(0, (sum, p) -> sum + p);
System.out.println("Total: " + total);
int total2 = prices.stream().reduce(0, Integer::sum);
System.out.println("Total with Integer::sum: " + total2);
int biggest = prices.stream().reduce(0, Integer::max);
System.out.println("Biggest: " + biggest);
int product = List.of(2, 3, 4).stream().reduce(1, (a, b) -> a * b);
System.out.println("2 x 3 x 4 = " + product);
}
}
Output:
Total: 545
Total with Integer::sum: 545
Biggest: 300
2 x 3 x 4 = 24
For the total, reduce works like this: 0 + 120 = 120, then 120 + 45 = 165, then 165 + 300 = 465, then 465 + 80 = 545.
For a product, the start value is 1, not 0, because anything times zero is zero.
count()
count() returns how many elements reached the end of the stream, as a long.
findFirst()
findFirst() returns the first element, wrapped in an Optional.
An Optional is a box that either holds one value or is empty. It exists because the stream might have no elements at all. Use orElse(fallback) to get the value, or a fallback if the box is empty.
anyMatch()
anyMatch(rule) returns true if at least one element passes the rule.
allMatch()
allMatch(rule) returns true if every element passes the rule.
There is also noneMatch(rule), which returns true if no element passes.
count, findFirst, anyMatch and allMatch
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> ages = List.of(14, 22, 17, 35, 19);
long adults = ages.stream().filter(a -> a >= 18).count();
System.out.println("Adults: " + adults);
int firstAdult = ages.stream().filter(a -> a >= 18).findFirst().orElse(-1);
System.out.println("First adult's age: " + firstAdult);
int firstSenior = ages.stream().filter(a -> a >= 60).findFirst().orElse(-1);
System.out.println("First senior's age: " + firstSenior);
System.out.println("Any under 15? " + ages.stream().anyMatch(a -> a < 15));
System.out.println("All over 10? " + ages.stream().allMatch(a -> a > 10));
System.out.println("All adults? " + ages.stream().allMatch(a -> a >= 18));
}
}
Expected output:
Adults: 3
First adult's age: 22
First senior's age: -1
Any under 15? true
All over 10? true
All adults? false
There is no age of 60 or more, so findFirst() returned an empty Optional, and orElse(-1) gave -1.
anyMatch and allMatch stop early. anyMatch stops at the first match. allMatch stops at the first failure.
| Operation | Returns | Example result |
|---|---|---|
collect(...) / toList() | A collection or other result | [22, 35, 19] |
forEach(action) | Nothing | Prints lines |
reduce(start, rule) | One combined value | 545 |
count() | A long | 3 |
findFirst() | An Optional | Optional[22] |
anyMatch(rule) | boolean | true |
allMatch(rule) | boolean | false |
A stream can only be used once. After a terminal operation, calling another operation on the same stream throws IllegalStateException: stream has already been operated upon or closed. Create a new stream with list.stream() each time.
Stream Pipelines
A pipeline is the whole chain: a source, some intermediate operations, and one terminal operation.
Good pipelines read top to bottom like a recipe. Put each step on its own line, starting with a dot.
A full pipeline over a list of orders
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("Asha", "Books", 450, true),
new Order("Ravi", "Phones", 18000, true),
new Order("Meena", "Books", 300, false),
new Order("Arjun", "Shoes", 2200, true),
new Order("Divya", "Phones", 15500, true),
new Order("Kiran", "Shoes", 1800, true));
String bigBuyers = orders.stream()
.filter(Order::paid)
.filter(o -> o.amount() > 1000)
.sorted((a, b) -> b.amount() - a.amount())
.map(Order::customer)
.collect(Collectors.joining(", "));
System.out.println("Paid orders over 1000, biggest first: " + bigBuyers);
int phoneRevenue = orders.stream()
.filter(o -> o.category().equals("Phones"))
.mapToInt(Order::amount)
.sum();
System.out.println("Phone revenue: " + phoneRevenue);
double averagePaid = orders.stream()
.filter(Order::paid)
.mapToInt(Order::amount)
.average()
.orElse(0);
System.out.println("Average paid order: " + averagePaid);
}
}
record Order(String customer, String category, int amount, boolean paid) {
}
Expected output:
Paid orders over 1000, biggest first: Ravi, Divya, Arjun, Kiran
Phone revenue: 33500
Average paid order: 7590.0
The first pipeline has five steps, and each does one simple thing: keep paid orders, keep big ones, sort biggest first, take the customer's name, join the names.
mapToInt turns the stream into an IntStream, which has sum() and average().
How Elements Flow
Elements do not go through each step as a whole batch. Each element travels down the whole pipeline before the next one starts. That is why findFirst and limit can stop early.
Watch each element travel through the steps
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> result = List.of("pen", "notebook", "bag", "calculator").stream()
.filter(s -> {
System.out.println("filter: " + s);
return s.length() > 3;
})
.map(s -> {
System.out.println(" map: " + s);
return s.toUpperCase();
})
.limit(1)
.toList();
System.out.println("Result: " + result);
}
}
Expected output:
filter: pen
filter: notebook
map: notebook
Result: [NOTEBOOK]
pen failed the filter, so it stopped there. notebook passed both steps. Then limit(1) was satisfied, so bag and calculator were never even looked at.
Easy Way to Remember: a pipeline is from → what to keep → how to change → what to give back. Read each line as one step of a recipe.
Parallel Streams
A parallel stream splits the work across several CPU cores at the same time, then combines the results.
You make one with list.parallelStream(), or by adding .parallel() to a stream.
long count = bigList.parallelStream()
.filter(n -> n % 3 == 0)
.count();
When Parallel Streams Help
- The data is large: many thousands of elements or more.
- Each element needs real work, like heavy calculations.
- The steps are independent: one element does not depend on another.
When They Do Not Help
- Small lists. Splitting and combining the work costs more than it saves.
- Steps that change a shared variable or collection. Several threads changing the same thing at once give wrong results.
- When the order of side effects matters.
forEachon a parallel stream runs in an unpredictable order. UseforEachOrderedif order matters.
Parallel and sequential give the same answer
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long sequential = IntStream.rangeClosed(1, 1000)
.filter(n -> n % 7 == 0)
.count();
long parallel = IntStream.rangeClosed(1, 1000)
.parallel()
.filter(n -> n % 7 == 0)
.count();
int sum = IntStream.rangeClosed(1, 100).parallel().sum();
System.out.println("Multiples of 7, sequential: " + sequential);
System.out.println("Multiples of 7, parallel: " + parallel);
System.out.println("Sum of 1 to 100 (parallel): " + sum);
}
}
Expected output:
Multiples of 7, sequential: 142
Multiples of 7, parallel: 142
Sum of 1 to 100 (parallel): 5050
Counting and summing give the same answer either way, because they do not depend on order.
On a list this small, the parallel version is not faster. The benefit only shows on large amounts of work.
The in-browser runtime in this course runs your code on a single thread, so a parallel stream here works but cannot actually run in parallel. On a real JVM with several cores, the same code splits the work across them.
| Sequential stream | Parallel stream | |
|---|---|---|
| Created with | list.stream() | list.parallelStream() or .parallel() |
| Uses | One thread | Several threads |
Element order in forEach | Predictable | Unpredictable |
| Best for | Most everyday work | Large data with heavy, independent work |
| Risk | None extra | Wrong results if steps share changing state |
Common mistakes
The following mistakes are common when working with streams. Each row shows what you see, why it happens and how to fix it.
| Symptom | Cause | Fix |
|---|---|---|
| A pipeline seems to do nothing | No terminal operation | End with toList, count, forEach or similar |
stream has already been operated upon or closed | A stream was used twice | Create a new stream for each pipeline |
| The original list is unchanged | Streams never change their source | Store the result in a new variable |
UnsupportedOperationException | Adding to a list from toList() | Copy it into new ArrayList<>(...) |
reduce always returns 0 | Start value 0 used for multiplying | Use 1 for products |
NoSuchElementException from get() on an Optional | findFirst found nothing | Use orElse(...) |
What to Remember
- A stream processes data in steps. It does not store data, and it never changes its source.
- A pipeline has a source, intermediate operations and one terminal operation. Nothing runs until the terminal operation.
filterkeeps some elements,mapchanges each one,sortedorders them,distinctremoves repeats,limitandskipcut the stream.collectandtoListgather results,forEachacts on each element,reducecombines them, andcountcounts them.findFirstreturns anOptional, andanyMatch/allMatchanswer yes-or-no questions.- A stream can be used only once.
- Parallel streams split work across cores. Use them only for large, independent work.
Next: File Handling