MacMusic  |  PcMusic  |  440 Software  |  440 Forums  |  440TV  |  Zicos
stream
Recherche

Java Stream API tutorial: How to create and use Java streams

jeudi 13 novembre 2025, 10:00 , par InfoWorld
You can think of a Java stream as a pipeline through which data flows. Instead of manually writing loops and conditionals to process a list, you tell Java what should happen to each element, and the Java Stream API takes care of how it happens internally.

A Java stream doesn’t hold data. Instead, it operates on an existing data source such as a List, Set, Map, or array. The stream applies a series of operations to the data source.

This article introduces you to Java streams. You’ll learn how to create streams from Java collections, get your first look at a stream pipeline, and see how lambdas, method references, and other functional programming elements work with Java streams. You’ll also learn how to combine collectors and optional chaining with Java streams, and when to use or not use streams in your programs.

Streams versus collections in Java

Many developers get tripped up by the difference between Java streams and Java collections:

Collections (like ArrayList or HashSet) are used for storage. They keep data in memory for you to access.

Streams are about behavior. They describe what to do with data, not how to store it.

As an analogy, consider that a collection is the cupboard holding ingredients, whereas a stream is the recipe for making them into a meal.

Streams give Java a functional and declarative feel by describing what to do instead of how to do it.

Why developers love Java streams

Java developers appreciate and use streams for a variety of reasons:

Cleaner code that replaces nested loops and conditionals.

Less boilerplate; no more manual for loops.

Readable logic—stream pipelines read like natural language.

We can begin to see these differences by comparing loops and streams.

Loops vs. streams

Streams often replace traditional loops in Java, and once you’ve started using them, it’s hard to go back. Here’s an example of a classic for loop:

List names = List.of('patrick', 'mike', 'james', 'bill');

List result = new ArrayList();
for (String name: names) {
if (name.length() > 4) {
result.add(name.toUpperCase());
}
}
Collections.sort(result);
System.out.println(result);

And here is the Java streams version:

List names = List.of('patrick', 'mike', 'james', 'bill');

List result = names.stream().filter(name -> name.length() > 4).map(String::toUpperCase).sorted().toList();

System.out.println(result);

Unlike a loop, the Stream reads almost like English: “Take the names, filter by length, convert to uppercase, sort them, then collect to a list.”

After completing, the output will be: [JAMES, PATRICK].

Creating Java streams from collections

Streams can start from many sources. Think of all the examples below as ways to “turn on the tap.”

Here’s how to create a Stream from a collection—in this case, a List of names:

List names = List.of('James', 'Bill', 'Patrick');
Stream nameStream = names.stream();

Here’s how to create a Stream from a Map:

Map idToName = Map.of(1, 'James', 2, 'Bill');
Stream entryStream = idToName.entrySet().stream();

And here is one created from an array:

String[] names = {'James', 'Bill', 'Patrick'};
Stream nameStream = Arrays.stream(names);

You can also create a Stream using Stream.of():

Stream numberStream = Stream.of(1, 2, 3, 4, 5);

Using Stream.of(), you can pass in any kind of value or object to create a Stream. It’s a simple way to quickly create a stream when you don’t already have a collection or array. Perfect for small, fixed sets of data or quick tests.

Using Stream.generate() (infinite streams)

The Stream.generate() method creates an infinite stream; it keeps producing values while the pipeline requests them:

Stream.generate(() -> 'hello').forEach(System.out::println);

This Stream never stops printing. Use limit() to control it:

Stream.generate(Math::random).limit(5).forEach(System.out::println);

Both Stream.generate() and Stream.iterate() can produce infinite sequences. Always limit or short-circuit them to avoid endless execution.

If you need to safely return an empty stream rather than null, use Stream.empty():

Stream emptyStream = Stream.empty();

This avoids null checks and makes methods returning streams safer and cleaner.

Intermediate and lazy stream operations

Streams have intermediate (lazy) and terminal (executing) operations. Together, these two types of operations form your data pipeline.

Intermediate operations (transforming on the way)

Intermediate streams operations don’t trigger execution right away. They just add steps to the recipe:

map(): Transforms each element.

filter(): Keeps only elements that match a condition.

sorted(): Arranges elements in order.

distinct(): Removes duplicates.

limit()/skip(): Trims the stream.

flatMap(): Flattens nested structures (e.g., lists of lists) into one stream.

peek(): Lets you look at elements as they pass through (great for debugging/logging, but not for side effects).

takeWhile(predicate): Keeps pulling elements until the predicate fails (like a conditional limit).

dropWhile(predicate): Skips elements while the predicate is true, then keeps the rest.

Streams are lazy

Streams prepare all their steps first (filtering, mapping, sorting), but nothing happens until a terminal operation triggers processing. This lazy evaluation makes them efficient by processing only what’s needed.

Take this stream pipeline, for example:

List names = List.of('james', 'bill', 'patrick', 'guy');
names.stream().filter(n -> n.length() > 3) // keep names longer than 3 characters.map(String::toUpperCase) // convert to uppercase.sorted(); // sort alphabetically

System.out.println('List result: ' + names);

The result will be: [james, bill, patrick, guy].

At first glance, it looks like this pipeline should:

filter out 'al' and 'bob' (since their length isn’t greater than 3),

map the rest to uppercase, and

sort them.

But in reality, the pipeline does none of that.

The reason is that streams in Java are lazy.

All those calls (filter, map, sorted) are intermediate operations.

They don’t run immediately. Instead, they “record the plan.”

The plan only runs when you add a terminal operation like.toList(), forEach(), or count().

Since there’s no terminal operation in the above code, the pipeline is discarded and the original list prints unchanged.

Terminal operations (serving the dish)

Now we can look at the second kind of stream operation. Terminal operations trigger the stream to run and produce a result:

forEach(): Do something with each element.

collect(): Gather elements into a collection.

toList(): Collect all elements into an immutable List (Java 16+).

reduce(): Fold elements into a single result (sum, product, etc.).

count(): How many items?

findFirst(): Returns the first element that matches the filtering conditions (useful when order matters).

findAny(): Returns any matching element (especially useful in parallel streams where order is not guaranteed).

toArray(): Collect results into an array.

min(Comparator) / max(Comparator): Find the smallest or largest element based on a comparator.

anyMatch(predicate): Does any element match?

allMatch(predicate): Do all elements match?

noneMatch(predicate): Do no elements match?

Here’s an example of a stream with terminal operations:

List names = List.of('james', 'bill', 'patrick', 'guy');

List result = names.stream().filter(n -> n.length() > 3).map(String::toUpperCase).sorted().toList(); // Terminal operation method triggers action here

System.out.println(result);

In this case, the output will be: [BILL, JAMES, PATRICK].

Streams are single use

Once a stream has been processed, it’s consumed and can’t be reused. A terminal operation closes the stream:

List names = List.of('James', 'Bill', 'Patrick');

Stream s = names.stream();
s.forEach(System.out::println); // OK
s.count(); // IllegalStateException — already processed

In this code, the first call pulls all data through the pipeline, and after that it’s closed. Create a new one if needed:

long count = names.stream().count(); // OK: new stream instance

Flow of a stream pipeline

To conclude this section, here is a stream pipeline with both intermediate and terminal streams operations:

List result = names.stream() // Source.filter(n -> n.length() > 3) // Intermediate operation.map(String::toUpperCase) // Intermediate operation.sorted() // Intermediate operation.toList(); // Terminal operation

Working with collectors

In addition to streams, Java 8 introduced collectors, which you can use to describe how to gather (collect) processed data.

Collecting to a list creates a new unmodifiable list of names longer than three characters. Immutable results make stream code safer and more functional:

List list = names.stream().filter(n -> n.length() > 3).toList(); // Java 16+

Here, we collect results into a set, automatically removing duplicates. Use a set when uniqueness matters more than order:

Set set = names.stream().map(String::toUpperCase).collect(Collectors.toSet());

Here, we collect to a Map, where each key is the String’s length and each value is the name itself:

Map map = names.stream().collect(Collectors.toMap(
String::length,
n -> n
));

If multiple names share the same length, a collision occurs. Handle it with a merge function:

Map safeMap = names.stream().collect(Collectors.toMap(
String::length,
n -> n,
(a, b) -> a // keep the first value if keys collide
));

Joining strings

Collectors.joining() merges all stream elements into one String using any delimiter you choose. You can use “ |”, “; ”, or even “n” to separate values however you like:

List names = List.of('Bill', 'James', 'Patrick');

String result = names.stream().map(String::toUpperCase).collect(Collectors.joining(', '));

System.out.println(result);

The output here will be: BILL, JAMES, PATRICK.

Grouping data

Collectors.groupingBy() groups elements by key (here it’s string length) and returns a Map:

List names = List.of('james', 'linus', 'john', 'bill', 'patrick');

Map grouped = names.stream().collect(Collectors.groupingBy(String::length));

The output will be: {4=[john, bill], 5=[james, linus], 7=[patrick]}.

Summarizing numbers

You can also use collectors for summarizing:

List numbers = List.of(3, 5, 7, 2, 10);

IntSummaryStatistics stats = numbers.stream().collect(Collectors.summarizingInt(n -> n));

System.out.println(stats);

The output in this case will be: IntSummaryStatistics{count=5, sum=27, min=2, average=5.4, max=10}.

Or, if you want just the average, you could do:

double avg = numbers.stream().collect(Collectors.averagingDouble(n -> n));

Functional programming with streams

Earlier, I mentioned that streams combine functional and declarative elements. Let’s look at some of the functional programming elements in streams.

Lambdas and method references

Lambdas define behavior inline, whereas method references reuse existing methods:

names.stream().filter(name -> name.length() > 3).map(String::toUpperCase).forEach(System.out::println);

map() vs. flatMap()

As a rule of thumb:

Use a map() when you have one input and want one output.

Use a flatMap() when you have one input and want many outputs (flattened).

Here is an example using map() in a stream:

List nested = List.of(
List.of('james', 'bill'),
List.of('patrick')
);

nested.stream().map(list -> list.stream()).forEach(System.out::println);

The output here will be:

java.util.stream.ReferencePipeline$Head@5ca881b5
java.util.stream.ReferencePipeline$Head@24d46ca6

There are two lines because there are two inner lists, so you need two Stream objects. Also note that hash values will vary.

Here is the same stream with flatMap():

nested.stream().flatMap(List::stream).forEach(System.out::println);

In this case, the output will be:

james
bill
patrick

For deeper nesting, use:

List deep = List.of(
List.of(List.of('James', 'Bill')),
List.of(List.of('Patrick'))
);

List flattened = deep.stream().flatMap(List::stream).flatMap(List::stream).toList();

System.out.println(flattened);

The output in this case will be: [James, Bill, Patrick].

Optional chaining

Optional chaining is another useful operation you can combine with streams:

List names = List.of('James', 'Bill', 'Patrick');

String found = names.stream().filter(n -> n.length() > 6).findFirst().map(String::toUpperCase).orElse('NOT FOUND');

System.out.println(found);

The output will be: NOT FOUND.

findFirst() returns an optional, which safely represents a value that might not exist. If nothing matches,.orElse() provides a fallback value. Methods like findAny(), min(), and max() also return optionals for the same reason.

Conclusion

The Java Stream API transforms how you handle data. You can declare what should happen—such as filtering, mapping, or sorting—while Java efficiently handles how it happens. Combining streams, collectors, and optionals makes modern Java concise, expressive, and robust. Use streams for transforming or analyzing data collections, not for indexed or heavily mutable tasks. Once you get into the flow, it’s hard to go back to traditional loops.

As you get more comfortable with the basics in this article, you can explore advanced topics like parallel streams, primitive streams, and custom collectors. And don’t forget to practice. Once you understand the code examples here, try running them and changing the code. Experimentation will help you acquire real understanding and skills.
https://www.infoworld.com/article/4078835/java-stream-api-tutorial-how-to-create-and-use-java-stream...

Voir aussi

News copyright owned by their original publishers | Copyright © 2004 - 2025 Zicos / 440Network
Date Actuelle
jeu. 13 nov. - 12:52 CET