Java 8 series Stream powerful tool Collector, streamcollector
Stream series:
- The basic syntax of Stream in Java 8 series
- Java 8 series Stream powerful tool Collector
- Reconstruction and customization of Java 8 series collectors
- Universal reduce in Stream of Java 8 series
Overview
Previously we used collect (toList () to generate a list in the stream. In the actual development process, List is the data structure we often use, but sometimes we also want Stream to convert and generate other values, such as Map or set, you even want to customize the desired data structure.
Collect, also known as the Collector, is a common structure for generating complex values from a Stream. As long as you pass it to the collect method, that is, the so-called conversion method, it will generate the desired data structure. Here I have to mention that the Collectors tool library encapsulates the corresponding conversion methods in this library. Of course, the Collectors tool Library only encapsulates some common scenarios. If you have special requirements, you need to customize them.
Obviously, List is the most natural data structure generated from the stream, but sometimes people want to generate other values from the stream, such as Map or Set, or you want to customize a class to abstract what you want.
As mentioned above, the signature of the method on the stream can be used to determine whether this operation is an early value calculation operation. The reduce operation is a good example, but sometimes people want to do more.
This is the collector, a common structure that generates complex values from a stream. You only need to pass it to the collect method, and all the streams can use it.
<R, A> R collect (Collector <? Super T, A, R> collector );
<R> R collect (Supplier <R> supplier,
BiConsumer <R ,? Super T> accumulator,
BiConsumer <R, R> combiner );
Auxiliary interface Supplier
The Supplier <T> interface is a function interface that declares a get method and is mainly used to create an object that returns a specified data type.
BiConsumer
The BiConsumer <T, U> interface is a function interface that declares the accept method and does not return values. This function interface is mainly used to declare some expected operations.
At the same time, this interface defines a default method andThen. This method accepts a BiConsumer and returns a combination of BiConsumer, which performs operations in sequence. If an exception is thrown when any operation is executed, it is passed to the caller of the combined operation. If an exception is thrown when this operation is executed, the post operation (after) will not be executed ).
@FunctionalInterfacepublic interface BiConsumer<T, U> { void accept(T t, U u); default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) { Objects.requireNonNull(after); return (l, r) -> { accept(l, r); after.accept(l, r); }; }}
BinaryOperator
The BinaryOperator interface inherits from the BiFunction interface, which specifies that the parameter type and Return Value Type of the apply method are T.
@FunctionalInterfacepublic interface BinaryOperator<T> extends BiFunction<T,T,T> { public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) { Objects.requireNonNull(comparator); return (a, b) -> comparator.compare(a, b) <= 0 ? a : b; } public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) { Objects.requireNonNull(comparator); return (a, b) -> comparator.compare(a, b) >= 0 ? a : b; }}@FunctionalInterfacepublic interface BiFunction<T, U, R> { R apply(T t, U u); default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t, U u) -> after.apply(apply(t, u)); }}
Function
Funtion is a function interface that defines a conversion function and converts T to R. For example, the map method in Stream accepts this function parameter and converts T to R.
@ FunctionalInterfacepublic interface Function <T, R> {/*** Conversion Function, convert T to R */R apply (T t ); /*** returns a Function combination. First, execute before and then execute this Function. ** if both functions throw an exception in evaluation, it will be relayed to the caller of the combined function. * If before is null, NullPointerException */default <V> Function <V, R> compose (Function <? Super V ,? Extends T> before) {Objects. requireNonNull (before); return (V v)-> apply (before. apply (v);}/*** returns a combined Function. First, execute the Function, and then execute after **. If both functions throw an exception, it will be relayed to the caller of the combined function. * If after is null, NullPointerException */default <V> Function <T, V> andThen (Function <? Super R ,? Extends V> after) {Objects. requireNonNull (after); return (T t)-> after. apply (t);}/*** returns the Function */static <T> Function <T, T> identity () {return t-> t ;}}
Collector
Collector is the variable reduce operation interface of Stream. The variable reduce operation includes accumulating elements into the collection, and connecting strings using StringBuilder; Calculating statistical information related to elements, such as sum, min, max or average. Collectors (class Collectors) provides the implementation of many common variable reduction operations.
Collector <T, A, R> accepts three generic parameters and limits the Data Types of Variable-reduction operations:
- T: input element type
- A: Variable Accumulation Type of the reduction operation (usually hidden as implementation details)
- R: variable reduction operation result type
The Collector interface declares four functions. These four functions are executed together to accumulate the elements into the variable result container, and the final transformation of the results can be performed selectively.
- Supplier <A> supplier (): Create A New Result
- BiConsumer <A, T> accumulator (): add the element to the result container
- BinaryOperator <A> combiner (): combines two result containers into one result container.
- Function <A, R> finisher (): converts the result container accordingly.
In the characteristics Method of the Collector interface, you can declare related constraints on Collector.
- Set <Characteristics> characteristics ():
Characteristics is an enumeration class in Collector. It declares three attributes, including CONCURRENT, UNORDERED, and IDENTITY_FINISH, to constrain Collector attributes.
Identity and correlation Constraints
Stream can be executed in sequence, concurrently, or sequentially. To ensure that Stream can produce the same results, the collector function must meet the identity and related item constraints.
According to the identity constraint, for any part of the cumulative results, it must be combined with the empty result container to produce equivalent results. That is to say, for part of the cumulative result a called by any series of accumulators and aggregators, a must be equal to combiner. apply (a, supplier. get ()).
In Relevance constraints, split computing must produce equivalent results. That is to say, for any input elements t1 and t2, The result r1 and r2 in the following calculation must be equivalent:
A a1 = supplier.get();accumulator.accept(a1,t1);accumulator.accept(a1,t2);R r1 = finisher.apply(a1); // result without splittingA a2 = supplier.get();accumulator.accept(a2,t1);A a3 = supplier.get();accumulator.accept(a3,t2);R r2 = finisher.apply(combiner.apply(a2,a3));
Create a custom Collector
Reconstruction and customization of Java 8 series collectors
Based on Collector tool Library
Many common collectors are declared in the Collector tool library for us to quickly create a Collector. As we have learned earlier, the collector function must meet the identity constraints and related item constraints. The following constraints must be observed when you create a Collector in a library that is simplified based on Collector (for example, Stream. collect (Collector:
Convert to another set
Many Stream chain operations are mentioned above. However, we always need to generate a set of Strea, such:
- The existing code is written for the set, so you need to replace the flow with the set to pass in;
- After performing a series of chained operations on the set, you finally want to generate a value;
- When writing unit tests, you need to assert a specific set.
Some streams can be converted into a set. For example, the toList class mentioned above generates an instance of java. util. List class. Of course, there are also toSet and toCollection instances that generate the Set and Collection classes respectively.
ToList
Example:
List <Integer> collectList = Stream. of (1, 2, 3, 4 ). collect (Collectors. toList (); System. out. println ("collectList:" + collectList); // print the result // collectList: [1, 2, 3, 4]
ToSet
Example:
Set <Integer> collectSet = Stream. of (1, 2, 3, 4 ). collect (Collectors. toSet (); System. out. println ("collectSet:" + collectSet); // print the result // collectSet: [1, 2, 3, 4]
ToCollection
Generally, when creating a set, you need to call an appropriate constructor to specify the specific type of the Set:
List<Artist> artists = new ArrayList<>();
However, when you call the toList or toSet method, you do not need to specify a specific type. The Stream class library will automatically infer and generate a suitable type. Of course, sometimes we have specific requirements for the set generated by the conversion. For example, we want to generate a TreeSet instead of a type Automatically specified by the Stream class library. In this case, toCollection is used. It accepts a function as a parameter to create a set.
It is worth noting that looking at the source code of Collectors, because the accepted function parameters must inherit from Collection, that is, Collection cannot be converted to all inheritance classes, the most obvious difference is that toCollection cannot be converted to Map.
ToMap
To generate a Map, we need to call the toMap method. Because Map has the Key and Value values, the method is different from the toSet, toList, and other processing methods. ToMap should take at least two parameters, one for generating the key and the other for generating the value. The toMap method has three types of Deformation:
Note: If the Stream contains Duplicate values, Duplicate keys will occur in the Map. during runtime, an exception occurs in java. lang. IllegalStateException: Duplicate key **
Convert to value
You can use collect to convert Stream to a value. MaxBy and minBy allow users to generate a value in a specific order.
- AveragingDouble: returns the average value. The element type of Stream is double.
- AveragingInt: calculates the average value. The element type of Stream is int.
- AveragingLong: calculates the average value. The element type of Stream is long.
- Counting: Number of Stream Elements
- MaxBy: The maximum element of a Stream under a specified condition.
- MinBy: The minimum element of Stream under the specified condition.
- Compaction: reduce operation
- SummarizingDouble: Collects the status of Stream data (double), including count, min, max, sum, and average.
- SummarizingInt: Collects the status of Stream data (int), including count, min, max, sum, and average.
- SummarizingLong: Collects the status of Stream data (long), including count, min, max, sum, and average.
- SummingDouble: sum. The element type of Stream is double.
- SummingInt: sum. The element type of Stream is int.
- SummingLong: sum. The element type of Stream is long.
Example:
Optional <Integer> collectMaxBy = Stream. of (1, 2, 3, 4 ). collect (Collectors. maxBy (Comparator. comparingInt (o-> o); System. out. println ("collectMaxBy:" + collectMaxBy. get (); // print the result // collectMaxBy: 4
Split data blocks
A common operation of collect splits Stream into two sets. For a Stream of numbers, we may want to divide them into two sets. One is an even number set and the other is an odd number set. The first thing we think of is the filtering operation. through two filtering operations, we can easily meet our needs.
But there is a problem with this operation. First, two streams are required to perform two Filter Operations. Second, if the filtering operation is complex and every stream needs to perform such an operation, the code will become redundant.
Here we have to say that the partitioningBy method in the Collectors library accepts a stream and divides it into two parts: Use the Predicate object, specify the condition and determine the part of an element, return a Map to the list based on the Boolean value. Therefore, the elements in the List corresponding to "key is true" must meet the conditions specified in the Predicate object. Similarly, the elements in the List corresponding to "key is false" cannot meet the conditions specified in the Predicate object.
In this way, we can use partitioningBy to break down the Stream of numbers into odd and even sets.
Map <Boolean, List <Integer> collectParti = Stream. of (1, 2, 3, 4 ). collect (Collectors. partitioningBy (it-> it % 2 = 0); System. out. println ("collectParti:" + collectParti); // print the result // collectParti: {false = [1, 3], true = [2, 4]}
Data Group
Data grouping is a more natural data Splitting Operation. Unlike dividing data into true and false parts, you can use any value to group data.
Call the Stream collect method and pass in a collector. groupingBy accepts a classification function to group data. Just like partitioningBy, it accepts
The Predicate object divides the data into true and false parts. The classifier we use is a Function object, which is used in the same way as map operations.
Example:
Map <Boolean, List <Integer> collectGroup = Stream. of (1, 2, 3, 4 ). collect (Collectors. groupingBy (it-> it> 3); System. out. println ("collectGroup:" + collectGroup); // print the result // collectGroup: {false = [1, 2, 3], true = [4]}
Note:
Looking at the groupingBy and partitioningBy examples, their effects are the same. They all split the Stream data and return a Map. The example may bring you a misunderstanding. In fact, the two of them are completely different.
String
Sometimes, we finally generate a set of strings for the elements of Stream (String type. For example, in Stream. of ("1", "2", "3", "4"), format Stream to "1, 2, 3, 4 ".
If Stream is not used, we can implement it through for loop iteration.
ArrayList <Integer> list = new ArrayList <> (); list. add (1); list. add (2); list. add (3); list. add (4); StringBuilder sb = new StringBuilder (); for (Integer it: list) {if (sb. length ()> 0) {sb. append (",");} sb. append (it);} System. out. println (sb. toString (); // print the result // 1, 2, 3, 4
In Java 1.8, Stream can be used for implementation. Here we will use Collectors. joining to collect values in Stream. This method can be used to conveniently get a string from Stream. The joining function accepts three parameters: Allow (used to separate elements), prefix, and suffix.
Example:
String strJoin = Stream. of ("1", "2", "3", "4 "). collect (Collectors. joining (",", "[", "]"); System. out. println ("strJoin:" + strJoin); // print the result // strJoin: [1, 2, 4]
Combined Collector
We have learned that Collector is powerful and very useful. If they are combined, isn't it more powerful? As shown in the preceding example, when grouping data, we obtain the data list collectGroup: {false = [1, 2, 3] After grouping. true = [4]}. If our requirements are higher, we don't need the list after grouping, as long as we get the number of lists After grouping.
At this time, many people subconsciously think that it is better to facilitate Map, and then use list. size () to easily obtain the number of lists for each group.
// Split the data block Map <Boolean, List <Integer> collectParti = Stream. of (1, 2, 3, 4 ). collect (Collectors. partitioningBy (it-> it % 2 = 0); Map <Boolean, Integer> mapSize = new HashMap <> (); collectParti. entrySet (). forEach (entry-> mapSize. put (entry. getKey (), entry. getValue (). size (); System. out. println ("mapSize:" + mapSize); // print the result // mapSize: {false = 2, true = 2}
In the partitioningBy method, there is such a deformation:
Map <Boolean, Long> partiCount = Stream. of (1, 2, 3, 4 ). collect (Collectors. partitioningBy (it-> it. intValue () % 2 = 0, Collectors. counting (); System. out. println ("partiCount:" + partiCount); // print the result // partiCount: {false = 2, true = 2}
In the partitioningBy method, we not only pass the condition function, but also pass in the second collector to collect a subset of the final result. These collectors are called downstream collectors. The collector is a formula used to generate the final result. The downstream collector is the formula used to generate some results. The downstream collector is used in the master collector. This combination of collectors makes them more powerful in the Stream class library.
Functions customized for basic types, such as averagingInt and summarizingLong, are in fact equivalent to calling methods on special streams, and they are used as downstream collectors.
View comments