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
- Streams do not store data; rather, they provide data from a source such as a collection, array, or IO channel.
- Streams do no modify the data source. They transform data into a new stream, for instance, when doing a filtering operation.
- Many stream operations are lazily-evaluated. This allows for automatic code optimizations and short-circuit evaluation.
- Stream can be infinite. Method such as
limit
allow us to get some result from infinite streams. - The elements of a stream can be reached only once during the life of a
stream. Like an
Iterator
, a new stream must be generated to revisit the same elements of the source. - Streams have methods, such as
forEach
andforEachOrdered
, for internal iteration of stream elements. - Streams support SQL-like operations and common functional operations, such as filter, map, reduce, find, match, and sorted.
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.
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
.
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.
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
.
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.
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.
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.
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.
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.
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.
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
.
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.
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.
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.
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.
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.
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.
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.
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.
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
In this article we covered Java streams.
Author
List all Java tutorials.