ZetCode

Java Stream

last modified July 8, 2024

In this article we work with Java streams.

Java stream definition

Stream is a sequence of elements from a source that supports sequential and parallel aggregate operations. The common aggregate operations are: filter, map, reduce, find, match, and sort. The source can be a collection, IO operation, or array, which provides data to a stream.

A Java collection is an in-memory data structure with all elements contained within memory while a stream is a data structure with all elements computed on demand. In contrast to collections, which are iterated explicitly (external iteration), stream operations do the iteration behind the scenes for us. Since Java 8, Java collections have a stream method that returns a stream from a collection.

The Stream interface is defined in java.util.stream package.

An operation on a stream produces a result without modifying its source.

Characteristics of a stream

Stream pipeline

A stream pipeline consists of a source, intermediate operations, and a terminal operation. Intermediate operations return a new modified stream; therefore, it is possible to chain multiple intermediate operations. Terminal operations, on the other hand, return void or a value. After a terminal operation it is not possible to work with the stream anymore. Short-circuiting a terminal operation means that the stream may terminate before all values are processed. This is useful if the stream is infinite.

Intermediate operations are lazy. They will not be invoked until the terminal operation is executed. This improves the performance when we are processing larger data streams.

Creating streams

Streams are created from various sources such as collections, arrays, strings, IO resources, or generators.

Main.java
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

void main() {

    List<String> words = List.of("pen", "coin", "desk", "chair");

    String word = words.stream().findFirst().get();
    System.out.println(word);

    Stream<String> letters = Arrays.stream(new String[]{"a", "b", "c"});
    System.out.printf("There are %d letters%n", letters.count());

    String day = "Sunday";
    IntStream istr = day.codePoints();

    String s = istr.filter(e -> e != 'n').collect(StringBuilder::new,
            StringBuilder::appendCodePoint, StringBuilder::append).toString();
    System.out.println(s);
}

In this example, we work with streams created from a list, array, and a string.

List<String> words = List.of("pen", "coin", "desk", "chair");

A list of strings is created.

String word = words.stream().findFirst().get();

With the stream method, we create a stream from a list collection. On the stream, we call the findFirst method which returns the first element of the stream. (It returns an Optional from which we fetch the value with the get method.)

Stream<String> letters = Arrays.stream(new String[]{ "a", "b", "c"});
System.out.printf("There are %d letters%n", letters.count());

We create a stream from an array. The count method of the stream returns the number of elements in the stream.

String day = "Sunday";
IntStream istr = day.codePoints();

String s = istr.filter(e -> e != 'n').collect(StringBuilder::new,
        StringBuilder::appendCodePoint, StringBuilder::append).toString();
System.out.println(s);

Here we create a stream from a string. We filter the characters and build a new string from the filtered characters.

$ java Main.java
pen
There are 3 letters
Suday

There are three Stream specializations: IntStream, DoubleStream, and LongStream.

Main.java
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;

void main() {

    IntStream integers = IntStream.rangeClosed(1, 16);
    var res = integers.average();

    if (res.isPresent()) {
        System.out.println(res.getAsDouble());
    }

    DoubleStream doubles = DoubleStream.of(2.3, 33.1, 45.3);
    doubles.forEachOrdered(System.out::println);

    LongStream longs = LongStream.range(6, 25);
    System.out.println(longs.count());
}

The example works with the three aforementioned specializations.

IntStream integers = IntStream.rangeClosed(1, 16);
System.out.println(integers.average().getAsDouble());

A stream of integers is created with the IntStream.rangeClosed method.

if (res.isPresent()) {
    System.out.println(res.getAsDouble());
}

If the value is present, we print the average to the console.

DoubleStream doubles = DoubleStream.of(2.3, 33.1, 45.3);
doubles.forEachOrdered(System.out::println);

A stream of double values is created with the DoubleStream.of method. We print the ordered list of elements to the console utilizing the forEachOrdered method.

LongStream longs = LongStream.range(6, 25);
System.out.println(longs.count());

A strem of long integers is created with the LongStream.range method. We print the count of the elements with the count method.

$ java Main.java
8.5
2.3
33.1
45.3
19

The Stream.of method returns a sequential ordered stream whose elements are the specified values.

Main.java
import java.util.Comparator;
import java.util.Optional;
import java.util.stream.Stream;

void main() {

    Stream<String> colours = Stream.of("red", "green", "blue");
    Optional<String> col = colours.skip(2).findFirst();

    col.ifPresent(System.out::println);

    Stream<Integer> nums = Stream.of(3, 4, 5, 6, 7);
    Optional<Integer> maxVal = nums.max(Comparator.naturalOrder());

    maxVal.ifPresent(System.out::println);
}

In the example, we create two streams with the Stream.of method.

Stream<String> colours = Stream.of("red", "green", "blue");

A stream of three strings is created.

Optional<String> col = colours.skip(2).findFirst();

With the skip method, we skip two elements and find the only one left with the findFirst method. The findFirst returns an Optional<String>.

col.ifPresent(System.out::println);

We print the value if it is present.

Stream<Integer> nums = Stream.of(3, 4, 5, 6, 7);
Optional<Integer> maxVal = nums.max(Comparator.naturalOrder());

maxVal.ifPresent(System.out::println);

We create a stream of integers and find its maximum number.

$ java Main.java
blue
7

Other methods to create streams are: Stream.iterate and Stream.generate.

Main.java
import java.util.Random;
import java.util.stream.Stream;

void main() {

    Stream<Integer> s1 = Stream.iterate(5, n -> n * 2).limit(10);
    s1.forEach(System.out::println);

    Stream.generate(new Random()::nextDouble)
            .map(e -> (e * 10))
            .limit(5)
            .forEach(System.out::println);
}

In the example, we create two streams with Stream.iterate and Stream.generate.

Stream<Integer> s1 = Stream.iterate(5, n -> n * 2).limit(10);
s1.forEach(System.out::println);

The Stream.iterate returns an infinite sequential ordered stream produced by iterative application of a function to an initial element. The initial element is called a seed. The second element is generated by applying the function to the first element. The third element is generated by applying the function on the second element etc.

Stream.generate(new Random()::nextDouble)
        .map(e -> (e * 10))
        .limit(5)
        .forEach(System.out::println);

A stream of five random doubles is created with the Stream.generate method. Each of the elements is multiplied by ten. In the end, we iterate over the stream and print each element to the console.

$ java Main.java
5
10
20
40
80
160
320
640
1280
2560
8.704675577530493
5.732011478196306
3.8978402578067515
3.6986033299500933
6.0976417139147205

It is possible to create a stream from a file.

Main.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

void main() throws IOException {

    Path path = Paths.get("/home/janbodnar/myfile.txt");

    try (Stream<String> stream = Files.lines(path)) {

        stream.forEach(System.out::println);
    }
}

The example reads a file and prints its contents using streams.

Path path = Paths.get("/home/janbodnar/myfile.txt");

A Path object is created with Paths.get method. A Path object is used to locate a file in a file system.

try (Stream<String> stream = Files.lines(path)) {
    ...
}

From the path, we create a stream using Files.lines method; each of the elements of the stream is a line.

stream.forEach(System.out::println);

We go through the elements of the stream and print them to the console.

Internal and external iteration

Depending on who controls the iteration process, we distinguish between external and internal iteration. External iteration, also known as active or explicit iteration, is handled by the programmer. Until Java 8, it was the only type of iteration in Java. For external iteration, we use for and while loops. Internal iteration, also called passive or implicit iteration, is controlled by the iterator itself. Internal iteration is available in Java streams.

Main.java
import java.util.Iterator;
import java.util.List;

void main() {

    var words = List.of("pen", "coin", "desk", "eye", "bottle");

    Iterator<String> it = words.iterator();

    while (it.hasNext()) {

        System.out.println(it.next());
    }
}

In the code example, we retrieve and iterator object from a list of strings. Using iterator's hasNext and next method in a while loop, we iterate over the elements of the list.

In the following example, we iterate the same list using an external iteration.

Main.java
import java.util.List;

void main() {

    var words = List.of("pen", "coin", "desk", "eye", "bottle");
    words.stream().forEach(System.out::println);
}

In the example, we create a stream from a list. We use the stream's forEach to internally iterate over the stream elements.

words.stream().forEach(System.out::println);

This can be shortened to words.forEach(System.out::println);.

Stream filtering

Filtering streams of data is one of the most important abilities of streams. The filter method is an intermediate operation which returns a stream consisting of the elements of a stream that match the given predicate. A predicate is a method that returns a boolean value.

Main.java
import java.util.Arrays;
import java.util.stream.IntStream;

void main() {

    IntStream nums = IntStream.rangeClosed(0, 25);
    int[] vals = nums.filter(e -> e > 15).toArray();

    System.out.println(Arrays.toString(vals));
}

The code example creates a stream of integers. The stream is filtered to contain only values greater than fifteen.

IntStream nums = IntStream.rangeClosed(0, 25);

With IntStream, a stream of twenty six integers is created. The rangeClose method creates a stream of integers from a bound of two values; these two values (start and end) are both included in the range.

int[] vals = nums.filter(e -> e > 15).toArray();

We pass a lambda expression (e -> e > 15) into the filter function; the expression returns true for values greater than 15. The toArray is a terminal operation that transforms a stream into an array of integers.

System.out.println(Arrays.toString(vals));

The array is printed to the console.

$ java Main.java
[16, 17, 18, 19, 20, 21, 22, 23, 24, 25]

The next example produces a list of event numbers.

Main.java
import java.util.stream.IntStream;

void main() {

    IntStream nums = IntStream.rangeClosed(0, 30);
    nums.filter(this::isEven).forEach(System.out::println);
}

boolean isEven(int e) {

    return e % 2 == 0;
}

To get even numbers from a stream, we pass an isEven method reference to the filter method.

nums.filter(this::isEven).forEach(System.out::println);

The double colon (::) operator is used to pass a method reference. The forEach method is a terminal operation that iterates over the elements of the stream. It takes a method reference to the System.out.println method.

Skipping and limiting elements

The skip(n) method skip the first n elements of the stream and the limit(m) method limits the number of elements in the stream to m.

Main.java
import java.util.stream.IntStream;

void main() {

    IntStream s = IntStream.range(0, 15);
    s.skip(3).limit(5).forEach(System.out::println);
}

The example creates a stream of fifteen integers. We skip the first three elements with the skip method and limit the number of elements to ten values.

$ java Main.java
3
4
5
6
7

Sorting elements

The sorted method sorts the elements of this stream, according to the provided Comparator.

Main.java
import java.util.Comparator;
import java.util.stream.IntStream;

void main() {

    IntStream nums = IntStream.of(4, 3, 2, 1, 8, 6, 7, 5);

    nums.boxed().sorted(Comparator.reverseOrder())
            .forEach(System.out::println);
}

The example sorts integer elements in a descending order. The boxed method converts IntStream to Stream<Integer>.

$ java Main.java
8
7
6
5
4
3
2
1

The next example shows how to compare a stream of objects.

Main.java
import java.util.Comparator;
import java.util.List;

record Car(String name, int price) {}

void main() {

    List<Car> cars = List.of(new Car("Citroen", 23000),
            new Car("Porsche", 65000), new Car("Skoda", 18000),
            new Car("Volkswagen", 33000), new Car("Volvo", 47000));

    cars.stream().sorted(Comparator.comparing(Car::price))
            .forEach(System.out::println);
}

The example sorts cars by their price.

List<Car> cars = List.of(new Car("Citroen", 23000),
    new Car("Porsche", 65000), new Car("Skoda", 18000),
    new Car("Volkswagen", 33000), new Car("Volvo", 47000));

A list of cars is created.

cars.stream().sorted(Comparator.comparing(Car::price))
    .forEach(System.out::println);

A stream is generated from a list using the stream method. We pass a reference to the Car's price method, which is used when comparing cars by their price.

$ java Main.java
Car{name=Skoda, price=18000}
Car{name=Citroen, price=23000}
Car{name=Volkswagen, price=33000}
Car{name=Volvo, price=47000}
Car{name=Porsche, price=65000}

Unique values

The distinct method returns a stream consisting of unique elements.

Main.java
import java.util.Arrays;
import java.util.stream.IntStream;

void main() {

    IntStream nums = IntStream.of(1, 1, 3, 4, 4, 6, 7, 7);
    int[] a = nums.distinct().toArray();

    System.out.println(Arrays.toString(a));
}

The example removes duplicate values from a stream of integers.

IntStream nums = IntStream.of(1, 1, 3, 4, 4, 6, 7, 7);

There are three duplicate values in the stream.

int[] a = nums.distinct().toArray();

We remove the duplicates with the distinct method.

$ java Main.java
[1, 3, 4, 6, 7]

Mapping operations

It is possible to change elements into a new stream; the original source is not modified. The map method returns a stream consisting of the results of applying the given function to the elements of a stream. The map is an itermediate operation.

Main.java
import java.util.Arrays;
import java.util.stream.IntStream;

void main() {

    IntStream nums = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8);
    int[] squares = nums.map(e -> e * e).toArray();

    System.out.println(Arrays.toString(squares));
}

We map a transformation function on each element of the stream.

int[] squares = nums.map(e -> e * e).toArray();

We apply a lamda expression (e -> e * e) on the stream: each of the elements is squared. A new stream is created which is transformed into an array with the toArray method.

$ java Main.java
[1, 4, 9, 16, 25, 36, 49, 64]

In the next example, we transform a stream of strings.

Main.java
import java.util.stream.Stream;

void main() {

    Stream<String> words = Stream.of("cardinal", "pen", "coin", "globe");
    words.map(this::capitalize).forEach(System.out::println);
}

String capitalize(String word) {

    word = word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase();
    return word;
}

We have a stream of strings. We capitalize each of the strings of the stream.

words.map(this::capitalize).forEach(System.out::println);

We pass a reference to the capitalize method to the map method.

$ java Main.java
Cardinal
Pen
Coin
Globe

Stream reductions

A reduction is a terminal operation that aggregates a stream into a type or a primitive.

Main.java
import java.util.OptionalInt;
import java.util.stream.IntStream;

void main() {

    IntStream nums = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8);
    OptionalInt maxValue = nums.max();

    if (maxValue.isPresent()) {
        System.out.printf("The maximum value is: %d%n", maxValue.getAsInt());
    }
}

Getting a maximum value from a stream of integers is a reduction operations.

OptionalInt maxValue = nums.max();

With the max method, we get the maximum element of the stream. The method returns an OptionalInt from which we get the integer using the getAsInt method later.

if (maxValue.isPresent()) {
    System.out.printf("The maximum value is: %d%n", maxValue.getAsInt());
}

We print the value if it is present.

$ java Main.java
The maximum value is: 8

A custom reduction can be created with the reduce method.

Main.java
import java.util.OptionalInt;
import java.util.stream.IntStream;

void main() {

    IntStream nums = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8);
    OptionalInt product = nums.reduce((a, b) -> a * b);
    
    if (product.isPresent()) {
        System.out.printf("The product is: %d%n", product.getAsInt());

    }
}

The example returns a product of the integer elements in the stream.

$ java Main.java
The product is: 40320

Collection operations

A collection is a terminal reduction operation which reduces elements of a stream into a Java collection, string, value, or specific grouping.

Main.java
import java.util.List;
import java.util.stream.Collectors;

record Car(String name, int price) {
}

void main() {

    List<Car> cars = List.of(new Car("Citroen", 23000),
            new Car("Porsche", 65000), new Car("Skoda", 18000),
            new Car("Volkswagen", 33000), new Car("Volvo", 47000));

    List<String> names = cars.stream().map(Car::name)
            .filter(name -> name.startsWith("Vo"))
            .collect(Collectors.toList());

    for (String name : names) {
        
        System.out.println(name);
    }
}

The example creates a stream from a list of car object, filters the cars by the their name, and returns a list of matching car names.

List<String> names = cars.stream().map(Car::name)
    .filter(name -> name.startsWith("Vo"))
    .collect(Collectors.toList());

At the end of the pipeline, we use the collect method to transform

$ java Main.java
Volkswagen
Volvo

In the next example, we use the collect method to group data.

Main.java
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

void main() {

    List<String> items = List.of("pen", "book", "pen", "coin",
            "book", "desk", "book", "pen", "book", "coin");

    Map<String, Long> result = items.stream().collect(
            Collectors.groupingBy(
                    Function.identity(), Collectors.counting()
            ));

    for (Map.Entry<String, Long> entry : result.entrySet()) {

        String key = entry.getKey();
        Long value = entry.getValue();

        System.out.format("%s: %d%n", key, value);
    }
}

The code example groups elements by their occurrence in a stream.

Map<String, Long> result = items.stream().collect(
        Collectors.groupingBy(
                Function.identity(), Collectors.counting()
        ));

With the Collectors.groupingBy method, we count the occurrences of the elements in the stream. The operation returns a map.

for (Map.Entry<String, Long> entry : result.entrySet()) {

    String key = entry.getKey();
    Long value = entry.getValue();

    System.out.format("%s: %d%n", key, value);
}

We go through the map and print its key/value pairs.

$ java Main.java
desk: 1
book: 4
pen: 3
coin: 2

Source

Java Stream documentation

In this article we covered Java streams.

Author

My name is Jan Bodnar and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.

List all Java tutorials.