Main reference
[1] http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples
1. How Streams Work
A stream represents a sequence of elements and supports different kind of operations to perform computations upon those elements:
List<String> myList =
Arrays.asList("a1", "a2", "b1", "c2", "c1"); myList
.stream()
.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println); // C1
// C2
2. Different Kinds of Streams
Streams can be created from various data sources, especially collections. Lists and Sets support new methods stream()
and parallelStream()
to either create a sequential or a parallel stream.
For Sequencial Stream
Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println); // a1
Calling the method stream()
on a list of objects returns a regular object stream. But we don't have to create collections in order to work with streams as we see in the next code sample:
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println); // a1
Just use Stream.of()
to create a stream from a bunch of object references.
Besides regular object streams Java 8 ships with special kinds of streams for working with the primitive data types int
, long
and double
. As you might have guessed it's IntStream
,LongStream
and DoubleStream
.
IntStreams can replace the regular for-loop utilizing IntStream.range()
IntStream.range(1, 4)
.forEach(System.out::println); // 1
// 2
// 3
All those primitive streams work just like regular object streams with the following differences: Primitive streams use specialized lambda expressions, e.g. IntFunction
instead ofFunction
or IntPredicate
instead of Predicate
. And primitive streams support the additional terminal aggregate operations sum()
and average()
Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println); // 5.0
Sometimes it's useful to transform a regular object stream to a primitive stream or vice versa. For that purpose object streams support the special mapping operations mapToInt()
,mapToLong()
and mapToDouble
Stream.of("a1", "a2", "a3")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println); // 3
Primitive streams can be transformed to object streams via mapToObj()
IntStream.range(1, 4)
.mapToObj(i -> "a" + i)
.forEach(System.out::println); // a1
// a2
// a3
Here's a combined example: the stream of doubles is first mapped to an int stream and than mapped to an object stream of strings:
Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue)
.mapToObj(i -> "a" + i)
.forEach(System.out::println); // a1
// a2
// a3
3. Processing Order
An important characteristic of intermediate operations is laziness.
4. Reusing Streams
Java 8 streams cannot be reused. As soon as you call any terminal operation the stream is closed
Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a")); stream.anyMatch(s -> true); // ok
stream.noneMatch(s -> true); // exception
To overcome this limitation we have to to create a new stream chain for every terminal operation we want to execute, e.g. we could create a stream supplier to construct a new stream with all intermediate operations already set up:
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a")); streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok
Each call to get()
constructs a new stream on which we are save to call the desired terminal operation.
5. Advanced Operations
Most code samples from this section use the following list of persons for demonstration purposes:
class Person {
String name;
int age; Person(String name, int age) {
this.name = name;
this.age = age;
} @Override
public String toString() {
return name;
}
} List<Person> persons =
Arrays.asList(
new Person("Max", 18),
new Person("Peter", 23),
new Person("Pamela", 23),
new Person("David", 12));
Collect
Collect is an extremely useful terminal operation to transform the elements of the stream into a different kind of result, e.g. a List
, Set
or Map
. Collect accepts a Collector
which consists of four different operations: a supplier, an accumulator, a combiner and a finisher. This sounds super complicated at first, but the good part is Java 8 supports various built-in collectors via the Collectors
class. So for the most common operations you don't have to implement a collector yourself.
List<Person> filtered =
persons
.stream()
.filter(p -> p.name.startsWith("P"))
.collect(Collectors.toList()); System.out.println(filtered); // [Peter, Pamela]
Then
Map<Integer, List<Person>> personsByAge = persons
.stream()
.collect(Collectors.groupingBy(p -> p.age)); personsByAge
.forEach((age, p) -> System.out.format("age %s: %s\n", age, p)); // age 18: [Max]
// age 23: [Peter, Pamela]
// age 12: [David]
Collectors are extremely versatile. You can also create aggregations on the elements of the stream, e.g. determining the average age of all persons:
Double averageAge = persons
.stream()
.collect(Collectors.averagingInt(p -> p.age)); System.out.println(averageAge); // 19.0
In order to transform the stream elements into a map, we have to specify how both the keys and the values should be mapped. Keep in mind that the mapped keys must be unique, otherwise an IllegalStateException
is thrown. You can optionally pass a merge function as an additional parameter to bypass the exception:
Map<Integer, String> map = persons
.stream()
.collect(Collectors.toMap(
p -> p.age,
p -> p.name,
(name1, name2) -> name1 + ";" + name2)); System.out.println(map);
// {18=Max, 23=Peter;Pamela, 12=David}
Now that we know some of the most powerful built-in collectors, let's try to build our own special collector. We want to transform all persons of the stream into a single string consisting of all names in upper letters separated by the |
pipe character. In order to achieve this we create a new collector via Collector.of()
. We have to pass the four ingredients of a collector: a supplier, an accumulator, a combiner and a finisher.
Collector<Person, StringJoiner, String> personNameCollector =
Collector.of(
() -> new StringJoiner(" | "), // supplier
(j, p) -> j.add(p.name.toUpperCase()), // accumulator
(j1, j2) -> j1.merge(j2), // combiner
StringJoiner::toString); // finisher String names = persons
.stream()
.collect(personNameCollector); System.out.println(names); // MAX | PETER | PAMELA | DAVID
Since strings in Java are immutable, we need a helper class like StringJoiner
to let the collector construct our string. The supplier initially constructs such a StringJoiner with the appropriate delimiter. The accumulator is used to add each persons upper-cased name to the StringJoiner. The combiner knows how to merge two StringJoiners into one. In the last step the finisher constructs the desired String from the StringJoiner.
FlatMap
We've already learned how to transform the objects of a stream into another type of objects by utilizing the map
operation. Map is kinda limited because every object can only be mapped to exactly one other object. But what if we want to transform one object into multiple others or none at all? This is where flatMap
comes to the rescue.
FlatMap transforms each element of the stream into a stream of other objects. So each object will be transformed into zero, one or multiple other objects backed by streams. The contents of those streams will then be placed into the returned stream of the flatMap
operation.
Before we see flatMap
in action we need an appropriate type hierarchy:
class Foo {
String name;
List<Bar> bars = new ArrayList<>(); Foo(String name) {
this.name = name;
}
} class Bar {
String name; Bar(String name) {
this.name = name;
}
}
Next, we utilize our knowledge about streams to instantiate a couple of objects:
List<Foo> foos = new ArrayList<>(); // create foos
IntStream
.range(1, 4)
.forEach(i -> foos.add(new Foo("Foo" + i))); // create bars
foos.forEach(f ->
IntStream
.range(1, 4)
.forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name))));
Now we have a list of three foos each consisting of three bars.
FlatMap accepts a function which has to return a stream of objects. So in order to resolve the bar objects of each foo, we just pass the appropriate function:
foos.stream()
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name)); // Bar1 <- Foo1
// Bar2 <- Foo1
// Bar3 <- Foo1
// Bar1 <- Foo2
// Bar2 <- Foo2
// Bar3 <- Foo2
// Bar1 <- Foo3
// Bar2 <- Foo3
// Bar3 <- Foo3
Finally, the above code example can be simplified into a single pipeline of stream operations:
IntStream.range(1, 4)
.mapToObj(i -> new Foo("Foo" + i))
.peek(f -> IntStream.range(1, 4)
.mapToObj(i -> new Bar("Bar" + i + " <- " f.name))
.forEach(f.bars::add))
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));
FlatMap is also available for the Optional
class introduced in Java 8. Optionals flatMap
operation returns an optional object of another type. So it can be utilized to prevent nastynull
checks.
Think of a highly hierarchical structure like this:
class Outer {
Nested nested;
} class Nested {
Inner inner;
} class Inner {
String foo;
}
In order to resolve the inner string foo
of an outer instance you have to add multiple null checks to prevent possible NullPointerExceptions:
Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);
}
The same behavior can be obtained by utilizing optionals flatMap
operation:
Optional.of(new Outer())
.flatMap(o -> Optional.ofNullable(o.nested))
.flatMap(n -> Optional.ofNullable(n.inner))
.flatMap(i -> Optional.ofNullable(i.foo))
.ifPresent(System.out::println);
Reduce
The reduction operation combines all elements of the stream into a single result. Java 8 supports three different kind of reduce
methods. The first one reduces a stream of elements to exactly one element of the stream.
persons
.stream()
.reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
.ifPresent(System.out::println); // Pamela
The second reduce
method accepts both an identity value and a BinaryOperator
accumulator. This method can be utilized to construct a new Person with the aggregated names and ages from all other persons in the stream:
Person result =
persons
.stream()
.reduce(new Person("", 0), (p1, p2) -> {
p1.age += p2.age;
p1.name += p2.name;
return p1;
}); System.out.format("name=%s; age=%s", result.name, result.age);
// name=MaxPeterPamelaDavid; age=76
The third reduce
method accepts three parameters: an identity value, a BiFunction
accumulator and a combiner function of type BinaryOperator
. Since the identity values type is not restricted to the Person
type, we can utilize this reduction to determine the sum of ages from all persons:
Integer ageSum = persons
.stream()
.reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2); System.out.println(ageSum); // 76
6. Parallel Streams
It can be stated that parallel streams can bring be a nice performance boost to streams with a large amount of input elements. But keep in mind that some parallel stream operations like reduce
and collect
need additional computations (combine operations) which isn't needed when executed sequentially.
Furthermore we've learned that all parallel stream operations share the same JVM-wide common ForkJoinPool
. So you probably want to avoid implementing slow blocking stream operations since that could potentially slow down other parts of your application which rely heavily on parallel streams.