“This is the seventh day of my participation in the First Challenge 2022.

Hello, I’m looking at the mountains.

Java8 is probably the major release in the industry, and one of the most important updates in this release is the Stream handling. This article mainly describes the use of the Collectors tool class in Stream.

The return values of methods can be used as input parameters of java.util.stream.Stream#collect to perform various operations on queues, including grouping and aggregation. The official document gives some examples:

Implementations of {@link Collector} that implement various useful reduction operations, such as accumulating elements into collections, summarizing elements according to various criteria, etc.

The following are examples of using the predefined collectors to perform common mutable reduction tasks:

// Accumulate names into a List
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());

// Accumulate names into a TreeSet
Set<String> set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));

// Convert elements to strings and concatenate them, separated by commas
String joined = things.stream()
        .map(Object::toString)
        .collect(Collectors.joining(", "));

// Compute sum of salaries of employee
int total = employees.stream()
        .collect(Collectors.summingInt(Employee::getSalary)));

// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment));

// Compute sum of salaries by department
Map<Department, Integer> totalByDept = employees.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.summingInt(Employee::getSalary)));

// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream()
        .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

Copy the code

Defining sample data

Define the object to be manipulated, a versatile Student class (using Lombok) :

@Data
@AllArgsConstructor
public class Student {
    private String id;
    private String name;
    private LocalDate birthday;
    private int age;
    private double score;
}
Copy the code

Then define a set of test data:

final List<Student> students = Lists.newArrayList();
students.add(new Student("1"."Zhang", LocalDate.of(2009, Month.JANUARY, 1), 12.12.123));
students.add(new Student("2"."Bill", LocalDate.of(2010, Month.FEBRUARY, 2), 11.22.123));
students.add(new Student("3"."Fifty", LocalDate.of(2011, Month.MARCH, 3), 10.32.123));
Copy the code

Data statistics

Number of elements: counting

Count the number of elements in the result of aggregation:

/ / 3
students.stream().collect(Collectors.counting())
Copy the code

Average: averagingDouble, averagingInt, averagingLong

These methods calculate the average value of aggregate elements. The difference is that the input parameter needs to be of the corresponding type.

For example, to average the student’s scores, we need to use averagingDouble without converting the scores:

/ / 22.123
students.stream().collect(Collectors.averagingDouble(Student::getScore))
Copy the code

If the conversion accuracy is considered, it can also be achieved:

/ / 22.0
students.stream().collect(Collectors.averagingInt(s -> (int)s.getScore()))
/ / 22.0
students.stream().collect(Collectors.averagingLong(s -> (long)s.getScore()))
Copy the code

If we want to find the average age of the students, since age is int, we can use either function arbitrarily:

/ / 11.0
students.stream().collect(Collectors.averagingInt(Student::getAge))
/ / 11.0
students.stream().collect(Collectors.averagingDouble(Student::getAge))
/ / 11.0
students.stream().collect(Collectors.averagingLong(Student::getAge))
Copy the code

Note that all three methods return a Double.

And: summingDouble, summingInt, summingLong

These three methods are similar to the above average method, but also need to pay attention to the type of the element, when the need to cast, need to cast:

/ / 66
students.stream().collect(Collectors.summingInt(s -> (int)s.getScore()))
/ / 66.369
students.stream().collect(Collectors.summingDouble(Student::getScore))
/ / 66
students.stream().collect(Collectors.summingLong(s -> (long)s.getScore()))
Copy the code

But for types that do not require casts, feel free to use either function:

/ / 33
students.stream().collect(Collectors.summingInt(Student::getAge))
/ / 33.0
students.stream().collect(Collectors.summingDouble(Student::getAge))
/ / 33
students.stream().collect(Collectors.summingLong(Student::getAge))
Copy the code

Note: summingDouble returns Double, summingInt returns Integer, and summingLong returns Long.

Max/min elements: maxBy, minBy

As the name implies, these two functions find the maximum/minimum element in the specified comparator of the aggregate element. For example, find the oldest/youngest Student object:

// Optional[Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123)
students.stream().collect(Collectors.minBy(Comparator.comparing(Student::getAge)))
// Optional[Student(id=1, name= 3, birthday=2009-01-01, age=12, score=12.123)
students.stream().collect(Collectors.maxBy(Comparator.comparing(Student::getAge)))
Copy the code

From the source can be seen, these two methods are the author to the welfare, used to improve the results of the data statistics. Inside are the classes that encapsulate the Reducing methods and BinaryOperator utility classes, which we’ll cover below.

public static<T> Collector<T, ? , Optional<T>> maxBy(Comparator<?super T> comparator) {
    return reducing(BinaryOperator.maxBy(comparator));
}

public static<T> Collector<T, ? , Optional<T>> minBy(Comparator<?super T> comparator) {
    return reducing(BinaryOperator.minBy(comparator));
}
Copy the code

SummarizingDouble, summarizingInt, and summarizingLong

Since it is a data operation, basically can not escape counting, draw, sum, maximum, minimum these several, so the author is also very thoughtful implementation of a group of aggregated data statistics methods.

This set of methods is similar to summing and averaging in that you need to pay attention to the method type. For example, to count by score, a cast is required:

// IntSummaryStatistics{count=3, sum=66, min=12, average=22.000000, Max =32}
students.stream().collect(Collectors.summarizingInt(s -> (int) s.getScore()))
DoubleSummaryStatistics{count=3, sum=66.369000, min=12.123000, average=22.123000, Max =32.123000}
students.stream().collect(Collectors.summarizingDouble(Student::getScore))
// LongSummaryStatistics{count=3, sum=66, min=12, average=22.000000, Max =32}
students.stream().collect(Collectors.summarizingLong(s -> (long) s.getScore()))
Copy the code

If you use age statistics, three methods apply:

// IntSummaryStatistics{count=3, sum=33, min=10, average=11.000000, Max =12}
students.stream().collect(Collectors.summarizingInt(Student::getAge))
DoubleSummaryStatistics{count=3, sum=33.000000, min=10.000000, average=11.000000, Max =12.000000}
students.stream().collect(Collectors.summarizingDouble(Student::getAge))
// LongSummaryStatistics{count=3, sum=33, min=10, average=11.000000, Max =12}
students.stream().collect(Collectors.summarizingLong(Student::getAge))
Copy the code

Note: SummarizingDouble returns DoubleSummaryStatistics, summarizingInt returns IntSummaryStatistics, SummarizingLong Returns the LongSummaryStatistics type.

Aggregation, grouping

Aggregate elements: toList, toSet, toCollection

These are simple functions that rewrap the aggregated elements into a queue and return them. For example, to get a list of all Student ids, just use different methods depending on the type of result you want:

// List: [1, 2, 3]
final List<String> idList = students.stream().map(Student::getId).collect(Collectors.toList());
// Set: [1, 2, 3]
final Set<String> idSet = students.stream().map(Student::getId).collect(Collectors.toSet());
// TreeSet: [1, 2, 3]
final Collection<String> idTreeSet = students.stream().map(Student::getId).collect(Collectors.toCollection(TreeSet::new));
Copy the code

Note: toList returns a List subclass, toSet returns a Set subclass, and toCollection returns a Collection subclass. As we all know, a Collection subclass includes List, Set, and many other subclasses, so toCollection is more flexible.

Aggregate elements: toMap, toConcurrentMap

What these two methods do is reassemble the aggregate elements into a Map structure, a K-V structure. ToConcurrentMap returns ConcurrentMap, that is, toConcurrentMap returns a thread-safe Map structure.

For example, we need to aggregate the id of Student:

// {1=Student(id=1, name= zhang, score=1), 2=Student(id=2, name= Li, birthday=2009-01-01, age=12, score=12.123), 3 =Student(id=2, name= Li, birthday=2010-02-02, Age =11, score=22.123), 3=Student(id=3, name= 5, birthday=2011-03-03, age=10, score=32.123)}
final Map<String, Student> map11 = students.stream()
    .collect(Collectors.toMap(Student::getId, Function.identity()));
Copy the code

However, if there is a Duplicate id, throws Java. Lang. An IllegalStateException: Duplicate key exception, so to be on the safe side, we need to use toMap another overloaded methods:

// {1=Student(id=1, name= zhang, score=1), 2=Student(id=2, name= Li, birthday=2009-01-01, age=12, score=12.123), 3 =Student(id=2, name= Li, birthday=2010-02-02, Age =11, score=22.123), 3=Student(id=3, name= 5, birthday=2011-03-03, age=10, score=32.123)}
final Map<String, Student> map2 = students.stream()
    .collect(Collectors.toMap(Student::getId, Function.identity(), (x, y) -> x));
Copy the code

As you can see, toMap has different overloaded methods that can implement more complex logic. For example, we need to get the name of the Student grouped by id:

{1= 1, 2= 2, 3= 3}
final Map<String, String> map3 = students.stream()
    .collect(Collectors.toMap(Student::getId, Student::getName, (x, y) -> x));
Copy the code

For example, we need to get the highest score set of Student objects of the same age:

// {10=Student(id=3, name= 3, score=3), 11=Student(id=2, name= 3, score=3), 11=Student(id=2, name= 3, score=3) Birthday =2010-02-02, age=11, score=22.123), 12=Student(id=1, name= zhang, birthday=2009-01-01, age=12, score=12.123)}
final Map<Integer, Student> map5 = students.stream()
    .collect(Collectors.toMap(Student::getAge, Function.identity(), BinaryOperator.maxBy(Comparator.comparing(Student::getScore))));
Copy the code

So, toMap is very playable.

Groups: groupingBy and groupingByConcurrent

Both groupingBy and toMap group aggregate elements. The difference is that the toMap result has a 1:1 K-V structure, while the groupingBy result has a 1: N K-V structure.

For example, we grouped students by age:

// List: {10=[Student(id= 2, name= 3, score=3), 11=[Student(id=2, name= 3, score=3)], 11=[Student(id=2, name= 3, score=3), Birthday =2010-02-02, age=11, score=22.123)], 12=[Student(id=1, name= zhang, age=12, score=12.123)]}
final Map<Integer, List<Student>> map1 = students.stream().collect(Collectors.groupingBy(Student::getAge));
// Set: {10=[Student(id= 2, name= 3, score=3), 11=[Student(id=2, name= 3, score=3)], 11=[Student(id=2, name= 3, score=3), Birthday =2010-02-02, age=11, score=22.123)], 12=[Student(id=1, name= zhang, age=12, score=12.123)]}
final Map<Integer, Set<Student>> map12 = students.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.toSet()));
Copy the code

Since groupingBy is also a grouping, is it possible to implement similar functionality to toMap, such as grouping students by ID:

// {1=Student(id=1, name= zhang, score=1), 2=Student(id=2, name= Li, birthday=2009-01-01, age=12, score=12.123), 3 =Student(id=2, name= Li, birthday=2010-02-02, Age =11, score=22.123), 3=Student(id=3, name= 5, birthday=2011-03-03, age=10, score=32.123)}
final Map<String, Student> map3 = students.stream()
    .collect(Collectors.groupingBy(Student::getId, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
Copy the code

For comparison, here is toMap’s notation:

// {1=Student(id=1, name= zhang, score=1), 2=Student(id=2, name= Li, birthday=2009-01-01, age=12, score=12.123), 3 =Student(id=2, name= Li, birthday=2010-02-02, Age =11, score=22.123), 3=Student(id=3, name= 5, birthday=2011-03-03, age=10, score=32.123)}
final Map<String, Student> map2 = students.stream()
    .collect(Collectors.toMap(Student::getId, Function.identity(), (x, y) -> x));
Copy the code

If you want a thread-safe Map, use groupingByConcurrent.

Grouping: partitioningBy

The difference between partitioningBy and groupingBy is that partitioningBy uses Predicate assertions to separate set elements into true and false parts. For example, group by age greater than or equal to 11:

// List: {false=[Student(id=2, name= 3, score=2, birthday=2010-02-02, age=11, score=22.123), Student(id=3, name= 3, birthday=2011-03-03, Age =10, score= score)], true=[Student(id=1, name= zhang, birthday=2009-01-01, age=12, score=12.123)]}
final Map<Boolean, List<Student>> map6 = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 11));
// Set: {false=[Student(id= 2, name= 3, score=3), Student(id=2, name= 3, score=3), Student(id=2, name= 3, score=3), Age =11, score=22.123)], true=[Student(id=1, name= zhang, birthday=2009-01-01, age=12, score=12.123)]}
final Map<Boolean, Set<Student>> map7 = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 11, Collectors.toSet()));
Copy the code

Connecting data: Joining

This method aggregates elements of type String and returns them as a String, similar to java.lang.String#join, providing three different overloaded methods for different purposes. Such as:

// javagosql
Stream.of("java"."go"."sql").collect(Collectors.joining());
// java, go, sql
Stream.of("java"."go"."sql").collect(Collectors.joining(","));
// 【 Java, go, SQL 】
Stream.of("java"."go"."sql").collect(Collectors.joining(","."【"."】"));
Copy the code

Operation chain: collectingAndThen

This method, as seen in the groupingBy example, aggregates the collection once and then processes the aggregated result again using a Function defined by Function.

Take the groupingBy example:

// {1=Student(id=1, name= zhang, score=1), 2=Student(id=2, name= Li, birthday=2009-01-01, age=12, score=12.123), 3 =Student(id=2, name= Li, birthday=2010-02-02, Age =11, score=22.123), 3=Student(id=3, name= 5, birthday=2011-03-03, age=10, score=32.123)}
final Map<String, Student> map3 = students.stream()
    .collect(Collectors.groupingBy(Student::getId, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
Copy the code

The display implements a 1:1 map structure by aggregating the results into a List List and returning the 0th element of the List.

To do a more complicated one, find the Student list with the correct age data in the aggregate element:

// [], the result is empty because all the ages in the example are correctstudents.stream() .collect( Collectors.collectingAndThen(Collectors.toList(), ( list -> list.stream() .filter(s -> (LocalDate.now().getYear() - s.getBirthday().getYear()) ! = s.getAge()) .collect(Collectors.toList())) ) );Copy the code

This example is purely for the use of collectingAndThen, which can be simplified to:

students.stream() .filter(s -> (LocalDate.now().getYear() - s.getBirthday().getYear()) ! = s.getAge()) .collect(Collectors.toList());Copy the code

Aggregation after operation: Mapping

Mapping first processes data through the Function Function and then aggregates elements through the Collector method. For example, get a list of names for students:

// [Zhang SAN, Li Si, Wang Wu]
students.stream()
        .collect(Collectors.mapping(Student::getName, Collectors.toList()));
Copy the code

This calculation is similar to the java.util.stream. stream #map method:

// [Zhang SAN, Li Si, Wang Wu]
students.stream()
        .map(Student::getName)
        .collect(Collectors.toList());
Copy the code

At this point, it is clearer to use java.util.stream.stream #map.

After aggregation, action: reducing

There are three overloaded methods provided by Reducing:

  • public static

    Collector

    > Reducing (BinaryOperator

    op) : Operate directly with the BinaryOperator and return Optional

    ,>

  • public static

    Collector

    reducing(T identity, BinaryOperator

    op) : Order defaults and then operate through the BinaryOperator

    ,>

  • public static

    Collector

    reducing(U identity, Function
    mapper, BinaryOperator
    op) : Predefined defaults that operate on elements through Function and then through BinaryOperator
    ,>
    ,>

For example, calculate the total score of all students:

// Optional[66.369], note that the return type is Optional
students.stream()
        .map(Student::getScore)
        .collect(Collectors.reducing(Double::sum));
/ / 66.369
students.stream()
        .map(Student::getScore)
        .collect(Collectors.reducing(0.0, Double::sum));
/ / 66.369
students.stream()
        .collect(Collectors.reducing(0.0, Student::getScore, Double::sum));
Copy the code

Reducing and mapping are similar to the java.util.stream. stream #reduce method:

// Optional[66.369], note that the return type is Optional
students.stream().map(Student::getScore).reduce(Double::sum);
/ / 66.369
students.stream().map(Student::getScore).reduce(0.0, Double::sum);
Copy the code

When WE talked about maxBy and minBy above, we mentioned that these two functions are implemented using REDUCING.

For mapping and Reducing, refer to the concept of map-reduce in functional programming.

At the end of the article to summarize

This article describes 24 Methods defined by Java8 Stream Collectors. The flow computing logic, which relies on the Fork/Join framework, has significant performance advantages. If you don’t master these usages, you may struggle to read the code later, since Java8 is basically the industry benchmark.

Green hills never change, green waters always flow. See you next time.

Recommended reading

  • This article describes 24 operations for Java8 Stream Collectors
  • Java8 Optional 6 kinds of operations
  • Use Lambda expressions to achieve super sorting functions
  • Java8 Time Library (1) : Describes the time class and common apis in Java8
  • Java8 time library (2) : Convert Date to LocalDate or LocalDateTime
  • Java8 Time Library (3) : Start using Java8 time classes
  • Java8 time library (4) : check if the date string is valid
  • New features in Java8
  • New features in Java9

Hello, I’m looking at the mountains. Swim in the code, play to enjoy life. If this article is helpful to you, please like, bookmark, follow.