Panduan untuk Kolektor Java 8

1. Ikhtisar

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

Jika Anda ingin membaca lebih lanjut tentang Stream API itu sendiri, lihat artikel ini.

Jika Anda ingin melihat bagaimana memanfaatkan kekuatan Kolektor untuk pemrosesan paralel, periksa proyek ini.

2. Stream.collect () Method

Stream.collect () adalah salah satu metode terminal API Stream Java 8 . Ini memungkinkan kita untuk melakukan operasi lipatan yang bisa berubah (mengemas ulang elemen ke beberapa struktur data dan menerapkan beberapa logika tambahan, menggabungkannya, dll.) Pada elemen data yang disimpan dalam instance Stream .

Strategi untuk operasi ini disediakan melalui implementasi antarmuka Collector .

3. Kolektor

Semua implementasi yang telah ditentukan dapat ditemukan di kelas Collectors . Ini adalah praktik umum untuk menggunakan impor statis berikut bersama mereka untuk meningkatkan keterbacaan:

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

atau hanya satu pengumpul impor 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 kembali daftar berikut:

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

3.1. Collectors.toList ()

Kolektor ToList dapat digunakan untuk mengumpulkan semua elemen Stream ke dalam instance List . Hal penting untuk diingat adalah kenyataan bahwa kita tidak dapat mengasumsikan implementasi List tertentu dengan metode ini. Jika Anda ingin memiliki kontrol lebih atas ini, gunakan toCollection sebagai gantinya.

Mari buat instance Stream yang mewakili urutan elemen dan kumpulkan menjadi instance List :

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

3.1.1. Collectors.toUnmodifiableList ()

Java 10 memperkenalkan cara mudah untuk mengakumulasi elemen Stream ke dalam Daftar yang tidak dapat dimodifikasi :

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

Jika sekarang kita mencoba mengubah Daftar hasil , kita akan mendapatkan UnsupportedOperationException :

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

3.2. Collectors.toSet ()

Kolektor ToSet dapat digunakan untuk mengumpulkan semua elemen Stream ke dalam instance Set . Hal penting untuk diingat adalah kenyataan bahwa kita tidak dapat mengasumsikan implementasi Set tertentu dengan metode ini. Jika kita ingin memiliki kendali lebih atas ini, kita dapat menggunakan toCollection sebagai gantinya.

Mari buat instance Stream yang mewakili urutan elemen dan kumpulkan menjadi instance Set :

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

Sebuah Set tidak mengandung elemen duplikat. Jika koleksi kita berisi elemen yang sama satu sama lain, mereka muncul di 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. Collectors.toUnmodifiableSet ()

Sejak Java 10 kita dapat dengan mudah membuat Set yang tidak dapat dimodifikasi menggunakan kolektor toUnmodifiableSet () :

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

Setiap upaya untuk mengubah Hasil Set akan berakhir dengan UnsupportedOperationException :

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

3.3. Collectors.toCollection ()

Seperti yang mungkin sudah Anda ketahui , saat menggunakan kolektor toSet dan toList , Anda tidak dapat membuat asumsi apa pun tentang implementasinya. Jika ingin menggunakan implementasi kustom, Anda harus menggunakan kolektor toCollection dengan koleksi pilihan Anda yang disediakan.

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

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

Perhatikan bahwa ini tidak akan berfungsi dengan koleksi yang tidak dapat diubah. Dalam kasus seperti itu, Anda perlu menulis implementasi Collector kustom atau menggunakan collectionAndThen .

3.4. Kolektor . untuk memetakan()

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

  • keyMapper
  • valueMapper

keyMapper akan digunakan untuk mengekstrak kunci Map dari elemen Stream , dan valueMapper akan digunakan untuk mengekstraksi nilai yang terkait dengan kunci tertentu.

Mari kumpulkan elemen-elemen itu ke dalam Peta yang menyimpan string sebagai kunci dan panjangnya sebagai nilai:

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

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

Apa yang terjadi jika koleksi kami mengandung elemen duplikat? Berlawanan dengan toSet , toMap tidak secara diam-diam memfilter duplikat. Ini bisa dimengerti - bagaimana cara 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 bahwa toMap bahkan tidak mengevaluasi apakah nilainya juga sama. Jika melihat kunci duplikat, IllegalStateException akan langsung dilontarkan .

Dalam kasus seperti tabrakan kunci, kita harus menggunakan toMap dengan tanda tangan lain:

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

Argumen ketiga di sini adalah BinaryOperator , di mana kita dapat menentukan bagaimana kita ingin menangani tabrakan. Dalam kasus ini, kita hanya akan memilih salah satu dari dua nilai yang bertabrakan ini karena kita tahu bahwa string yang sama akan selalu memiliki panjang yang sama juga.

3.4.1. Collectors.toUnmodifiableMap ()

Demikian pula untuk List dan Set , Java 10 memperkenalkan cara mudah untuk mengumpulkan elemen Stream ke dalam Peta yang tidak dapat dimodifikasi :

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

Seperti yang bisa kita lihat, jika kita mencoba memasukkan entri baru ke dalam peta hasil , kita akan mendapatkan 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; }
    

Metode karakteristik () digunakan untuk menyediakan Stream dengan beberapa informasi tambahan yang akan digunakan untuk pengoptimalan internal. Dalam hal ini, kami tidak memperhatikan urutan elemen dalam sebuah Set sehingga kami akan menggunakan Characteristics.UNORDERED . Untuk mendapatkan informasi lebih lanjut tentang subjek ini, periksa Karakteristik 'JavaDoc.

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

Berikut implementasi lengkap beserta 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 beraksi:

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

5. Kesimpulan

Dalam artikel ini, kami menjelajahi Kolektor Java 8 secara mendalam dan menunjukkan cara menerapkannya. Pastikan untuk memeriksa salah satu proyek saya yang meningkatkan kemampuan pemrosesan paralel di Java.

Semua contoh kode tersedia di GitHub. Anda dapat membaca lebih banyak artikel menarik di situs saya.