Ръководство за колекционерите на Java 8

1. Общ преглед

В този урок ще разгледаме Колекционерите на Java 8, които се използват в последната стъпка от обработката на поток .

Ако искате да прочетете повече за самия API на Stream , проверете тази статия.

Ако искате да видите как да използвате силата на колекторите за паралелна обработка, проверете този проект.

2. Stream.collect () метод

Stream.collect () е един от терминалните методи на Java 8 Stream API . Позволява ни да изпълняваме операции с променливо сгъване (преопаковане на елементи към някои структури от данни и прилагане на допълнителна логика, конкатенация и т.н.) върху елементи от данни, съхранявани в екземпляр на поток .

Стратегията за тази операция е предоставена чрез внедряване на интерфейс Collector .

3. Колекционери

Всички предварително дефинирани реализации могат да бъдат намерени в класа Collectors . Честа практика е да се използва следният статичен импорт с тях, за да се повиши четливостта:

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

или просто единични колектори за внос по ваш избор:

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

В следващите примери ще използваме повторно следния списък:

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

3.1. Collectors.toList ()

Събирачът на ToList може да се използва за събиране на всички елементи на потока в екземпляр на Списък . Важното нещо, което трябва да запомните, е фактът, че не можем да приемем конкретна реализация на Списък с този метод. Ако искате да имате по-голям контрол над това, използвайте toCollection вместо това.

Нека създадем екземпляр Stream, представляващ последователност от елементи, и ги съберем в екземпляр на List :

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

3.1.1. Collectors.toUnmodifiableList ()

Java 10 представи удобен начин за натрупване на поточните елементи в немодифицируем списък :

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

Ако сега се опитам да променя резултат списъка , ще получите UnsupportedOperationException :

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

3.2. Collectors.toSet ()

Събирачът ToSet може да се използва за събиране на всички елементи на потока в екземпляр Set . Важното нещо, което трябва да запомните, е фактът, че не можем да приемем конкретна реализация на Set с този метод. Ако искаме да имаме по-голям контрол над това, вместо това можем да използваме toCollection .

Нека създадем екземпляр Stream, представляващ последователност от елементи, и ги съберем в екземпляр Set :

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

А Set не съдържа дублиращи се елементи. Ако нашата колекция съдържа елементи, равни помежду си, те се появяват в получения набор само веднъж:

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

3.2.1. Collectors.toUnmodifiableSet ()

Тъй като Java 10 можем лесно да създадем немодифицируем набор, използвайки колектора toUnmodifiableSet () :

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

Всеки опит за модифициране на резултата ще завърши с UnsupportedOperationException :

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

3.3. Collectors.toCollection ()

Както вероятно вече сте забелязали, когато използвате колектори toSet и toList , не можете да правите никакви предположения за техните реализации. Ако искате да използвате персонализирана реализация, ще трябва да използвате колектора toCollection с предоставена колекция по ваш избор.

Нека създадем екземпляр Stream, представляващ последователност от елементи и ги съберем в екземпляр LinkedList :

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

Забележете, че това няма да работи с непроменими колекции. В такъв случай ще трябва да напишете персонализирана реализация на Collector или да използвате collectionAndThen .

3.4. Колекционери . toMap ()

Събирачът на ToMap може да се използва за събиране на поточни елементи в екземпляр на Map . За целта трябва да осигурим две функции:

  • keyMapper
  • valueMapper

keyMapper ще се използва за извличане на ключ на карта от елемент Stream , а valueMapper ще се използва за извличане на стойност, свързана с даден ключ.

Нека съберем тези елементи в Карта, която съхранява низове като ключове и техните дължини като стойности:

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

Function.identity () е просто пряк път за дефиниране на функция, която приема и връща същата стойност.

Какво се случва, ако нашата колекция съдържа дублиращи се елементи? Противно на toSet , toMap не филтрира тихо дубликати. Разбираемо е - как трябва да разбере коя стойност да се избере за този ключ?

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

Имайте предвид, че toMap дори не оценява дали стойностите също са равни. Ако види дублиращи се ключове, той незабавно изхвърля IllegalStateException .

В такива случаи при сблъсък на ключове трябва да използваме toMap с друг подпис:

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

Третият аргумент тук е BinaryOperator , където можем да посочим как искаме да се обработват сблъсъци. В този случай просто ще изберем някоя от тези две сблъскващи се стойности, защото знаем, че едни и същи низове също винаги ще имат еднакви дължини.

3.4.1. Collectors.toUnmodifiableMap ()

По същия начин, както за Списък ите и Set ите, Java 10 въвежда един лесен начин да се съберат поток елементи в един unmodifiable Карта :

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

Както виждаме, ако се опитаме да въведем нов запис в карта с резултати , ще получим UnsupportedOperationException :

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

3.5. Колекционери .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; }
    

Методът характеристики () се използва, за да предостави на Stream някаква допълнителна информация, която ще се използва за вътрешни оптимизации. В този случай не обръщаме внимание на реда на елементите в набор , за да използваме Characteristics.UNORDERED . За да получите повече информация по този въпрос, проверете Характеристики 'JavaDoc.

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

Ето пълното внедряване заедно с използването:

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(); }
        
       
      
     
    

и тук в действие:

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

5. Заключение

В тази статия разгледахме задълбочено Колекционерите на Java 8 и показахме как да внедрим такъв. Не забравяйте да проверите един от моите проекти, който подобрява възможностите на паралелната обработка в Java.

Всички примери за код са достъпни на GitHub. Можете да прочетете още интересни статии на моя сайт.