Java Stream API

Stream API is a major feature introduced in Java 8. It provides convenient operations on collections.

The most used class is Stream Interface. It is defined in java.util.stream package. Stream class is used to support functional-style operations on streams of elements.

Stream can only store a sequence of objects. For primitives, there are separate classes for that. like Stream, They inherit from BaseStream Interface but with primitive specific methods such as avarage, range and rangeClosed.

  • IntStream
  • LongStream
  • DeoubleStream

Stream operations are divided into intermediate and terminal operations. Intermediate operations return a new stream. Terminal operation produce a result

Intermediate Operations

  • filter()
  • map()
  • flatMap()
  • distinct()
  • sorted()
  • peek()
  • limit()
  • skip()

Terminal Operations

  • forEach()
  • forEachOrdered()
  • toArray()
  • toList()
  • reduce()
  • collect()
  • min()
  • max()
  • count()
  • anyMatch()
  • allMatch()
  • noneMatch()
  • findFirst()
  • findAny()

Stream Creation

Collection

Collection interface provides stream() method to create stream. You can also use parallelStream() method to create a parallel stream.

1
2
List<String> list = Arrays.asList("the", "quick", "brown", "fox");
Stream<String> stream = list.stream();

Array

You can also create stream from array

1
2
String[] strArray = new String[] {"the", "quick", "brown", "fox"};
Stream<String> stream = Arrays.stream(strArray);

Stream.of() can be used to create stream too

1
Stream<String> stream =  Stream.of("the", "quick", "brown", "fox");

Empty Stream

use Stream.empty() method to create empty stream

1
Stream<String> emptyStream = Stream.empty();

Primitive Stream

Stream<T> can not be used for primitives. Java 8 provides special class for primitive streams - IntStream, LongStream, DoubleStream

1
2
3
IntStream intStream = IntStream.range(0, 2); // 0, 1
LongStream longStream = LongStream.rangeClosed(0, 2); // 0, 1, 2
DoubleStream doubleStream = new Random().doubles(3); // 3 doubles

Stream.builder

use Stream.builder() to build the stream

1
Stream<String> stream = Stream.<String>builder().add("the").add("quick").add("brown").add("fox").build();

Stream.iterate

use Stream.iterate() static method

1
Stream.iterate(0, i -> i+1).limit(5).forEach(System.out::println); // 0, 1, 2, 3, 4

Stream.generate

use Stream.generate()

1
Stream.generate(() -> new Random().nextInt(5)).limit(5).forEach(System.out::println);

Create Stream from File

java.nio.file.Files class provides lines() method to generate Stream from a text file.

1
2
3
4
5
6
Path path = Paths.get("/tmp/test.txt");
try (Stream<String> lines = Files.lines(path)) {
lines.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}

Merge Streams

use concat() static method to merge two Streams.

1
2
3
Stream<String> stream1 = Stream.of("Hello", "One");
Stream<String> stream2 = Stream.of("Hello", "Two");
Stream<String> mergedStream = Stream.concat(stream1, stream2);

If there are more than two Streams to merge, this is more complex. You can call multiple Stream.concat method or use the flatMap method

1
2
3
4
Stream<String> stream1 = Stream.of("Hello", "One");
Stream<String> stream2 = Stream.of("Hello", "Two");
Stream<String> stream3 = Stream.of("Hello", "Three");
Stream<String> mergedStream = Stream.of(stream1, stream2, stream3).flatMap(s -> s);

Here we first combine the 3 streams to create a Stream<Stream<String>> variable, then use flatMap method to flatten the variable and return Stream<String> variable.

Create Parallel Stream

Stream class provides a parallel() method to create a parallel stream. However, it doesn’t guarantee a performance increase. Be very careful when using parallel stream. for more info, see Think Twice Before Using Java 8 Parallel Streams by Lukas Krecan

Operations

filter

filter elements based on a criteria.

This is an intermediate operation.

1
list.stream().filter( s -> s.startsWith("q"))

map

1
Stream<R> map(Function<? super T,? extends R> mapper)

Apply function to each element in the stream.

map Post to its title.

1
2
3
List<Post> posts = List.of(post1, post2, post3);
Stream<String> titles = posts.stream()
.map(post -> post.getTitle());

There are variations if return type of the Function parameter is not an Object

  • mapToDouble
  • mapToInt
  • mapToLong

flatMap

1
Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

flatMap does a map operation and then follow by an flat operation. flat opertion avoids nested Stream<Stream<R>> structure.

This method is usually used to merge collections. We can merge multiple list of Cities into one list in a single line.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<County> counties = new ArrayList<>();

County county1 = new County("County1");
county1.addCity(new City("A1 City"));
county1.addCity(new City("A2 City"));
counties.add(county1);

County county2 = new County("County2");
county1.addCity(new City("B1 City"));
county1.addCity(new City("B2 City"));
counties.add(county2);

List<City> allCities = counties.stream()
.flatMap(county -> county.getCities().stream())
.collect(Collectors.toList());

allCities.stream().forEach(System.out::println); // prints all cities

There are variations if return type of the Function parameter is not an Object

  • flatMapToDouble
  • flatMapToInt
  • flatMapToLong

Compare map() and flatMap()

  • Both map and flatMap can be applied to a Stream<T> and they both return a Stream<R>
  • map() operation takes a Function, which should apply to each value in the stream and produce a single value for each value.
  • flatMap() operation also takes a Function, which should apply to each value in the stream and produce an arbitrary number of values.

limit

limit the number of elements

1
Stream.generate(() -> new Random().nextInt(5)).limit(5)

distinct

Returns a stream consisting of the distinct elements (according to Object.equals(Object)) of this stream.

1
list.stream().distinct().forEach(System.out::println);

sorted

sorted() method sort the elements in the stream

1
new Random().ints(5).sorted().forEach(System.out::println);

Sort String by length

1
2
Stream<String> stringStream = Stream.of("aaaa", "bb", "fff", "o");
stringStream.sorted((s1, s2) -> s1.length() - s2.length()).forEach(System.out::println);

or

1
2
Stream<String> stringStream =  Stream.of("aaaa", "bb", "fff", "o");
stringStream.sorted(Comparator.comparing(String::length)).forEach(System.out::println);

Sort String by length, reverse order.

1
2
Stream<String> stringStream =  Stream.of("aaaa", "bb", "fff", "o");
stringStream.sorted(Comparator.comparing(String::length).reversed()).forEach(System.out::println);

peek

peek performs action on each element. This is an intermediate operation.

1
2
3
4
5
Stream<String> stringStream = Stream.of("a", "c", "b");
List<String> upperCaseStringStream = stringStream
.map(String::toUpperCase)
.peek(System.out::println)
.toList();

NOTE: This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline

forEach

Performs an action for each element of this stream.

1
2
3
Stream<String> stream =  Stream.of("the", "quick", "brown", "fox");
stream.forEach(System.out::println);

forEach vs forEachOrdered

For forEach(), The behavior of this operation is explicitly nondeterministic. For parallel stream pipelines, this operation does not guarantee to respect the encounter order of the stream, as doing so would sacrifice the benefit of parallelism.

for forEachOrdered(), This operation processes the elements one at a time, in encounter order if one exists. So this operation sacrifice the benefit of parallelism for order.

forEach vs forEachOrdered

1
2
3
4
List<String> list =  Arrays.asList("the", "quick", "brown", "fox");
list.parallelStream().forEach(System.out::println);
System.out.println("---");
list.parallelStream().forEachOrdered(System.out::println);

output

1
2
3
4
5
6
7
8
9
brown
the
quick
fox
---
the
quick
brown
fox

anyMatch, allMatch and noneMatch

There are three methods you can use to find match: anyMatch(), allMatch(), nonMatch(). These are all terminal operation.

  • anyMatch() - Returns whether any elements of this stream match the provided predicate.
  • allMatch() - Returns whether all elements of this stream match the provided predicate.
  • noneMatch() - Returns whether no elements of this stream match the provided predicate.
1
2
List<String> list = Arrays.asList("ggg", "tech");
boolean hasTStart = list.stream().anyMatch(s -> s.startsWith("t"));

findFirst and findAny

findFirst returns an Optional describing the first element of this stream, or an empty Optional if the stream is empty.

findAny also returns an element but its return element is nondeterministic.

1
2
list.stream().findFirst().ifPresent(System.out::println);
list.stream().findAny().ifPresent(System.out::println);

min and max

  • min method returns the minimum value of the stream. It returns empty stream if these is no element. Similarly, max method returns the maximum value of the stream.
  • The input parameteris a Comparator.
1
2
3
List<Integer> list = Arrays.asList(1, 3, 2, 4);
Optional<Integer> min = list.stream().min((i, j) -> i.compareTo(j));
min.ifPresent(System.out::println);

count

Returns the count of elements in this stream. This is a terminal operation.

1
System.out.println(list.stream().count());

reduce

  • reduce takes an identity(initial value) and accumulator to do the reduction.
  • If the input Stream is empty, the return value will be the idenity(inital value).

Integer sum

1
int sum = IntStream.range(0, 9).reduce(0, (a,b) -> a + b);

or

1
int sum = IntStream.range(0, 9).reduce(0, Integer::sum);

BigDecimal sum

1
2
List<BigDecimal> decimals = Arrays.asList(BigDecimal.ONE, BigDecimal.ZERO, BigDecimal.valueOf(2.2));
BigDecimal sum = decimals.stream().reduce(BigDecimal.ZERO, BigDecimal::add);

reduce without initial value

  • If identity(inital value) is not provided, then reduce method will return an Optional.
  • When the input stream is empty, reduce method will return Optional.empty().
1
2
OptionalInt optionalSum = IntStream.range(0, 9).reduce( Integer::sum);
optionalSum.ifPresent(System.out::println); // 36

When input Stream is empty, optionalSum will be Optional.empty() too.

1
OptionalInt optionalSum = IntStream.empty().reduce(Integer::sum);

toArray

Returns an array containing the elements of this stream. Note that the return type of toArray is Object[].

1
2
3
Stream<String> stream = Stream.of("a", "c", "b");
Object[] objects = stream.toArray();
System.out.println(Arrays.toString(objects)); // output [a, c, b]

If you want the return array have actual types, then provide a generator function

1
2
3
Stream<String> stream = Stream.of("a", "c", "b");
String[] strArray = stream.toArray(String[]::new);
System.out.println(Arrays.toString(strArray));

toList

Accumulates the elements of this stream into a List. This method was introduced in Java 16. If this method is not available, use .collect(Collectors.toList()) instead.

1
2
Stream<String> stream = Stream.of("a", "c", "b");
List<String> strList = stream.toList();

collect methods

performs mutable reduction operation on elements of a stream.

A mutable reduction operation accumulates input elements into a mutable result container, such as a Collection or StringBuilder, as it processes the elements in the stream.

.collect(Collectors.toList())

The most common operation is to return a collection using a list collector.

1
2
3
List<String> upperCaseStringStream = stringStream
.map(String::toUpperCase)
.collect(Collectors.toList());

Note that javadoc shows Collectors.toList() method doesn’t guarantee the return List type of the Collector. To explicitly set the return collection type, use toCollection method. e.g. collect(Collectors.toCollection(ArrayList::new))

.collect(Collectors.toSet())

1
2
3
Set<Person> personSet = persons.stream()
.filter(person -> person.id > 500)
.collect(Collectors.toSet());

.collect(Collectors.toMap())

1
2
3
4
5
6
7
8
9
10
11
12
13
List<Person> persons = new ArrayList<>();
persons.add(new Person(624L, "John"));
persons.add(new Person(223L, "Bob"));
persons.add(new Person(546L, "Adams"));
persons.add(new Person(100L, "Jones"));

Map<Long, Person> personMap = persons.stream().collect(
Collectors.toMap(Person::getId, Function.identity())
);

for (Map.Entry<Long, Person> entry : personMap.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}

Here, we use Function.identity() as the value mapper. It returns the input value as the output value.

group elements

1
2
Stream<Book> books = Stream.of(new Book("t1", "teen"),new Book("s1", "sci-fi"), new Book("t2", "teen"));
Map<String, List<Book>> byCategory = books.collect(Collectors.groupingBy(Book::getCategory));

String joining

1
2
Stream<String> stringStream = Stream.of("one", "two", "three", "four");
String result = stringStream.collect(Collectors.joining(",")); // "one,two,three,four"

Miscellaneous

Map-Reduce

Map-Reduce is a very powerful programming technique in solving complex problems. With the introduction of Stream API, We can now perform Map Reduce to solve complex problems more elegantly.

Example: find average salary of full time employees

1
double average = employees.stream().filter(e -> e.isFulltime()).mapToDouble(e -> e.getSalary()).average().getAsDouble();

Note: average method is a reduce operation.

If you want to perform Map-Reduce in large scale, use parallelStream() instead of stream() method.

Filtering Null Values from a Stream

use filter method to filter out the null values in a stream

1
2
List<String> list =  Arrays.asList("the", "quick", null, "brown", null, "fox");
list.stream().filter(s -> s!=null).collect(Collectors.toList());

Alternatively, we can use Objects::nonNull to do the filter

1
2
List<String> list =  Arrays.asList("the", "quick", null, "brown", null, "fox");
list.stream().filter(Objects::nonNull).collect(Collectors.toList());

References