Translated from winterbe.com/posts/2014/…
Author: @ Winterbe
Welcome to pay attention to personal wechat public number: xiaha learning Java, you can get free no routine 10G interview learning materials oh, the end of the data screenshots.
Personal website: www.exception.site/java8/java8…
Stream is arguably one of the most exciting features of the new Java8 feature, eliminating the tedious for loop from the action set. However, there are many friends who are not familiar with Stream. Take a closer look at how to use it today with this translation from @winterbe.
directory
How does a Stream work?
Different types of streams
The sequence of Stream processing
Fourth, the middle operation order is so important?
5. Data stream reuse
Six, advanced operation
- 6.1 Collect
- 6.2 FlatMap
- 6.3 the Reduce
Parallel flow
Eight, epilogue
When I first read the Stream API in Java8, TO be honest, I was very confused because its name sounded very similar to InputStream and OutputStream in the Java I0 framework. But in reality, they are completely different things.
Java8 Stream uses the functional programming pattern and, as its name suggests, can be used to chain Stream operations on collections.
This article will show you how to use different types of Stream operations in Java 8. You’ll also learn about the order in which streams are processed and how the flow operations in different orders affect runtime performance.
We’ll also take a closer look at the terminal operation apis reduce, collect and flatMap, and finally take a closer look at Java8 parallel flows.
Note: If you are not familiar with Java8 lambda expressions, functional interfaces, and method references, you may want to take a look at another translation of Ha’s tutorial on New Features in Java8.
Next, let’s get down to business.
How does a Stream work?
A stream represents a collection of elements that we can perform different types of operations on to perform a calculation on those elements. This might sound like a mouthful, but let’s do it in code:
List<String> myList =
Arrays.asList("a1"."a2"."b1"."c2"."c1");
myList
.stream() / / create the stream
.filter(s -> s.startsWith("c")) // Perform filtering to filter out strings prefixed with c
.map(String::toUpperCase) // Convert to uppercase
.sorted() / / sorting
.forEach(System.out::println); // for loop print
// C1
// C2
Copy the code
We can do intermediate or terminal operations on the flow. You might ask, right? What is an intermediate operation? What is terminal operation?
- 1.: Intermediate operations return a stream again, so we can link multiple intermediate operations without a semicolon. In the picture above
filter
Filtering,map
Object conversion,sorted
Sorting is an intermediate operation. - 2.A terminal operation is an end action of a convection operation, usually a return
void
Or a non-streaming result. In the picture aboveforEach
A loop is a termination operation.
After reading the above operation, the feeling is not very like an assembly line operation.
In fact, most shunt operations support lambda expressions as arguments and, properly understood, accept the implementation of a functional interface as arguments.
Different types of streams
We can create streams from various data sources, of which the Collection Collection is the most common. Lists and sets, for example, support the stream() method to create sequential or parallel streams.
Parallel streaming is executed in a multi-threaded manner, which takes full advantage of multi-core cpus to improve performance. Parallel flows are introduced at the end of this article, and sequential flows are discussed first:
Arrays.asList("a1"."a2"."a3")
.stream() / / create the stream
.findFirst() // Find the first element
.ifPresent(System.out::println); // If so, output
// a1
Copy the code
Calling the stream() method on the collection returns a plain stream. However, instead of creating a collection to retrieve a Stream, you can do something like this:
Stream.of("a1"."a2"."a3")
.findFirst()
.ifPresent(System.out::println); // a1
Copy the code
For example, we can create a Stream from a bunch of objects with stream.of ().
In addition to regular object streams, Java 8 comes with special types of streams for handling primitive data types int, long, and double. By now, you might have guessed that they are IntStream, LongStream, and DoubleStream.
The intStream.range () method can also be used to replace the regular for loop, as follows:
IntStream.range(1.4)
.forEach(System.out::println); // for (int I = 1; i < 4; i++) {}
/ / 1
/ / 2
/ / 3
Copy the code
These primitive-type streams work in much the same way as regular object streams, but there are some slight differences:
-
Primitive type streams use their own functional interfaces, such as IntFunction instead of Function and IntPredicate instead of Predicate.
-
Primitive streams support additional terminal aggregation operations, sum() and average(), as follows:
Arrays.stream(new int[] {1.2.3})
.map(n -> 2 * n + 1) // Perform the 2*n + 1 operation on each object in the number
.average() // Find the average value
.ifPresent(System.out::println); // If the value is not null, output
/ / 5.0
Copy the code
However, occasionally we need to convert a stream of regular objects to a stream of primitive types. Intermediate operators mapToInt(), mapToLong(), and mapToDouble come in handy:
Stream.of("a1"."a2"."a3")
.map(s -> s.substring(1)) // Intercepts each string element from subscript 1
.mapToInt(Integer::parseInt) // Convert to int base type type stream
.max() // take the maximum value
.ifPresent(System.out::println); // If not null, output
/ / 3
Copy the code
If you need to replace a primitive type stream with an object stream, you can do this using mapToObj() :
IntStream.range(1.4)
.mapToObj(i -> "a" + i) // for loop 1->4, concatenate prefix A
.forEach(System.out::println); // for loop print
// a1
// a2
// a3
Copy the code
Here is a combined example where we first convert a double stream to an int stream and then install it as an object stream:
Stream.of(1.0.2.0.3.0)
.mapToInt(Double::intValue) // Double to int
.mapToObj(i -> "a" + i) // Pair values concatenate prefix A
.forEach(System.out::println); // for loop print
// a1
// a2
// a3
Copy the code
The sequence of Stream processing
Now that we’ve learned how to create different types of streams in the previous section, let’s take a closer look at the order in which data streams are executed.
Before discussing processing order, you need to make it clear that there is an important feature of intermediate operations — latency. Observe the following example code without terminal operations:
Stream.of("d2"."a2"."b1"."b3"."c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
});
Copy the code
When executing this code snippet, you might think that the “D2 “,” A2 “, “B1 “,” B3 “, “C” elements would be printed in sequence. However, when you actually do it, it doesn’t print anything.
Why is that?
The reason is that intermediate operations are performed if and only if terminal operations exist.
Don’t believe me? Next, add the forEach terminal action to the code above:
Stream.of("d2"."a2"."b1"."b3"."c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out.println("forEach: " + s));
Copy the code
Executing again, we should see the following output:
filter: d2
forEach: d2
filter: a2
forEach: a2
filter: b1
forEach: b1
filter: b3
forEach: b3
filter: c
forEach: c
Copy the code
The order of output may surprise you! In your mind’s eye, you might want to print all the filter prefix strings before printing the forEach prefix strings.
In fact, the output moves vertically along the chain. For example, when a Stream starts processing a D2 element, it actually performs a filter operation and then a forEach operation before processing the second element.
Isn’t that amazing? Why is it designed this way?
The reason is performance. This reduces the number of actual operands per element, as you can see from the following code:
Stream.of("d2"."a2"."b1"."b3"."c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); / / caps
})
.anyMatch(s -> {
System.out.println("anyMatch: " + s);
return s.startsWith("A"); // Filter out elements prefixed with A
});
// map: d2
// anyMatch: D2
// map: a2
// anyMatch: A2
Copy the code
The terminal operator anyMatch(), which prefixes any element with A, returns true and stops the loop. So it will match from d2, and then when it loops to A2, it will return true and stop the loop.
Since the chain calls to the data flow are executed vertically, map only needs to be executed twice. Instead of mapping all elements, the map is executed as few times as possible as opposed to horizontal execution.
Fourth, the middle operation order is so important?
The following example consists of two intermediate operations map and Filter, and one terminal operation forEach. Let’s look again at how these operations are performed:
Stream.of("d2"."a2"."b1"."b3"."c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); / / caps
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A"); // Filter out elements prefixed with A
})
.forEach(s -> System.out.println("forEach: " + s)); // for loop output
// map: d2
// filter: D2
// map: a2
// filter: A2
// forEach: A2
// map: b1
// filter: B1
// map: b3
// filter: B3
// map: c
// filter: C
Copy the code
From the previous section, you should have learned that map and filter are called five times forEach string in the collection, whereas forEach is called only once because only “a2” satisfies the filtering criteria.
If we change the order of the intermediate operations and move the filter to the beginning of the header, we can greatly reduce the actual number of executions:
Stream.of("d2"."a2"."b1"."b3"."c")
.filter(s -> {
System.out.println("filter: " + s)
return s.startsWith("a"); // Filter out elements prefixed with a
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); / / caps
})
.forEach(s -> System.out.println("forEach: " + s)); // for loop output
// filter: d2
// filter: a2
// map: a2
// forEach: A2
// filter: b1
// filter: b3
// filter: c
Copy the code
Map now only needs to be called once to improve performance, a trick that can be useful when there are a large number of elements in a stream.
Next, let’s add an intermediate operation called sorted to the above code:
Stream.of("d2"."a2"."b1"."b3"."c")
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2); / / sorting
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a"); // Filter out elements prefixed with a
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); / / caps
})
.forEach(s -> System.out.println("forEach: " + s)); // for loop output
Copy the code
Sorted is a stateful operation because it needs to save state during processing to sort the elements in the collection.
Execute the code above and the output looks like this:
sort: a2; d2
sort: b1; a2
sort: b1; d2
sort: b1; a2
sort: b3; b1
sort: b3; d2
sort: c; b3
sort: c; d2
filter: a2
map: a2
forEach: A2
filter: b1
filter: b3
filter: c
filter: d2
Copy the code
Yi yi yi? It’s not vertical this time. What you need to know is that sorted is performed horizontally. Therefore, in this case, sorted makes eight calls to the combination of elements in the collection. Here, we can also use the optimization technique described above to move the intermediate filter operation to the beginning:
Stream.of("d2"."a2"."b1"."b3"."c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
// filter: d2
// filter: a2
// filter: b1
// filter: b3
// filter: c
// map: a2
// forEach: A2
Copy the code
From the output above, we see that sorted is never called because the number of elements filtered has been reduced to just one, in which case sorting is not required. So performance is greatly improved.
5. Data stream reuse
A Java8 Stream cannot be reused. As soon as you invoke any terminal operation, the Stream will be 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
Copy the code
When we call the anyMatch terminal operation on stream, the stream is closed and a call to noneMatch raises an exception:
java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)
Copy the code
To overcome this limitation, we must create a new Stream chain for each terminal operation we want to perform. For example, we can wrap a Stream through Supplier and build a new Stream through get(), as shown below:
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
Copy the code
It is also a trick to get around the restriction that a stream cannot be reused by constructing a new stream.
Six, advanced operation
Streams supports a wide variety of operations, in addition to the more common intermediate operations described above, such as filter or map (see Stream Javadoc). There are more complex operations such as Collect, flatMap, and reduce. Next, let’s learn:
Most of the code examples in this section are illustrated using the following List
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString(a) {
returnname; }}// Build a Person collection
List<Person> persons =
Arrays.asList(
new Person("Max".18),
new Person("Peter".23),
new Person("Pamela".23),
new Person("David".12));
Copy the code
6.1 Collect
Collect is a very useful terminal operation that transforms elements in a stream into a different object, such as a List, Set, or Map. Collect accepts the input parameter Collector, which consists of four distinct operations: supplier, accumulator, combiner, and finisher.
What is all this? Don’t panic, it looks pretty complicated, but in most cases, you don’t need to implement the collector yourself. Because Java 8 has a variety of commonly used Collectors built into it through the Collectors class, you can simply use them.
Let’s start with a very common use case:
List<Person> filtered =
persons
.stream() / / build flow
.filter(p -> p.name.startsWith("P")) // Filter out names that begin with P
.collect(Collectors.toList()); // Generate a new List
System.out.println(filtered); // [Peter, Pamela]
Copy the code
As you can see, constructing a List from a stream is surprisingly easy. If you need to construct a Set collection, just use receipts.toset ().
The following example will group everyone by age:
Map<Integer, List<Person>> personsByAge = persons
.stream()
.collect(Collectors.groupingBy(p -> p.age)); // Group by age
personsByAge
.forEach((age, p) -> System.out.format("age %s: %s\n", age, p));
// age 18: [Max]
// age 23: [Peter, Pamela]
// age 12: [David]
Copy the code
In addition to these operations. You can also perform aggregation operations on the flow, for example, to calculate the average age of all people:
Double averageAge = persons
.stream()
.collect(Collectors.averagingInt(p -> p.age)); // Aggregate out the average age
System.out.println(averageAge); / / 19.0
Copy the code
If you also want more comprehensive statistics, the summary collector can return a special built-in statistic object. From it, we can simply calculate the minimum age, maximum age, average age, sum, and total amount.
IntSummaryStatistics ageSummary =
persons
.stream()
.collect(Collectors.summarizingInt(p -> p.age)); // Generate summary statistics
System.out.println(ageSummary);
// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, Max =23}
Copy the code
In the next example, you can concatenate all the names into a single string:
String phrase = persons
.stream()
.filter(p -> p.age >= 18) // Filter out age 18 or older
.map(p -> p.name) // Extract the name
.collect(Collectors.joining(" and "."In Germany "." are of legal age.")); // Start with In Germany, and connect the elements with are of legal age. The end of the
System.out.println(phrase);
// In Germany Max and Peter and Pamela are of legal age.
Copy the code
The input parameters to the connection collector accept delimiters and optional prefixes and suffixes.
For how to transform a stream into a Map collection, we must specify the Map’s keys and values. Note that Map keys must be unique, or IllegalStateException will be thrown.
You can optionally pass a merge function as an extra argument to avoid this exception:
Map<Integer, String> map = persons
.stream()
.collect(Collectors.toMap(
p -> p.age,
p -> p.name,
(name1, name2) -> name1 + ";" + name2)); // Concatenate values for the same key
System.out.println(map);
// {18=Max, 23=Peter; Pamela, 12=David}
Copy the code
Now that we know about these powerful built-in collectors, let’s try to build a custom collector.
For example, we want to flow all the people in the converted into a string, containing all the name of the capital, and to | segmentation. To achieve this, we need to create a new Collector through collector.of (). At the same time, we also need to pass in four components of the collector: the supplier, the accumulator, the combinator, and the terminator.
Collector<Person, StringJoiner, String> personNameCollector =
Collector.of(
() -> new StringJoiner("|"), // supplier supplier
(j, p) -> j.add(p.name.toUpperCase()), // Accumulator
(j1, j2) -> j1.merge(j2), // combiner combiner
StringJoiner::toString); // finisher
String names = persons
.stream()
.collect(personNameCollector); // Pass in a custom collector
System.out.println(names); // MAX | PETER | PAMELA | DAVID
Copy the code
Since strings in Java are final, we need the StringJoiner helper class to help us construct strings.
Initially the provider constructs a StringJointer using a delimiter.
The accumulator is used to uppercase everyone’s name and then add it to StringJointer.
The combinator merges two StringJointers into one.
Finally, the finalizer constructs the expected string from StringJointer.
6.2 FlatMap
Above we learned how to convert an object in a stream to another type, for example through a map operation. However, a Map can only Map each object to another object.
What if we wanted to convert one object to multiple other objects or do no conversion at all? This is where flatMap comes in handy.
FlatMap can transform each element of a stream into a stream of other objects. Thus, each object can be converted to zero, one or more other objects, and returned as a stream. The contents of these streams are then put into the stream returned by the flatMap.
Before we learn how to actually manipulate flatMap, let’s create two new classes to test:
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; }}Copy the code
Next, instantiate some objects using what we learned about streams above:
List<Foo> foos = new ArrayList<>();
// Create a foos collection
IntStream
.range(1.4)
.forEach(i -> foos.add(new Foo("Foo" + i)));
// Create the bars collection
foos.forEach(f ->
IntStream
.range(1.4)
.forEach(i -> f.bars.add(new Bar("Bar" + i + "< -" + f.name))));
Copy the code
We create a collection of three Foo’s, each of which contains three bars.
The flatMap input parameter takes a function that returns a stream of objects. To process each bar in foo, we need to pass in the corresponding stream:
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
Copy the code
As shown above, we have successfully converted a stream of three Foo objects into a stream of nine Bar objects.
Finally, the above code can be simplified to a single streaming operation:
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));
Copy the code
FlatMap is also available for the Optional classes introduced in Java8. The Optional flatMap operation returns an Optional or other type of object. So it can be used to avoid tedious NULL checks.
Next, let’s create a deeper object:
class Outer {
Nested nested;
}
class Nested {
Inner inner;
}
class Inner {
String foo;
}
Copy the code
To handle retrieving the lowest level of foo from the Outer object, you need to add multiple NULL checks to avoid possible NullPointerExceptions, as shown below:
Outer outer = new Outer();
if(outer ! =null&& outer.nested ! =null&& outer.nested.inner ! =null) {
System.out.println(outer.nested.inner.foo);
}
Copy the code
We can also use the Optional flatMap operation to do the same, but more elegantly:
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);
Copy the code
Each flatMap call returns the Optional wrapper of the expected object if it is not null, or the Optional wrapper class if it is not null.
For Optional, see “how to prevent null pointer exceptions” in Java8.
6.3 the Reduce
A specification operation can combine all the elements of a flow into a single result. Java 8 supports three different Reduce methods. The first defines an element in the flow as an element in the flow.
Let’s see how we can use this method to screen out the oldest person:
persons
.stream()
.reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
.ifPresent(System.out::println); // Pamela
Copy the code
The Reduce method accepts the BinaryOperator accumulation function. The function is actually two operands of the same type BiFunction. BiFunction functions the same as Function, but it takes two arguments. In the sample code, we compare the ages of two people to return the older person.
The second reduce method accepts an identity value and a BinaryOperator accumulator. This method can be used to construct a new Person with aggregated names and ages from all the other people 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
Copy the code
The third reduce method takes three arguments: the identity value, the BiFunction accumulator, and the type of combinator function BinaryOperator. Since the initial value is not of type Person, we can use this reduction function to calculate the total age of all people:
Integer ageSum = persons
.stream()
.reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2);
System.out.println(ageSum); / / 76
Copy the code
It’s 76, but what’s going on inside? Let’s print some more debug logs:
Integer ageSum = persons
.stream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
return sum1 + sum2;
});
// accumulator: sum=0; person=Max
// accumulator: sum=18; person=Peter
// accumulator: sum=41; person=Pamela
// accumulator: sum=64; person=David
Copy the code
As you can see, the accumulator function does all the work. It starts with an initial value of 0 plus the age of the first person. For the next three steps, sum will keep increasing until it reaches 76.
And so on? There seems to be something wrong! Is the combinator never called?
Let’s run the above code as a parallel stream and look at the log output:
Integer ageSum = persons
.parallelStream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
return sum1 + sum2;
});
// accumulator: sum=0; person=Pamela
// accumulator: sum=0; person=David
// accumulator: sum=0; person=Max
// accumulator: sum=0; person=Peter
// combiner: sum1=18; sum2=23
// combiner: sum1=23; sum2=12
// combiner: sum1=41; sum2=35
Copy the code
Parallel flows are executed completely differently. Here the combinator is called. In fact, since the accumulator is called in parallel, the combinator needs to be used to calculate the sum of the partial sums.
Let’s dig deeper into parallel flows in the next chapter.
Parallel flow
Streams can be executed in parallel, which can significantly improve performance when there are a large number of elements in a stream. The ForkJoinPool used at the bottom of the parallel stream is provided by the ForkJoinPool.commonPool() method. The size of the underlying thread pool is up to five – depending on the number of cores available to the CPU:
ForkJoinPool commonPool = ForkJoinPool.commonPool();
System.out.println(commonPool.getParallelism()); / / 3
Copy the code
On my machine, the default for public pool initialization is 3. You can also decrease or increase this value by setting the following JVM parameters:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=5
Copy the code
Collections support the parallelStream() method to create parallel streams of elements. Alternatively, you can convert a serial stream to a parallel stream by calling the intermediate method parallel() on an existing data stream, which is also possible.
To understand the execution behavior of parallel streams in detail, we print information about the current thread in the following sample code:
Arrays.asList("a1"."a2"."b1"."c2"."c1")
.parallelStream()
.filter(s -> {
System.out.format("filter: %s [%s]\n",
s, Thread.currentThread().getName());
return true;
})
.map(s -> {
System.out.format("map: %s [%s]\n",
s, Thread.currentThread().getName());
return s.toUpperCase();
})
.forEach(s -> System.out.format("forEach: %s [%s]\n",
s, Thread.currentThread().getName()));
Copy the code
By logging, we can gain a better understanding of which threads are used to perform streaming operations:
filter: b1 [main]
filter: a2 [ForkJoinPool.commonPool-worker-1]
map: a2 [ForkJoinPool.commonPool-worker-1]
filter: c2 [ForkJoinPool.commonPool-worker-3]
map: c2 [ForkJoinPool.commonPool-worker-3]
filter: c1 [ForkJoinPool.commonPool-worker-2]
map: c1 [ForkJoinPool.commonPool-worker-2]
forEach: C2 [ForkJoinPool.commonPool-worker-3]
forEach: A2 [ForkJoinPool.commonPool-worker-1]
map: b1 [main]
forEach: B1 [main]
filter: a1 [ForkJoinPool.commonPool-worker-3]
map: a1 [ForkJoinPool.commonPool-worker-3]
forEach: A1 [ForkJoinPool.commonPool-worker-3]
forEach: C1 [ForkJoinPool.commonPool-worker-2]
Copy the code
As you can see, a parallel stream uses all available threads in the ForkJoinPool to perform streaming operations. In a continuous run, the output may vary because the particular thread used is non-specific.
Let’s extend the above example by adding the intermediate operation sort:
Arrays.asList("a1"."a2"."b1"."c2"."c1")
.parallelStream()
.filter(s -> {
System.out.format("filter: %s [%s]\n",
s, Thread.currentThread().getName());
return true;
})
.map(s -> {
System.out.format("map: %s [%s]\n",
s, Thread.currentThread().getName());
return s.toUpperCase();
})
.sorted((s1, s2) -> {
System.out.format("sort: %s <> %s [%s]\n",
s1, s2, Thread.currentThread().getName());
return s1.compareTo(s2);
})
.forEach(s -> System.out.format("forEach: %s [%s]\n",
s, Thread.currentThread().getName()));
Copy the code
Run the code and the output looks a bit strange:
filter: c2 [ForkJoinPool.commonPool-worker-3]
filter: c1 [ForkJoinPool.commonPool-worker-2]
map: c1 [ForkJoinPool.commonPool-worker-2]
filter: a2 [ForkJoinPool.commonPool-worker-1]
map: a2 [ForkJoinPool.commonPool-worker-1]
filter: b1 [main]
map: b1 [main]
filter: a1 [ForkJoinPool.commonPool-worker-2]
map: a1 [ForkJoinPool.commonPool-worker-2]
map: c2 [ForkJoinPool.commonPool-worker-3]
sort: A2 <> A1 [main]
sort: B1 <> A2 [main]
sort: C2 <> B1 [main]
sort: C1 <> C2 [main]
sort: C1 <> B1 [main]
sort: C1 <> C2 [main]
forEach: A1 [ForkJoinPool.commonPool-worker-1]
forEach: C2 [ForkJoinPool.commonPool-worker-3]
forEach: B1 [main]
forEach: A2 [ForkJoinPool.commonPool-worker-2]
forEach: C1 [ForkJoinPool.commonPool-worker-1]
Copy the code
It looks like sort is only executed serially on the main thread. But sort in parallel flows actually uses the new Java8 method arrays.parallelsort () underneath. As explained in the official Javadoc documentation, this method is executed serially or in parallel depending on the length of the data.
If the specified length of data is less than the minimum value, it uses the corresponding arrays.sort method for sorting.
Go back to the reduce example in the previous section. We have found that combinator functions are called only in parallel streams, not in serial streams.
Let’s actually see which thread is involved:
List<Person> persons = Arrays.asList(
new Person("Max", 18),
new Person("Peter", 23),
new Person("Pamela", 23),
new Person("David", 12));
persons
.parallelStream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s [%s]\n",
sum, p, Thread.currentThread().getName());
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s [%s]\n",
sum1, sum2, Thread.currentThread().getName());
return sum1 + sum2;
});
Copy the code
With console log output, both the accumulator and combinator execute in parallel on all available threads:
accumulator: sum=0; person=Pamela; [main]
accumulator: sum=0; person=Max; [ForkJoinPool.commonPool-worker-3]
accumulator: sum=0; person=David; [ForkJoinPool.commonPool-worker-2]
accumulator: sum=0; person=Peter; [ForkJoinPool.commonPool-worker-1]
combiner: sum1=18; sum2=23; [ForkJoinPool.commonPool-worker-1]
combiner: sum1=23; sum2=12; [ForkJoinPool.commonPool-worker-2]
combiner: sum1=41; sum2=35; [ForkJoinPool.commonPool-worker-2]
Copy the code
In summary, what you need to keep in mind is that parallel streams greatly improve performance for data streams with a large number of elements. But you also need to keep in mind that some operations on parallel streams, such as reduce and Collect operations, require additional calculations (such as combined operations) that are not required for serial execution.
In addition, we learned that all parallel stream operations share the same JVM-RELATED public ForkJoinPool. So you may want to avoid writing streaming operations that are slow and slow, which may slow down the performance of other parts of your application that rely heavily on parallel streaming.
Eight, epilogue
This concludes the Java8 Stream programming guide. If you are interested in learning more about Java 8 Stream streams, I recommend reading the official documentation using Stream Javadoc. For more information on the underlying mechanics, you can also read Martin Fowlers on Collection Pipelines.
Finally, I wish you a happy study!
Free 10G interview & learning resources
How to obtain: Follow wechat official account: XiaoxuxueJava, reply “666” in the background, you can get resource links for free without any routine, the following is the catalog and some screenshots: