Hướng dẫn về Trình thu thập của Java 8

1. Khái quát chung

Trong hướng dẫn này, chúng ta sẽ xem xét Bộ sưu tập của Java 8, được sử dụng ở bước cuối cùng của quá trình xử lý Luồng .

Nếu bạn muốn đọc thêm về chính Stream API, hãy xem bài viết này.

Nếu bạn muốn xem cách tận dụng sức mạnh của Bộ sưu tập để xử lý song song, hãy kiểm tra dự án này.

2. Phương thức Stream.collect ()

Stream.collect () là một trong những phương thức đầu cuối của API Stream của Java 8 . Nó cho phép chúng tôi thực hiện các hoạt động gấp có thể thay đổi (đóng gói lại các phần tử vào một số cấu trúc dữ liệu và áp dụng một số logic bổ sung, nối chúng, v.v.) trên các phần tử dữ liệu được giữ trong một cá thể Stream .

Chiến lược cho hoạt động này được cung cấp thông qua triển khai giao diện Collector .

3. Người sưu tầm

Tất cả các triển khai xác định trước có thể được tìm thấy trong lớp Collectors . Một thực tế phổ biến là sử dụng nhập tĩnh sau với chúng để tăng khả năng đọc:

import static java.util.stream.Collectors.*;

hoặc chỉ những người thu gom hàng nhập khẩu do bạn lựa chọn:

import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet;

Trong các ví dụ sau, chúng tôi sẽ sử dụng lại danh sách sau:

List givenList = Arrays.asList("a", "bb", "ccc", "dd");

3.1. Collectors.toList ()

Bộ sưu tập ToList có thể được sử dụng để thu thập tất cả các phần tử Luồng vào một cá thể Danh sách . Điều quan trọng cần nhớ là thực tế là chúng ta không thể giả định bất kỳ triển khai Danh sách cụ thể nào với phương pháp này. Nếu bạn muốn kiểm soát nhiều hơn điều này, hãy sử dụng toCollection .

Hãy tạo một cá thể Dòng đại diện cho một chuỗi các phần tử và thu thập chúng vào một cá thể Danh sách :

List result = givenList.stream() .collect(toList());

3.1.1. Collectors.toUnmodifiableList ()

Java 10 đã giới thiệu một cách thuận tiện để tích lũy các phần tử Luồng vào một Danh sách không thể sửa đổi :

List result = givenList.stream() .collect(toUnmodifiableList());

Nếu bây giờ chúng tôi cố gắng sửa đổi Danh sách kết quả , chúng tôi sẽ nhận được một Ngoại lệ không được hỗ trợOperationException :

assertThatThrownBy(() -> result.add("foo")) .isInstanceOf(UnsupportedOperationException.class);

3.2. Collectors.toSet ()

Bộ sưu tập ToSet có thể được sử dụng để thu thập tất cả các phần tử Stream vào một cá thể Set . Điều quan trọng cần nhớ là chúng ta không thể giả định bất kỳ triển khai Set cụ thể nào với phương thức này. Nếu chúng ta muốn kiểm soát nhiều hơn điều này, chúng ta có thể sử dụng toCollection .

Hãy tạo một cá thể Stream đại diện cho một chuỗi các phần tử và thu thập chúng vào một cá thể Set :

Set result = givenList.stream() .collect(toSet());

Một Tập hợp không chứa các phần tử trùng lặp. Nếu bộ sưu tập của chúng tôi chứa các yếu tố tương đương với nhau, chúng xuất hiện trong kết quả Set chỉ một lần:

List listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb"); Set result = listWithDuplicates.stream().collect(toSet()); assertThat(result).hasSize(4);

3.2.1. Collectors.toUnmodifiableSet ()

Kể từ Java 10, chúng ta có thể dễ dàng tạo một Tập hợp không thể sửa đổi bằng cách sử dụng bộ sưu tập toUnmodifiableSet () :

Set result = givenList.stream() .collect(toUnmodifiableSet());

Bất kỳ nỗ lực nào để sửa đổi Bộ kết quả sẽ kết thúc với UnsupportedOperationException :

assertThatThrownBy(() -> result.add("foo")) .isInstanceOf(UnsupportedOperationException.class);

3.3. Collectors.toCollection ()

Như bạn có thể đã nhận thấy, khi sử dụng bộ sưu tập toSet và toList , bạn không thể đưa ra bất kỳ giả định nào về việc triển khai chúng. Nếu bạn muốn sử dụng triển khai tùy chỉnh, bạn sẽ cần sử dụng bộ sưu tập toCollection với một bộ sưu tập được cung cấp mà bạn lựa chọn.

Hãy tạo một phiên bản Stream đại diện cho một chuỗi các phần tử và thu thập chúng vào một phiên bản LinkedList :

List result = givenList.stream() .collect(toCollection(LinkedList::new))

Lưu ý rằng điều này sẽ không hoạt động với bất kỳ bộ sưu tập bất biến nào. Trong trường hợp như vậy, bạn sẽ cần phải viết triển khai Collector tùy chỉnh hoặc sử dụng collectAndThen .

3.4. Người sưu tầm . toMap ()

Bộ sưu tập ToMap có thể được sử dụng để thu thập các phần tử Luồng vào một cá thể Bản đồ . Để làm điều này, chúng tôi cần cung cấp hai chức năng:

  • keyMapper
  • valueMapper

keyMapper sẽ được sử dụng để trích xuất khóa Bản đồ từ phần tử LuồngvalueMapper sẽ được sử dụng để trích xuất một giá trị được liên kết với một khóa nhất định.

Hãy thu thập các phần tử đó vào một Bản đồ lưu trữ các chuỗi dưới dạng khóa và độ dài của chúng dưới dạng giá trị:

Map result = givenList.stream() .collect(toMap(Function.identity(), String::length))

Function.identity () chỉ là một phím tắt để định nghĩa một hàm chấp nhận và trả về cùng một giá trị.

Điều gì xảy ra nếu bộ sưu tập của chúng tôi chứa các phần tử trùng lặp? Trái ngược với toSet , toMap không âm thầm lọc các bản sao. Điều đó có thể hiểu được - làm thế nào nó nên tìm ra giá trị nào để chọn cho khóa này?

List listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb"); assertThatThrownBy(() -> { listWithDuplicates.stream().collect(toMap(Function.identity(), String::length)); }).isInstanceOf(IllegalStateException.class);

Lưu ý rằng toMap thậm chí không đánh giá liệu các giá trị có bằng nhau hay không. Nếu nó thấy các khóa trùng lặp, nó sẽ ngay lập tức ném ra một IllegalStateException .

Trong trường hợp xung đột khóa như vậy, chúng ta nên sử dụng toMap với một chữ ký khác:

Map result = givenList.stream() .collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));

Đối số thứ ba ở đây là BinaryOperator , nơi chúng ta có thể chỉ định cách chúng ta muốn xử lý các xung đột. Trong trường hợp này, chúng tôi sẽ chỉ chọn bất kỳ giá trị nào trong hai giá trị va chạm này vì chúng tôi biết rằng các chuỗi giống nhau sẽ luôn có cùng độ dài.

3.4.1. Collectors.toUnmodifiableMap ()

Tương tự như đối với Danh sáchTập hợp , Java 10 đã giới thiệu một cách dễ dàng để thu thập các phần tử Luồng vào một Bản đồ không thể sửa đổi :

Map result = givenList.stream() .collect(toMap(Function.identity(), String::length))

Như chúng ta có thể thấy, nếu chúng ta cố gắng đưa một mục mới vào Bản đồ kết quả , chúng ta sẽ nhận được UnsupportedOperationException :

assertThatThrownBy(() -> result.put("foo", 3)) .isInstanceOf(UnsupportedOperationException.class);

3.5. Collectors .c ollectingAndThen ()

CollectingAndThen is a special collector that allows performing another action on a result straight after collecting ends.

Let's collect Stream elements to a List instance and then convert the result into an ImmutableList instance:

List result = givenList.stream() .collect(collectingAndThen(toList(), ImmutableList::copyOf))

3.6. Collectors.joining()

Joining collector can be used for joining Stream elements.

We can join them together by doing:

String result = givenList.stream() .collect(joining());

which will result in:

"abbcccdd"

You can also specify custom separators, prefixes, postfixes:

String result = givenList.stream() .collect(joining(" "));

which will result in:

"a bb ccc dd"

or you can write:

String result = givenList.stream() .collect(joining(" ", "PRE-", "-POST"));

which will result in:

"PRE-a bb ccc dd-POST"

3.7. Collectors.counting()

Counting is a simple collector that allows simply counting of all Stream elements.

Now we can write:

Long result = givenList.stream() .collect(counting());

3.8. Collectors.summarizingDouble/Long/Int()

SummarizingDouble/Long/Int is a collector that returns a special class containing statistical information about numerical data in a Stream of extracted elements.

We can obtain information about string lengths by doing:

DoubleSummaryStatistics result = givenList.stream() .collect(summarizingDouble(String::length));

In this case, the following will be true:

assertThat(result.getAverage()).isEqualTo(2); assertThat(result.getCount()).isEqualTo(4); assertThat(result.getMax()).isEqualTo(3); assertThat(result.getMin()).isEqualTo(1); assertThat(result.getSum()).isEqualTo(8);

3.9. Collectors.averagingDouble/Long/Int()

AveragingDouble/Long/Int is a collector that simply returns an average of extracted elements.

We can get average string length by doing:

Double result = givenList.stream() .collect(averagingDouble(String::length));

3.10. Collectors.summingDouble/Long/Int()

SummingDouble/Long/Int is a collector that simply returns a sum of extracted elements.

We can get a sum of all string lengths by doing:

Double result = givenList.stream() .collect(summingDouble(String::length));

3.11. Collectors.maxBy()/minBy()

MaxBy/MinBy collectors return the biggest/the smallest element of a Stream according to a provided Comparator instance.

We can pick the biggest element by doing:

Optional result = givenList.stream() .collect(maxBy(Comparator.naturalOrder()));

Notice that returned value is wrapped in an Optional instance. This forces users to rethink the empty collection corner case.

3.12. Collectors.groupingBy()

GroupingBy collector is used for grouping objects by some property and storing results in a Map instance.

We can group them by string length and store grouping results in Set instances:

Map
    
      result = givenList.stream() .collect(groupingBy(String::length, toSet()));
    

This will result in the following being true:

assertThat(result) .containsEntry(1, newHashSet("a")) .containsEntry(2, newHashSet("bb", "dd")) .containsEntry(3, newHashSet("ccc")); 

Notice that the second argument of the groupingBy method is a Collector and you are free to use any Collector of your choice.

3.13. Collectors.partitioningBy()

PartitioningBy is a specialized case of groupingBy that accepts a Predicate instance and collects Stream elements into a Map instance that stores Boolean values as keys and collections as values. Under the “true” key, you can find a collection of elements matching the given Predicate, and under the “false” key, you can find a collection of elements not matching the given Predicate.

You can write:

Map
    
      result = givenList.stream() .collect(partitioningBy(s -> s.length() > 2))
    

Which results in a Map containing:

{false=["a", "bb", "dd"], true=["ccc"]} 

3.14. Collectors.teeing()

Let's find the maximum and minimum numbers from a given Stream using the collectors we've learned so far:

List numbers = Arrays.asList(42, 4, 2, 24); Optional min = numbers.stream().collect(minBy(Integer::compareTo)); Optional max = numbers.stream().collect(maxBy(Integer::compareTo)); // do something useful with min and max

Here, we're using two different collectors and then combining the result of those two to create something meaningful. Before Java 12, in order to cover such use cases, we had to operate on the given Stream twice, store the intermediate results into temporary variables and then combine those results afterward.

Fortunately, Java 12 offers a built-in collector that takes care of these steps on our behalf: all we have to do is provide the two collectors and the combiner function.

Since this new collector tees the given stream towards two different directions, it's called teeing:

numbers.stream().collect(teeing( minBy(Integer::compareTo), // The first collector maxBy(Integer::compareTo), // The second collector (min, max) -> // Receives the result from those collectors and combines them ));

This example is available on GitHub in the core-java-12 project.

4. Custom Collectors

If you want to write your Collector implementation, you need to implement Collector interface and specify its three generic parameters:

public interface Collector {...}
  1. T – the type of objects that will be available for collection,
  2. A – the type of a mutable accumulator object,
  3. R – the type of a final result.

Let's write an example Collector for collecting elements into an ImmutableSet instance. We start by specifying the right types:

private class ImmutableSetCollector implements Collector
    
      {...}
    

Since we need a mutable collection for internal collection operation handling, we can't use ImmutableSet for this; we need to use some other mutable collection or any other class that could temporarily accumulate objects for us.

In this case, we will go on with an ImmutableSet.Builder and now we need to implement 5 methods:

  • Supplier supplier()
  • BiConsumer accumulator()
  • BinaryOperator combiner()
  • Function finisher()
  • Set characteristics()

The supplier()method returns a Supplier instance that generates an empty accumulator instance, so, in this case, we can simply write:

@Override public Supplier
    
      supplier() { return ImmutableSet::builder; } 
    

The accumulator() method returns a function that is used for adding a new element to an existing accumulator object, so let's just use the Builder‘s add method.

@Override public BiConsumer
    
      accumulator() { return ImmutableSet.Builder::add; }
    

The combiner()method returns a function that is used for merging two accumulators together:

@Override public BinaryOperator
    
      combiner() { return (left, right) -> left.addAll(right.build()); }
    

The finisher() method returns a function that is used for converting an accumulator to final result type, so in this case, we will just use Builder‘s build method:

@Override public Function
    
      finisher() { return ImmutableSet.Builder::build; }
    

Phương thức đặc tính () được sử dụng để cung cấp cho Luồng một số thông tin bổ sung sẽ được sử dụng để tối ưu hóa nội bộ. Trong trường hợp này, chúng ta không chú ý đến thứ tự các phần tử trong một Tập hợp để chúng ta sẽ sử dụng Đặc điểm.UNORDERED . Để có thêm thông tin về chủ đề này, hãy kiểm traJavaDoc của Đặc điểm .

@Override public Set characteristics() { return Sets.immutableEnumSet(Characteristics.UNORDERED); }

Đây là cách triển khai đầy đủ cùng với cách sử dụng:

public class ImmutableSetCollector implements Collector
    
      { @Override public Supplier
     
       supplier() { return ImmutableSet::builder; } @Override public BiConsumer
      
        accumulator() { return ImmutableSet.Builder::add; } @Override public BinaryOperator
       
         combiner() { return (left, right) -> left.addAll(right.build()); } @Override public Function
        
          finisher() { return ImmutableSet.Builder::build; } @Override public Set characteristics() { return Sets.immutableEnumSet(Characteristics.UNORDERED); } public static ImmutableSetCollector toImmutableSet() { return new ImmutableSetCollector(); }
        
       
      
     
    

và ở đây đang hoạt động:

List givenList = Arrays.asList("a", "bb", "ccc", "dddd"); ImmutableSet result = givenList.stream() .collect(toImmutableSet());

5. Kết luận

Trong bài viết này, chúng tôi đã khám phá chuyên sâu Bộ sưu tập của Java 8 và chỉ ra cách triển khai một. Đảm bảo kiểm tra một trong các dự án của tôi để nâng cao khả năng xử lý song song trong Java.

Tất cả các ví dụ về mã đều có sẵn trên GitHub. Bạn có thể đọc nhiều bài viết thú vị hơn trên trang web của tôi.