9 C
New York
Friday, November 14, 2025
Array

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



List<String> 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<String> 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<Integer, String> 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<Integer, String> 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<String> 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<Key, List<Value>>:

List<String> names = List.of("james", "linus", "john", "bill", "patrick");

Map<Integer, List<String>> 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<Integer> 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<List<String>> 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<List<List<String>>> deep = List.of(
    List.of(List.of("James", "Bill")),
    List.of(List.of("Patrick"))
);

List<String> 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<String> 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.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Stay Connected

0FansLike
0FollowersFollow
0FollowersFollow
0SubscribersSubscribe
- Advertisement -spot_img

CATEGORIES & TAGS

- Advertisement -spot_img

LATEST COMMENTS

Most Popular

WhatsApp