Collector adalah resep pengumpulan hasil stream ke dalam wadah akhirnya, dan seluruh resep tersedia sebagai metode static pada kelas Collectors. Operasi terminal collect menerima resep ini, sehingga aliran data dapat ditampung menjadi List, Map, teks gabungan, atau pengelompokan tanpa perulangan manual. Topik ini membedah lima collector yang paling sering dipakai, yaitu toList, joining, toMap, groupingBy, dan partitioningBy, kemudian menutup dengan cara menyusun collector di dalam collector.
toList, joining, dan toMap#
Tiga collector pertama menutup kebutuhan penampungan yang paling umum. toList menyalin elemen aliran ke List, joining menyambung elemen-elemen string menjadi satu teks, dan toMap membangun Map dari dua fungsi, yaitu pemilih kunci dan pemilih nilainya. Program berikut menjalankan ketiganya di atas satu daftar kata yang sama, termasuk perilaku toMap ketika kuncinya kembar.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class DemoCollectorDasar {
public static void main(String[] args) {
List<String> kata = List.of("ombak", "api", "rumah", "api");
List<String> salinan = kata.stream().collect(Collectors.toList());
System.out.println("toList : " + salinan);
String gabungan = kata.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("joining : " + gabungan);
Map<String, Integer> panjangKata = kata.stream()
.distinct()
.collect(Collectors.toMap(k -> k, String::length));
System.out.println("toMap : " + panjangKata);
try {
Map<String, Integer> rusak = kata.stream()
.collect(Collectors.toMap(k -> k, String::length));
System.out.println("Tidak sampai sini: " + rusak);
} catch (IllegalStateException e) {
System.out.println("Exception tertangkap: " + e.getMessage());
}
Map<String, Integer> denganPenggabung = kata.stream()
.collect(Collectors.toMap(k -> k, String::length, (lama, baru) -> lama));
System.out.println("toMap merge: " + denganPenggabung);
}
}toList : [ombak, api, rumah, api]
joining : [ombak, api, rumah, api]
toMap : {ombak=5, api=3, rumah=5}
Exception tertangkap: Duplicate key api (attempted merging values 3 and 3)
toMap merge: {ombak=5, api=3, rumah=5}Kata api muncul dua kali pada sumbernya, dan kehadiran ganda itu ditolak toMap dengan IllegalStateException bercerita Duplicate key api, yang pada contoh ditangkap lalu dicetak agar pesannya terbaca. Solusinya adalah bentuk tiga argumen yang menambahkan fungsi penggabung (lama, baru) -> lama, dan fungsi itulah yang menentukan nilai mana yang dipertahankan ketika kunci bertabrakan. Adapun joining menerima pemisah, awalan, dan akhiran, sehingga hasilnya langsung siap dicetak tanpa penggabungan manual lewat StringBuilder.
Kehadiran Stream.toList() sejak Java 16 menggeser sebagian peran Collectors.toList(), tetapi keduanya belum saling menggantikan sepenuhnya. Hasil Collectors.toList() dapat diubah isinya lewat add atau set, sedangkan hasil Stream.toList() dikunci dari pembuatnya. Bila daftar hasilnya akan diperkaya lagi setelah aliran selesai, pilih collect(Collectors.toList()), dan bila daftar hasilnya hanya dibaca, bentuk yang terkunci lebih aman dari perubahan tak sengaja.
Pengelompokan dengan groupingBy dan partitioningBy#
Pengelompokan menutup kebutuhan yang tidak terjawab oleh toMap, yaitu memetakan satu kunci ke sekelompok nilai, bukan satu nilai saja. groupingBy menerima fungsi klasifikasi yang menentukan kuncinya, lalu mengumpulkan seluruh elemen yang menjawab kunci yang sama ke dalam satu kelompok. Bentuk satu argumennya langsung menghasilkan Map<K, List<T>>, sedangkan bentuk dua argumennya menambahkan collector turunan yang membentuk isi tiap kelompok. Program berikut mengelompokkan mahasiswa menurut prodi, menghitung banyaknya per prodi, lalu membagi dua kelompok menurut ambang nilai.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class DemoGrouping {
record Mahasiswa(String nama, String prodi, int nilai) {}
public static void main(String[] args) {
List<Mahasiswa> kelas = List.of(
new Mahasiswa("Ali", "Informatika", 85),
new Mahasiswa("Budi", "Sipil", 70),
new Mahasiswa("Citra", "Informatika", 92),
new Mahasiswa("Dewi", "Sipil", 78)
);
Map<String, List<String>> perProdi = kelas.stream()
.collect(Collectors.groupingBy(
Mahasiswa::prodi,
Collectors.mapping(Mahasiswa::nama, Collectors.toList())
));
System.out.println("groupingBy : " + perProdi);
Map<String, Long> hitungPerProdi = kelas.stream()
.collect(Collectors.groupingBy(Mahasiswa::prodi, Collectors.counting()));
System.out.println("groupingBy hitung: " + hitungPerProdi);
Map<Boolean, List<String>> kelompokNilai = kelas.stream()
.collect(Collectors.partitioningBy(
m -> m.nilai() >= 80,
Collectors.mapping(Mahasiswa::nama, Collectors.toList())
));
System.out.println("partitioningBy : " + kelompokNilai);
}
}groupingBy : {Informatika=[Ali, Citra], Sipil=[Budi, Dewi]}
groupingBy hitung: {Informatika=2, Sipil=2}
partitioningBy : {false=[Budi, Dewi], true=[Ali, Citra]}Kunci hasil pengelompokan bertipe String karena fungsi klasifikasinya adalah Mahasiswa::prodi, sedangkan isi tiap kelompok berupa daftar nama yang dihasilkan mapping. Bentuk kedua memakai turunan counting sehingga isinya banyaknya anggota tiap kelompok, bukan daftarnya. partitioningBy adalah kasus khusus groupingBy dengan kunci Boolean, dan kedua kunci true maupun false selalu hadir meskipun salah satunya kosong, sehingga cocok untuk pembagian dua sisi menurut satu syarat.
Merangkai collector di dalam collector#
Kekuatan sesungguhnya dari collector terletak pada susunannya, karena hampir semua collector dapat menjadi turunan dari collector lain. Argumen kedua groupingBy pada contoh sebelumnya adalah collector turunan (downstream collector) yang menentukan bentuk isi tiap kelompok, dan isinya boleh diganti sesuai kebutuhan.
// tidak-dijalankan
Map<String, Double> rataPerProdi = kelas.stream()
.collect(Collectors.groupingBy(
Mahasiswa::prodi,
Collectors.averagingInt(Mahasiswa::nilai)));
Map<String, String> namaPerProdi = kelas.stream()
.collect(Collectors.groupingBy(
Mahasiswa::prodi,
Collectors.mapping(Mahasiswa::nama,
Collectors.joining(", "))));Kelompok pertama mengganti isinya dengan rata-rata nilai tiap prodi, sedangkan kelompok kedua menyambung nama-nama anggotanya menjadi satu teks lewat mapping dan joining. Pola semacam ini berlaku berlapis pula, misalnya groupingBy di dalam groupingBy untuk pengelompokan dua tingkat seperti prodi lalu angkatan. Dengan kebiasaan ini, hampir seluruh kebutuhan pengumpulan data dapat dinyatakan sebagai satu pemanggilan collect yang utuh.
Meski begitu, penyusunan yang terlalu dalam punya batasnya. Rangkaian groupingBy bertingkat lebih dari dua sulit dibaca sekaligus sulit diuji, dan pada titik itu kelas hasil yang dirancang sendiri atau beberapa pemanggilan collect yang terpisah justru lebih mudah dirawat. Patokan akhirnya tetap sama dengan kebiasaan lain dalam kode, yaitu kemudahan dibaca oleh rekan yang membacanya berikutnya.
Kesalahan umum#
- Memanggil
toMaptanpa fungsi penggabung pada data berkunci ganda. Kompilasinya sah, tetapi program berhenti denganIllegalStateExceptionsaat dijalankan. Tambahkan fungsi penggabung ketiga yang menyatakan nilai mana yang dipertahankan. - Mengharapkan urutan tertentu pada Map hasil collector.
toMapdangroupingBymemakai HashMap yang urutannya tidak terjamin. Pakai bentuk yang menerima pemasok Map, misalnyagroupingBy(classifier, LinkedHashMap::new, downstream), bila urutan penyisipan ingin dipertahankan. - Membangun teks gabungan lewat
forEachdan StringBuilder. Pola ini panjang dan menyimpan keadaan di luar lambda, sedangkanjoiningmenyediakan hasil yang sama dalam satu baris. Pindahkan penggabungannya ke collector. - Mengira
partitioningByhanya mengisi kunci yang beranggota. Kedua kuncitruedanfalseselalu hadir, sehingga membaca sisi yang kosong tetap menghasilkan daftar kosong, bukannull. Sifat ini justru menyederhanakan pemakaiannya.
Ringkasan#
collectmenerima collector dari kelasCollectorsuntuk menampung aliran menjadi List, Map, teks gabungan, atau pengelompokan.toMapmenuntut fungsi penggabung bila kuncinya bisa ganda, sedangkanjoiningmenyambung teks lengkap dengan pemisah, awalan, dan akhirannya.groupingBymemetakan kunci ke satu kelompok, danpartitioningBymembagi dua sisi menurut satu syarat dengan kunciBooleanyang selalu lengkap.- Collector dapat menjadi turunan collector lain, sehingga isi tiap kelompok dapat dibentuk bebas, dari daftar, hitungan, rata-rata, sampai teks gabungan.
Referensi#
- Collectors (Java SE 21 & JDK 21) — Oracle — daftar lengkap collector bawaan beserta bentuk turunannya pada dokumentasi resmi JDK 21.
- Collector (Java SE 21 & JDK 21) — Oracle — kontrak empat fungsi yang menyusun satu collector.