Terminal operation reduce melipat stream menjadi satu nilai, tetapi pekerjaan nyata sering meminta bentuk hasil yang lebih kaya: teks gabungan, statistik angka, Map nama-ke-harga, hingga laporan ringkas yang menggabungkan beberapa angka sekaligus. Untuk kebutuhan tersebut, Stream API menyediakan method collect yang bekerja sama dengan kelas java.util.stream.Collectors — pabrik Collector siap pakai. Pada Chapter ini kita akan memakai Collector bawaan untuk joining, counting, summingInt, averagingInt, maxBy, minBy, toMap, mapping, hingga teeing, semuanya tanpa satu pun perulangan manual.
Method collect dan Kelas Collectors #
collect menjalankan pola mutable reduction: alih-alih membuat nilai baru pada setiap langkah seperti reduce, collect menumbuhkan satu wadah yang bisa diubah — StringBuilder, ArrayList, atau HashMap — lalu mengembalikan wadah itu sebagai hasil akhir. Implementasinya diatur oleh interface Collector, yang menyimpan empat komponen: pabrik wadah (supplier), cara menambah satu elemen (accumulator), cara menggabungkan dua wadah saat stream berjalan paralel (combiner), dan transformasi akhir opsional (finisher).
Kita hampir tidak pernah menulis Collector sendiri dari nol. Cukup panggil method statis pada Collectors dan biarkan pustaka menyusun keempat komponen tersebut. Berikut Collector yang akan kita pakai di chapter ini:
| Collector | Hasil | Keterangan |
|---|---|---|
joining(d, p, s) | String | gabungkan teks dengan pemisah, prefiks, sufiks |
counting() | Long | jumlah elemen |
summingInt(...) | Integer | total nilai fungsi int |
averagingInt(...) | Double | rata-rata nilai fungsi int |
maxBy(k) / minBy(k) | Optional<T> | elemen terbesar/terkecil menurut pembanding |
toMap(k, v, merge) | Map<K,V> | bangun Map; merge menyelesaikan kunci ganda |
mapping(f, lain) | bervariasi | ubah elemen sebelum dikumpulkan downstream |
teeing(c1, c2, gabung) | bervariasi | jalankan dua Collector lalu gabungkan hasilnya |
Laporan Nilai dengan joining dan Statistik #
Bayangkan kita menyimpan nilai ulangan satu rombongan dan harus mencetak laporan singkat: daftar nama, jumlah siswa, total nilai, rata-rata, serta pemilik nilai tertinggi dan terendah. Program berikut menyelesaikan seluruh tugas itu dengan satu class Siswa berbentuk record dan serangkaian pemanggilan collect:
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class LaporanNilai {
record Siswa(String nama, String kelas, int nilai) {}
public static void main(String[] args) {
List<Siswa> rombongan = List.of(
new Siswa("Budi", "10A", 78),
new Siswa("Siti", "10A", 92),
new Siswa("Dewi", "10A", 85),
new Siswa("Ani", "10A", 74)
);
// joining: gabungkan nama dengan pemisah, prefiks, dan sufiks
String daftarNama = rombongan.stream()
.map(Siswa::nama)
.collect(Collectors.joining(", ", "Siswa: [", "]"));
System.out.println(daftarNama);
// counting: jumlah elemen sebagai Long
Long jumlah = rombongan.stream().collect(Collectors.counting());
System.out.println("Jumlah siswa: " + jumlah);
// summingInt dan averagingInt
Integer total = rombongan.stream().collect(Collectors.summingInt(Siswa::nilai));
Double rataRata = rombongan.stream().collect(Collectors.averagingInt(Siswa::nilai));
System.out.println("Total nilai: " + total);
System.out.printf("Rata-rata: %.2f%n", rataRata);
// maxBy dan minBy menghasilkan Optional<Siswa>
Comparator<Siswa> byNilai = Comparator.comparingInt(Siswa::nilai);
Optional<Siswa> tertinggi = rombongan.stream().collect(Collectors.maxBy(byNilai));
Optional<Siswa> terendah = rombongan.stream().collect(Collectors.minBy(byNilai));
tertinggi.ifPresent(s -> System.out.println("Nilai tertinggi: " + s.nama() + " (" + s.nilai() + ")"));
System.out.println("Nilai terendah: " + terendah.map(Siswa::nama).orElse("tidak ada data"));
}
}Output yang Dihasilkan:
Siswa: [Budi, Siti, Dewi, Ani]
Jumlah siswa: 4
Total nilai: 329
Rata-rata: 82.25
Nilai tertinggi: Siti (92)
Nilai terendah: AniMari kita telaah baris-baris kode di atas:
- Baris 8 (
record Siswa(String nama, String kelas, int nilai) {}): mendefinisikan model data ringkas. Methodnama(),kelas(), dannilai()dibuat otomatis, sehingga method reference sepertiSiswa::namalangsung tersedia untuk pipeline. - Baris 19-21 (
rombongan.stream().map(Siswa::nama).collect(...)): pipeline mengubah setiapSiswamenjadiStringnama, lalujoiningmerangkai hasilnya. Argumen pertama", "adalah pemisah antar elemen, argumen kedua"Siswa: ["prefiks, dan argumen ketiga"]"sufiks — ketiganya boleh dikosongkan bila tidak dibutuhkan. - Baris 25 (
Long jumlah = ... Collectors.counting()):counting()menghitung elemen dan mengembalikanLong, bukanint— selaras dengan terminal operationcount()yang juga mengembalikanlong, sehingga kedua jalur hitung memakai tipe yang sama. - Baris 29-30 (
summingIntdanaveragingInt): kedua Collector menerimaToIntFunction, di siniSiswa::nilai.summingIntmenghasilkanIntegerbertotal 329, sedangkanaveragingIntmenghasilkanDouble82.25; pembulatan tampilan dua angka di baris 32 ditanganiprintfdengan format%.2f. - Baris 35-37 (
ComparatorlalumaxBy/minBy):maxBydanminBymenerimaComparatordan menghasilkanOptional<Siswa>karena stream kosong tidak punya elemen terbesar. WadahOptionalmemaksa kita menangani kasus kosong secara eksplisit. - Baris 38-39 (
ifPresentdanmap(...).orElse(...)): kedua baris membukaOptionaldengan aman —ifPresentmenjalankan aksi hanya bila nilai ada, sedangkanmap(Siswa::nama).orElse("tidak ada data")menyiapkan nilai cadangan tanpa pernah memanggilget()tanpa pemeriksaan.
Membangun Map dengan toMap dan Merge Function #
toMap memetakan setiap elemen menjadi satu pasangan kunci-nilai. Masalah klasiknya: dua elemen bisa menghasilkan kunci yang sama. Tanpa penyelesai, toMap melempar IllegalStateException; dengan merge function, kita yang memutuskan nilai mana yang bertahan. Toko contoh kita menyimpan dua entri Pena dengan harga berbeda, dan merge function Math::min memilih harga termurah:
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class HargaProduk {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> etalase = List.of(
new Produk("Pena", "alat tulis", 2000),
new Produk("Pensil", "alat tulis", 1500),
new Produk("Pena", "alat tulis", 2500),
new Produk("Bag", "tas", 75000)
);
// toMap dengan merge function: kunci ganda diselesaikan, bukan error
Map<String, Integer> hargaTermurah = etalase.stream()
.collect(Collectors.toMap(
Produk::nama,
Produk::harga,
(hargaLama, hargaBaru) -> Math.min(hargaLama, hargaBaru)));
System.out.println("Harga termurah per nama: " + hargaTermurah);
// mapping sebagai downstream collector pada groupingBy
Map<String, List<String>> namaPerKategori = etalase.stream()
.collect(Collectors.groupingBy(
Produk::kategori,
Collectors.mapping(Produk::nama, Collectors.toList())));
System.out.println("Nama per kategori: " + namaPerKategori);
// teeing: dua collector berjalan pada stream yang sama lalu hasilnya digabung
String ringkasan = etalase.stream().collect(Collectors.teeing(
Collectors.counting(),
Collectors.summingInt(Produk::harga),
(jumlah, total) -> jumlah + " produk dengan total harga " + total));
System.out.println(ringkasan);
}
}Output yang Dihasilkan:
Harga termurah per nama: {Bag=75000, Pensil=1500, Pena=2000}
Nama per kategori: {tas=[Bag], alat tulis=[Pena, Pensil, Pena]}
4 produk dengan total harga 81000Mari kita telaah bagian-bagian pentingnya:
- Baris 18-22 (
toMap(Produk::nama, Produk::harga, ...)): argumen pertama menentukan kunci, argumen kedua menentukan nilai, dan lambda ketiga(hargaLama, hargaBaru) -> Math.min(hargaLama, hargaBaru)dipanggil hanya saat kunciPenamuncul kedua kali. Hasil akhirnyaPena=2000, bukan galat. - Baris 26-29 (
groupingBy+mapping):mapping(Produk::nama, Collectors.toList())adalah downstream collector — Collector yang memproses nilai di dalam setiap kelompok alih-alih elemen aslinya. Kelompok"alat tulis"berisiPena,Pensil, laluPenalagi karena sumbernya memang belum dibersihkan dari duplikat; pembahasan penuhgroupingBymenanti Chapter berikutnya. - Baris 33-36 (
teeing):teeing(hadir sejak Java 12) membelah pekerjaan menjadi dua Collector —counting()dansummingInt— lalu lambda terakhir menggabungkan hasil4dan81000menjadi satu kalimat. Pola ini menggantikan kebutuhan menjalankan stream dua kali.
Bila merge function sengaja dihapus, program yang sama berhenti dengan galat. Berkas berikut sengaja dibiarkan gagal untuk membuktikannya:
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class TanpaMerge {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> etalase = List.of(
new Produk("Pena", "alat tulis", 2000),
new Produk("Pena", "alat tulis", 2500)
);
// TANPA merge function: kunci ganda memicu IllegalStateException
Map<String, Integer> harga = etalase.stream()
.collect(Collectors.toMap(Produk::nama, Produk::harga));
System.out.println(harga);
}
}Galat tersebut terverifikasi dengan menjalankan berkasnya: JVM mencetak Exception in thread "main" java.lang.IllegalStateException: Duplicate key Pena (attempted merging values 2000 and 2500) — pesan yang menyebut kunci ganda beserta kedua nilai yang bertabrakan. Gunakan merge function setiap kali kunci berpotensi sama, misalnya (a, b) -> a untuk mempertahankan nilai pertama.
Dengan koleksi Collector ini, laporan satu dimensi sudah terasa ringan untuk ditulis. Pertanyaan berikutnya adalah bagaimana mengelompokkan data menjadi kategori — tugas yang dijawab langsung oleh groupingBy dan partitioningBy pada Chapter berikutnya.
Referensi #
- Oracle: java.util.stream.Collectors
- Oracle: package java.util.stream
- Oracle: Java Tutorials — Reduction
- dev.java — Learn Java
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
HargaProduk.java
Unduh berkasimport java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class HargaProduk {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> etalase = List.of(
new Produk("Pena", "alat tulis", 2000),
new Produk("Pensil", "alat tulis", 1500),
new Produk("Pena", "alat tulis", 2500),
new Produk("Bag", "tas", 75000)
);
// toMap dengan merge function: kunci ganda diselesaikan, bukan error
Map<String, Integer> hargaTermurah = etalase.stream()
.collect(Collectors.toMap(
Produk::nama,
Produk::harga,
(hargaLama, hargaBaru) -> Math.min(hargaLama, hargaBaru)));
System.out.println("Harga termurah per nama: " + hargaTermurah);
// mapping sebagai downstream collector pada groupingBy
Map<String, List<String>> namaPerKategori = etalase.stream()
.collect(Collectors.groupingBy(
Produk::kategori,
Collectors.mapping(Produk::nama, Collectors.toList())));
System.out.println("Nama per kategori: " + namaPerKategori);
// teeing: dua collector berjalan pada stream yang sama lalu hasilnya digabung
String ringkasan = etalase.stream().collect(Collectors.teeing(
Collectors.counting(),
Collectors.summingInt(Produk::harga),
(jumlah, total) -> jumlah + " produk dengan total harga " + total));
System.out.println(ringkasan);
}
}
LaporanNilai.java
Unduh berkasimport java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class LaporanNilai {
record Siswa(String nama, String kelas, int nilai) {}
public static void main(String[] args) {
List<Siswa> rombongan = List.of(
new Siswa("Budi", "10A", 78),
new Siswa("Siti", "10A", 92),
new Siswa("Dewi", "10A", 85),
new Siswa("Ani", "10A", 74)
);
// joining: gabungkan nama dengan pemisah, prefiks, dan sufiks
String daftarNama = rombongan.stream()
.map(Siswa::nama)
.collect(Collectors.joining(", ", "Siswa: [", "]"));
System.out.println(daftarNama);
// counting: jumlah elemen sebagai Long
Long jumlah = rombongan.stream().collect(Collectors.counting());
System.out.println("Jumlah siswa: " + jumlah);
// summingInt dan averagingInt
Integer total = rombongan.stream().collect(Collectors.summingInt(Siswa::nilai));
Double rataRata = rombongan.stream().collect(Collectors.averagingInt(Siswa::nilai));
System.out.println("Total nilai: " + total);
System.out.printf("Rata-rata: %.2f%n", rataRata);
// maxBy dan minBy menghasilkan Optional<Siswa>
Comparator<Siswa> byNilai = Comparator.comparingInt(Siswa::nilai);
Optional<Siswa> tertinggi = rombongan.stream().collect(Collectors.maxBy(byNilai));
Optional<Siswa> terendah = rombongan.stream().collect(Collectors.minBy(byNilai));
tertinggi.ifPresent(s -> System.out.println("Nilai tertinggi: " + s.nama() + " (" + s.nilai() + ")"));
System.out.println("Nilai terendah: " + terendah.map(Siswa::nama).orElse("tidak ada data"));
}
}
TanpaMerge.java
Unduh berkasimport java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class TanpaMerge {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> etalase = List.of(
new Produk("Pena", "alat tulis", 2000),
new Produk("Pena", "alat tulis", 2500)
);
// TANPA merge function: kunci ganda memicu IllegalStateException
Map<String, Integer> harga = etalase.stream()
.collect(Collectors.toMap(Produk::nama, Produk::harga));
System.out.println(harga);
}
}