Laporan sering kali bukan satu angka, melainkan sekumpulan angka per kategori: jumlah siswa per kelas, total penjualan per kategori barang, atau daftar nama peserta remedial. Chapter sebelumnya memberi kita Collector satu hasil seperti counting dan averagingInt; sekarang kita naik satu tingkat ke Collector yang membangun Map: groupingBy untuk pengelompokan berdasarkan kunci apa pun, dan partitioningBy untuk pembagian dua kelompok berdasarkan kondisi benar-salah. Keduanya setara klausa GROUP BY pada SQL, tetapi bekerja sepenuhnya di dalam pipeline Java.
Cara Kerja groupingBy #
Collectors.groupingBy menerima sebuah classifier function — fungsi yang mengubah setiap elemen menjadi kunci Map. Untuk setiap elemen, classifier dijalankan, kunci hasilnya dicari di Map, dan elemen dimasukkan ke daftar milik kunci tersebut. Bila kunci belum pernah muncul, groupingBy membuat daftar baru lebih dulu; peristilahannya, elemen diakumulasikan ke dalam kelompok.
Tiga overload groupingBy menentukan seberapa jauh kita mengendalikan hasilnya. Overload satu argumen hanya menerima classifier dan menghasilkan Map<K, List<T>> dengan daftar sebagai nilai standar. Overload dua argumen menerima downstream collector — Collector yang memproses isi tiap kelompok, misalnya counting() untuk menghitung anggota atau mapping(...) untuk mengubah isinya. Overload tiga argumen menambahkan supplier Map, sehingga kita bisa memilih TreeMap agar kunci terurut.
groupingBy dengan counting dan mapping #
Kita mulai dari data lima siswa pada tiga kelas berbeda. Program berikut menyusun tiga bentuk laporan dari sumber yang sama: daftar siswa per kelas, jumlah siswa per kelas, dan daftar nama saja per kelas:
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class PengelompokanSiswa {
record Siswa(String nama, String kelas, int nilai) {}
public static void main(String[] args) {
List<Siswa> rombongan = List.of(
new Siswa("Budi", "XII-1", 78),
new Siswa("Siti", "X-2", 92),
new Siswa("Dewi", "X-2", 70),
new Siswa("Ani", "XI-3", 85),
new Siswa("Rahmat", "XII-1", 88)
);
// groupingBy dasar: kelas -> daftar Siswa
Map<String, List<Siswa>> perKelas = rombongan.stream()
.collect(Collectors.groupingBy(Siswa::kelas));
System.out.println("perKelas = " + perKelas);
// groupingBy + counting: kelas -> jumlah siswa
Map<String, Long> jumlahPerKelas = rombongan.stream()
.collect(Collectors.groupingBy(Siswa::kelas, Collectors.counting()));
System.out.println("jumlahPerKelas = " + jumlahPerKelas);
// groupingBy + mapping: kelas -> daftar nama saja
Map<String, List<String>> namaPerKelas = rombongan.stream()
.collect(Collectors.groupingBy(Siswa::kelas,
Collectors.mapping(Siswa::nama, Collectors.toList())));
System.out.println("namaPerKelas = " + namaPerKelas);
}
}Output yang Dihasilkan:
perKelas = {XII-1=[Siswa[nama=Budi, kelas=XII-1, nilai=78], Siswa[nama=Rahmat, kelas=XII-1, nilai=88]], X-2=[Siswa[nama=Siti, kelas=X-2, nilai=92], Siswa[nama=Dewi, kelas=X-2, nilai=70]], XI-3=[Siswa[nama=Ani, kelas=XI-3, nilai=85]]}
jumlahPerKelas = {XII-1=2, X-2=2, XI-3=1}
namaPerKelas = {XII-1=[Budi, Rahmat], X-2=[Siti, Dewi], XI-3=[Ani]}Mari kita telaah baris-baris kode di atas:
- Baris 19-20 (
groupingBy(Siswa::kelas)): classifierSiswa::kelasmengembalikanStringkelas sebagai kunci. Tanpa downstream, nilai standarnyaList<Siswa>, sehingga tipe hasilnyaMap<String, List<Siswa>>. - Baris 24-25 (
groupingBy(Siswa::kelas, Collectors.counting())): downstreamcounting()mengganti isi tiap kelompok dengan jumlah anggotanya bertipeLong. HasilnyaMap<String, Long>, bukan lagi daftar siswa. - Baris 29-31 (
groupingBy+mapping): downstreammapping(Siswa::nama, Collectors.toList())memotong setiap anggota kelompok menjadiStringnama sebelum dikumpulkan menjadi daftar. KombinasigroupingBy+mappingadalah resep paling sering dipakai untuk laporan “kunci ke daftar atribut”. - Urutan kunci pada output:
groupingBysatu argumen memakaiHashMap, sehingga urutan tampilan kunci mengikuti perhitungan hash, bukan urutan alfabet — perhatikanXII-1tampil sebelumX-2. Urutan ini stabil untuk data yang sama, tetapi bukan janji pengurutan.
groupingBy Berjenjang dan Peta Terurut #
Laporan boleh bertingkat. Dengan menjadikan groupingBy sebagai downstream dari groupingBy lain, kita membangun Map bersarang: kunci tingkat pertama kelas, kunci tingkat kedua status lulus, dan nilainya daftar nama. Overload tiga argumen sekaligus memperkenalkan TreeMap sebagai supplier agar kunci tingkat pertama terurut:
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class PengelompokanBerjenjang {
record Siswa(String nama, String kelas, int nilai) {}
public static void main(String[] args) {
List<Siswa> rombongan = List.of(
new Siswa("Budi", "XII-1", 78),
new Siswa("Siti", "X-2", 92),
new Siswa("Dewi", "X-2", 70),
new Siswa("Ani", "XI-3", 85),
new Siswa("Rahmat", "XII-1", 60)
);
// groupingBy berjenjang: kelas -> (lulus/remedial) -> daftar nama
Map<String, Map<Boolean, List<String>>> berjenjang = rombongan.stream()
.collect(Collectors.groupingBy(Siswa::kelas,
Collectors.groupingBy(s -> s.nilai() >= 75,
Collectors.mapping(Siswa::nama, Collectors.toList()))));
System.out.println("berjenjang = " + berjenjang);
// TreeMap sebagai supplier Map: kunci hasil terurut menurut alfabet
Map<String, Long> terurut = rombongan.stream()
.collect(Collectors.groupingBy(
Siswa::kelas, TreeMap::new, Collectors.counting()));
System.out.println("terurut = " + terurut);
}
}Output yang Dihasilkan:
berjenjang = {XII-1={false=[Rahmat], true=[Budi]}, X-2={false=[Dewi], true=[Siti]}, XI-3={true=[Ani]}}
terurut = {X-2=2, XI-3=1, XII-1=2}Mari kita telaah bagian-bagian pentingnya:
- Baris 19-22 (
groupingBybersarang): classifier luarSiswa::kelasmembagi siswa per kelas. Downstream-nya bukantoList(), melainkangroupingBy(s -> s.nilai() >= 75, ...)yang membagi lagi tiap kelas menjadi dua kelompokBoolean, lalumappingmengumpulkan nama-namanya. Tipe hasil akhirnyaMap<String, Map<Boolean, List<String>>>. - Baris 20-22 (predikat menjadi kunci): lambda
s -> s.nilai() >= 75menghasilkantrueataufalse, sehingga peta tingkat kedua berisi kuncitrue(lulus, nilai minimal 75) danfalse(remedial). - Baris 26-28 (
groupingBy(kunci, TreeMap::new, counting())): argumen keduaTreeMap::newadalah supplier yang menentukan jenisMaphasilnya.TreeMapmenyimpan kunci dalam urutan alamiString, terbukti pada outputterurutyang menampilkanX-2,XI-3, laluXII-1— berbeda dengan urutan hashHashMappada bagian pertama program.
partitioningBy untuk Dua Kelompok #
Bila pengelompokannya memang biner — lulus/remedial, murah/mahal, aktif/tidak aktif — partitioningBy lebih tepat daripada groupingBy dengan classifier Boolean. Method ini menerima satu Predicate, menghasilkan Map<Boolean, List<T>>, dan selalu menyediakan kedua kunci true serta false meskipun salah satu kelompok kosong:
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class PembagianLulus {
record Siswa(String nama, String kelas, int nilai) {}
public static void main(String[] args) {
List<Siswa> rombongan = List.of(
new Siswa("Budi", "XII-1", 78),
new Siswa("Siti", "X-2", 92),
new Siswa("Dewi", "X-2", 70),
new Siswa("Rahmat", "XII-1", 60)
);
// partitioningBy dasar: Map<Boolean, List<Siswa>>
Map<Boolean, List<Siswa>> pembagian = rombongan.stream()
.collect(Collectors.partitioningBy(s -> s.nilai() >= 75));
System.out.println("Lulus: " + pembagian.get(true));
System.out.println("Remedial: " + pembagian.get(false));
// partitioningBy dengan downstream counting
Map<Boolean, Long> jumlah = rombongan.stream()
.collect(Collectors.partitioningBy(s -> s.nilai() >= 75,
Collectors.counting()));
System.out.println("jumlah = " + jumlah);
// Stream kosong tetap menghasilkan kedua kunci
Map<Boolean, List<Siswa>> kosong = List.<Siswa>of().stream()
.collect(Collectors.partitioningBy(s -> s.nilai() >= 75));
System.out.println("kosong = " + kosong);
}
}Output yang Dihasilkan:
Lulus: [Siswa[nama=Budi, kelas=XII-1, nilai=78], Siswa[nama=Siti, kelas=X-2, nilai=92]]
Remedial: [Siswa[nama=Dewi, kelas=X-2, nilai=70], Siswa[nama=Rahmat, kelas=XII-1, nilai=60]]
jumlah = {false=2, true=2}
kosong = {false=[], true=[]}Mari kita telaah baris-baris kode di atas:
- Baris 18-19 (
partitioningBy(s -> s.nilai() >= 75)):Predicatemenentukan keanggotaan gruptrueataufalse. Tidak ada classifier bebas seperti padagroupingBy; hanya dua kotak yang tersedia. - Baris 22-23 (
pembagian.get(true)danget(false)): kedua kunci dijamin ada, sehingga pemanggilangetaman tanpa pemeriksaancontainsKey. Perbandingannya,groupingByhanya membuat kelompok yang benar-benar terisi. - Baris 25-27 (downstream
counting()): sama sepertigroupingBy,partitioningBymenerima downstream collector. HasilnyaMap<Boolean, Long>berisifalse=2, true=2. - Baris 31-33 (stream kosong):
partitioningBypada stream kosong tetap menghasilkan{false=[], true=[]}. Perilaku ini berguna saat kode pembaca laporan mengiterasi kedua grup tanpa perlu menanganinull.
Pengelompokan dan statistik pada chapter ini masih bekerja dengan Stream<Integer> untuk angka — setiap nilai tersimpan sebagai objek. Chapter berikutnya memperkenalkan primitive streams (IntStream, LongStream, DoubleStream) yang mengolah angka mentah tanpa boxing, lengkap dengan operasi statistik bawaan seperti sum dan summaryStatistics.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PembagianLulus.java
Unduh berkasimport java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class PembagianLulus {
record Siswa(String nama, String kelas, int nilai) {}
public static void main(String[] args) {
List<Siswa> rombongan = List.of(
new Siswa("Budi", "XII-1", 78),
new Siswa("Siti", "X-2", 92),
new Siswa("Dewi", "X-2", 70),
new Siswa("Rahmat", "XII-1", 60)
);
// partitioningBy dasar: Map<Boolean, List<Siswa>>
Map<Boolean, List<Siswa>> pembagian = rombongan.stream()
.collect(Collectors.partitioningBy(s -> s.nilai() >= 75));
System.out.println("Lulus: " + pembagian.get(true));
System.out.println("Remedial: " + pembagian.get(false));
// partitioningBy dengan downstream counting
Map<Boolean, Long> jumlah = rombongan.stream()
.collect(Collectors.partitioningBy(s -> s.nilai() >= 75,
Collectors.counting()));
System.out.println("jumlah = " + jumlah);
// Stream kosong tetap menghasilkan kedua kunci
Map<Boolean, List<Siswa>> kosong = List.<Siswa>of().stream()
.collect(Collectors.partitioningBy(s -> s.nilai() >= 75));
System.out.println("kosong = " + kosong);
}
}
PengelompokanBerjenjang.java
Unduh berkasimport java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class PengelompokanBerjenjang {
record Siswa(String nama, String kelas, int nilai) {}
public static void main(String[] args) {
List<Siswa> rombongan = List.of(
new Siswa("Budi", "XII-1", 78),
new Siswa("Siti", "X-2", 92),
new Siswa("Dewi", "X-2", 70),
new Siswa("Ani", "XI-3", 85),
new Siswa("Rahmat", "XII-1", 60)
);
// groupingBy berjenjang: kelas -> (lulus/remedial) -> daftar nama
Map<String, Map<Boolean, List<String>>> berjenjang = rombongan.stream()
.collect(Collectors.groupingBy(Siswa::kelas,
Collectors.groupingBy(s -> s.nilai() >= 75,
Collectors.mapping(Siswa::nama, Collectors.toList()))));
System.out.println("berjenjang = " + berjenjang);
// TreeMap sebagai supplier Map: kunci hasil terurut menurut alfabet
Map<String, Long> terurut = rombongan.stream()
.collect(Collectors.groupingBy(
Siswa::kelas, TreeMap::new, Collectors.counting()));
System.out.println("terurut = " + terurut);
}
}
PengelompokanSiswa.java
Unduh berkasimport java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class PengelompokanSiswa {
record Siswa(String nama, String kelas, int nilai) {}
public static void main(String[] args) {
List<Siswa> rombongan = List.of(
new Siswa("Budi", "XII-1", 78),
new Siswa("Siti", "X-2", 92),
new Siswa("Dewi", "X-2", 70),
new Siswa("Ani", "XI-3", 85),
new Siswa("Rahmat", "XII-1", 88)
);
// groupingBy dasar: kelas -> daftar Siswa
Map<String, List<Siswa>> perKelas = rombongan.stream()
.collect(Collectors.groupingBy(Siswa::kelas));
System.out.println("perKelas = " + perKelas);
// groupingBy + counting: kelas -> jumlah siswa
Map<String, Long> jumlahPerKelas = rombongan.stream()
.collect(Collectors.groupingBy(Siswa::kelas, Collectors.counting()));
System.out.println("jumlahPerKelas = " + jumlahPerKelas);
// groupingBy + mapping: kelas -> daftar nama saja
Map<String, List<String>> namaPerKelas = rombongan.stream()
.collect(Collectors.groupingBy(Siswa::kelas,
Collectors.mapping(Siswa::nama, Collectors.toList())));
System.out.println("namaPerKelas = " + namaPerKelas);
}
}