Panduan untuk Pengumpul Java 8

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan melalui Java 8's Collectors, yang digunakan pada langkah terakhir memproses Stream .

Sekiranya anda ingin membaca lebih lanjut mengenai Stream API itu sendiri, baca artikel ini.

Sekiranya anda ingin melihat bagaimana memanfaatkan kekuatan Pengumpul untuk pemprosesan selari, periksa projek ini.

2. Stream.collect () Menghubungi

Stream.collect () adalah salah satu kaedah terminal API Java 8's Stream . Ini membolehkan kita melakukan operasi lipatan yang dapat diubah (mengemas semula elemen ke beberapa struktur data dan menerapkan beberapa logik tambahan, menggabungkannya, dan lain-lain) pada elemen data yang terdapat dalam instance Stream .

Strategi untuk operasi ini disediakan melalui pelaksanaan antara muka Kolektor .

3. Pengumpul

Semua pelaksanaan yang telah ditentukan boleh didapati di kelas Pengumpul . Menjadi amalan biasa untuk menggunakan import statik berikut dengan mereka untuk meningkatkan peningkatan kebolehbacaan:

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

atau hanya pengumpul import tunggal pilihan anda:

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

Dalam contoh berikut, kami akan menggunakan semula senarai berikut:

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

3.1. Collectors.toList ()

Pengumpul ToList boleh digunakan untuk mengumpulkan semua elemen Stream ke dalam instance List . Perkara penting yang perlu diingat adalah kenyataan bahawa kita tidak dapat melaksanakan pelaksanaan Daftar tertentu dengan kaedah ini. Sekiranya anda ingin mempunyai lebih banyak kawalan terhadap ini, gunakan toCollection sebagai gantinya.

Mari buat contoh Aliran yang mewakili urutan elemen dan kumpulkan ke dalam contoh Senarai :

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

3.1.1. Collectors.toUnmodifiableList ()

Java 10 diperkenalkan cara yang mudah untuk mengumpul Stream elemen ke dalam unmodifiable Senarai :

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

Sekiranya sekarang kita cuba mengubah Senarai hasil , kita akan mendapat UnsupportedOperationException :

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

3.2. Pengumpul.toSet ()

Pengumpul ToSet dapat digunakan untuk mengumpulkan semua elemen Stream ke dalam instance Set . Perkara penting yang perlu diingat adalah kenyataan bahawa kita tidak dapat menganggap pelaksanaan Set tertentu dengan kaedah ini. Sekiranya kita ingin mempunyai lebih banyak kawalan terhadap ini, kita boleh menggunakan toCollection sebagai gantinya.

Mari buat contoh Aliran yang mewakili urutan elemen dan kumpulkan ke dalam contoh Set :

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

A Set tidak mengandungi unsur-unsur pendua. Sekiranya koleksi kami mengandungi unsur yang sama antara satu sama lain, koleksi tersebut muncul dalam Set yang dihasilkan hanya sekali:

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

3.2.1. Pengumpul.untuk Tidak Boleh Diubahsuai ()

Oleh kerana Java 10 kita dapat membuat Set yang tidak dapat diubah dengan mudah menggunakan pengumpul toUnmodifiableSet () :

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

Sebarang percubaan untuk mengubah Set hasil akan berakhir dengan UnsupportedOperationException :

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

3.3. Collectors.toCollection ()

Seperti yang mungkin anda perhatikan, semasa menggunakan pengumpul toSet dan toList , anda tidak boleh membuat andaian pelaksanaannya. Sekiranya anda ingin menggunakan implementasi khusus, anda perlu menggunakan pengumpul toCollection dengan koleksi pilihan anda.

Mari buat instance Stream yang mewakili urutan elemen dan kumpulkan ke dalam instance LinkedList :

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

Perhatikan bahawa ini tidak akan berfungsi dengan koleksi yang tidak berubah. Dalam kes sedemikian, anda perlu menulis pelaksanaan Pengumpul khusus atau menggunakan pengumpulanAndThen .

3.4. Pengumpul . ke Peta ()

Pengumpul ToMap dapat digunakan untuk mengumpulkan elemen Stream ke dalam contoh Peta . Untuk melakukan ini, kita perlu menyediakan dua fungsi:

  • pemeta kunci
  • nilaiMapper

keyMapper akan digunakan untuk mengekstrak kunci Peta dari elemen Aliran , dan valueMapper akan digunakan untuk mengekstrak nilai yang dikaitkan dengan kunci yang diberikan.

Mari kumpulkan elemen-elemen tersebut ke dalam Peta yang menyimpan rentetan sebagai kunci dan panjangnya sebagai nilai:

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

Function.identity () hanyalah jalan pintas untuk menentukan fungsi yang menerima dan mengembalikan nilai yang sama.

Apa yang berlaku sekiranya koleksi kami mengandungi unsur pendua? Berbeza dengan toSet , toMap tidak menapis pendua secara senyap. Ia dapat difahami - bagaimana harus mengetahui nilai mana yang harus dipilih untuk kunci ini?

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

Perhatikan bahawa toMap bahkan tidak menilai sama ada nilainya juga sama. Sekiranya ia melihat kunci pendua, ia akan segera membuang IllegalStateException .

Dalam kes seperti perlanggaran kunci, kita harus menggunakan toMap dengan tandatangan lain:

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

Hujah ketiga di sini adalah BinaryOperator , di mana kita dapat menentukan bagaimana kita mahu perlanggaran ditangani. Dalam kes ini, kita hanya akan memilih salah satu daripada dua nilai bertabrakan ini kerana kita tahu bahawa tali yang sama juga akan mempunyai panjang yang sama.

3.4.1. Pemungut.untuk Tidak Boleh Diubah Peta ()

Begitu juga dengan List s dan Set s, Java 10 memperkenalkan cara mudah untuk mengumpulkan elemen Stream ke dalam Peta yang tidak dapat diubah :

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

Seperti yang kita lihat, jika kita mencuba memasukkan entri baru ke dalam Peta hasil , kita akan mendapat UnsupportedOperationException :

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

3.5. Pengumpul .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; }
    

Kaedah ciri () digunakan untuk menyediakan Stream dengan beberapa maklumat tambahan yang akan digunakan untuk pengoptimuman dalaman. Dalam kes ini, kita tidak memperhatikan susunan elemen dalam Set sehingga kita akan menggunakan Karakteristik . Untuk mendapatkan maklumat lanjut mengenai perkara ini, daftar Ciri-ciri Javadoc '.

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

Berikut adalah pelaksanaan lengkap bersama penggunaannya:

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

dan di sini dalam tindakan:

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

5. Kesimpulan

Dalam artikel ini, kami menjelajahi Pengumpul Java 8 secara mendalam dan menunjukkan cara menerapkannya. Pastikan untuk memeriksa salah satu projek saya yang meningkatkan keupayaan pemprosesan selari di Java.

Semua contoh kod boleh didapati di GitHub. Anda boleh membaca lebih banyak artikel menarik di laman web saya.