Pipeline sepanjang apa pun akhirnya harus menyerahkan hasil yang bisa dipakai program lain — List untuk ditampilkan, Set untuk keanggotaan cepat, satu String untuk struk belanja. Di sinilah collect berdiri sebagai operasi akhir paling produktif di Stream API. Berbeda dari reduce yang melipat elemen menjadi satu nilai baru, collect mengisi sebuah container yang bisa diubah — elemen demi elemen — lalu menyerahkan container tersebut sebagai hasil. Pada Chapter ini kita akan memakai Collectors.toList, toSet, toCollection, dan joining, membandingkan Stream.toList() dengan Collectors.toList(), lalu menutup dengan filosofi kapan collect lebih tepat daripada reduce.
Menampung Hasil dengan collect #
Bentuk paling mentah collect menerima tiga bahan: collect(pemasok, akumulator, penggabung). Pemasok (Supplier) membuat container kosong — misalnya ArrayList::new; akumulator (BiConsumer) mengisi container dengan satu elemen; penggabung menyatukan dua container saat stream berjalan paralel. Ketiganya bekerja dengan memutasi container yang sama sepanjang pipeline, bukan membuat salinan baru di tiap langkah seperti reduce. Menulis ketiganya manual membosankan, karena itu class java.util.stream.Collectors menyediakan pabrik siap pakai — toList(), toSet(), toCollection(...), joining(...) — yang tinggal diberikan ke collect.
Empat pabrik tersebut kita rangkum dalam satu program dengan domain toko yang sudah akrab:
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class KumpulkanHasil {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Roti Gandum", "makanan", 30000),
new Produk("Madu Hutan", "makanan", 120000),
new Produk("Air Mineral", "minuman", 8000)
);
// toList: menampung ke List (implementasi ArrayList)
List<String> namaProduk = produk.stream()
.map(Produk::nama)
.collect(Collectors.toList());
System.out.println("toList : " + namaProduk);
// toSet: duplikat kategori otomatis dibuang
Set<String> kategori = produk.stream()
.map(Produk::kategori)
.collect(Collectors.toSet());
System.out.println("toSet : " + kategori.size() + " kategori unik");
System.out.println("berisi minuman? " + kategori.contains("minuman"));
// toCollection: kita yang memilih implementasi collection
LinkedList<String> antrean = produk.stream()
.filter(p -> p.harga() <= 50000)
.map(Produk::nama)
.collect(Collectors.toCollection(LinkedList::new));
System.out.println("toCollection : " + antrean.getClass().getSimpleName() + " " + antrean);
// joining: merangkai teks dengan pemisah, prefiks, dan sufiks
String label = produk.stream()
.filter(p -> p.kategori().equals("minuman"))
.map(Produk::nama)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("joining : " + label);
}
}Output yang Dihasilkan:
toList : [Kopi Arabika, Teh Hijau, Roti Gandum, Madu Hutan, Air Mineral]
toSet : 2 kategori unik
berisi minuman? true
toCollection : LinkedList [Teh Hijau, Roti Gandum, Air Mineral]
joining : [Kopi Arabika, Teh Hijau, Air Mineral]Mari kita bedah keempat penampungan tersebut:
- Baris 20-22 (
Collectors.toList()): hasil berupaList<String>yang memelihara urutan sumber — kelima nama tercetak sesuai urutanListasli. Implementasi di baliknya adalahArrayList. - Baris 26-30 (
Collectors.toSet()): lima produk hanya menyimpan dua kategori unik. HasilSetsengaja tidak kami cetak isinya:HashSettidak menjamin urutan iterasi antar konteks JVM, sehingga program contoh mencetaksize()dancontains()— dua operasi yang tidak bergantung urutan. - Baris 33-36 (
Collectors.toCollection(LinkedList::new)): saat implementasi bawaan kurang pas,toCollectionmenerimaSuppliercollection pilihan kita — di siniLinkedListyang efisien untuk operasi ujung, terlihat darigetClass().getSimpleName()yang tercetak. - Baris 40-43 (
Collectors.joining(", ", "[", "]")): pabrik teks dengan tiga parameter — pemisah antar elemen, prefiks, dan sufiks. Hasilnya satuString[Kopi Arabika, Teh Hijau, Air Mineral]: pengganti lima barisStringBuildermanual, jauh lebih ringkas daripadareduce((a, b) -> a + ", " + b).
Stream.toList() vs Collectors.toList() #
Sejak Java 16 ada dua jalan menuju List: method toList() langsung pada stream, dan collect(Collectors.toList()) yang lebih tua. Hasilnya sama-sama List berisi elemen terurut, tetapi kontrak mutabilitasnya berbeda — dan perbedaan ini menentukan pilihan. Collectors.toList() saat ini menghasilkan ArrayList yang bisa diubah; Stream.toList() menghasilkan List unmodifiable — implementasi internal yang menolak add, set, dan remove. Program berikut membuktikan keduanya:
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class MutabilitasToList {
public static void main(String[] args) {
// collect(Collectors.toList()): hasil MUTABEL (implementasi ArrayList)
List<String> lewatCollect = Stream.of("Budi", "Siti")
.collect(Collectors.toList());
lewatCollect.add("Dewi");
System.out.println("collect(Collectors.toList()) : " + lewatCollect);
// Stream.toList(): hasil TIDAK MUTABEL (sejak Java 16)
List<String> lewatMethod = Stream.of("Budi", "Siti")
.toList();
System.out.println("stream.toList() : " + lewatMethod);
try {
lewatMethod.add("Dewi");
} catch (UnsupportedOperationException e) {
System.out.println("stream.toList() menolak mutasi -> " + e.getClass().getSimpleName());
}
}
}Output yang Dihasilkan:
collect(Collectors.toList()) : [Budi, Siti, Dewi]
stream.toList() : [Budi, Siti]
stream.toList() menolak mutasi -> UnsupportedOperationExceptionMari kita amati perbedaan perilakunya:
- Baris 9-12 (
collect(Collectors.toList())):lewatCollectmenerimaadd("Dewi")tanpa keluhan — hasil cetaknya tiga nama. Implementasi yang dikembalikan adalahArrayListmutabel. - Baris 15-17 (
stream.toList()): penampulan berjalan sama, hasil cetaknya dua nama. Tipe deklarasinya tetapList<String>— perbedaan kontrak tidak terlihat dari tipe. - Baris 18-22 (
try/catch): pemanggilanaddpada hasiltoList()melemparUnsupportedOperationExceptionyang ditangkap lalu dicetak nama class-nya. Compiler tidak menandai mutasi ini sebagai galat; penolakan terjadi saat runtime.
collect atau reduce #
Filosofi keduanya menjawab kebutuhan berbeda. reduce melipat elemen menjadi satu nilai immutable — setiap langkah accumulator menghasilkan nilai baru (a + b membuat String baru; hasil antara tidak boleh dimodifikasi). collect mengisi container mutable — satu StringBuilder atau ArrayList dibuat sekali oleh pemasok, lalu dimutasi oleh akumulator sepanjang pipeline. Untuk rangkaian teks, perbedaan mekanismenya terasa: reduce dengan a + b menyalin isi string lama ke string baru di tiap elemen, sedangkan collect dengan StringBuilder.append menambah di tempat.
Program berikut menyelesaikan tugas yang sama — menggabungkan nama item — dengan ketiga jalan tersebut:
import java.util.List;
import java.util.stream.Collectors;
public class LipatVsTampung {
public static void main(String[] args) {
List<String> item = List.of("Kopi", "Teh", "Roti", "Madu");
// reduce: tiap penggabungan menghasilkan String BARU (immutable)
String viaReduce = item.stream()
.reduce("", (a, b) -> a + b);
System.out.println("reduce : " + viaReduce);
// collect bersama joining: container diisi bertahap
String viaCollect = item.stream()
.collect(Collectors.joining());
System.out.println("collect : " + viaCollect);
// collect bentuk paling mentah: supplier + accumulator + combiner
StringBuilder builder = item.stream()
.collect(StringBuilder::new,
(sb, s) -> sb.append(s),
(sbLain, sbBerikut) -> sbLain.append(sbBerikut));
System.out.println("builder : " + builder);
}
}Output yang Dihasilkan:
reduce : KopiTehRotiMadu
collect : KopiTehRotiMadu
builder : KopiTehRotiMaduKetiga jalur mencetak hasil identik — pembedanya ada di mekanisme:
- Baris 10-11 (
reduce):"" + "Kopi"menghasilkan"Kopi", lalu"Kopi" + "Teh"menghasilkan"KopiTeh"— empatStringbaru diciptakan berurutan, yang lama menjadi sampah memori. Untuk puluhan ribu elemen, pola ini mahal. - Baris 15-16 (
collect(Collectors.joining())):joining()membungkus polaStringBuilderdi dalamCollectors— container diisi denganappenddi tempat lalu diringkas menjadiStringsekali di akhir. Bentuk paling ringkas untuk merangkai teks. - Baris 20-23 (collect mentah): ketiga bahan tertulis terbuka —
StringBuilder::newsebagai pemasok, lambdaappendsebagai akumulator, dan penggabung yang menyatukan dua builder (dipakai saat stream paralel). Inilah bentuk yang sama yang disembunyikan oleh semua factory di classCollectors. - Kaidah praktisnya: gunakan
reducebila hasilnya satu nilai sederhana yang tidak berupa container — angka, boolean, record ringkasan; gunakancollectbila hasilnya kumpulan atau rakitan bertahap —List,Set,Map, teks panjang.
collect yang kita bahas di sini baru menyentuh pabrik-pabrik dasar. Masih satu keluarga besar menunggu: groupingBy untuk mengelompokkan produk per kategori, partitioningBy untuk membelah data berdasar kondisi, hingga mapping dan counting untuk komposisi laporan. Chapter Collectors membuka kotak peralatan lengkap tersebut.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
KumpulkanHasil.java
Unduh berkasimport java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class KumpulkanHasil {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Roti Gandum", "makanan", 30000),
new Produk("Madu Hutan", "makanan", 120000),
new Produk("Air Mineral", "minuman", 8000)
);
// toList: menampung ke List (implementasi ArrayList)
List<String> namaProduk = produk.stream()
.map(Produk::nama)
.collect(Collectors.toList());
System.out.println("toList : " + namaProduk);
// toSet: duplikat kategori otomatis dibuang
Set<String> kategori = produk.stream()
.map(Produk::kategori)
.collect(Collectors.toSet());
System.out.println("toSet : " + kategori.size() + " kategori unik");
System.out.println("berisi minuman? " + kategori.contains("minuman"));
// toCollection: kita yang memilih implementasi collection
LinkedList<String> antrean = produk.stream()
.filter(p -> p.harga() <= 50000)
.map(Produk::nama)
.collect(Collectors.toCollection(LinkedList::new));
System.out.println("toCollection : " + antrean.getClass().getSimpleName() + " " + antrean);
// joining: merangkai teks dengan pemisah, prefiks, dan sufiks
String label = produk.stream()
.filter(p -> p.kategori().equals("minuman"))
.map(Produk::nama)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("joining : " + label);
}
}
LipatVsTampung.java
Unduh berkasimport java.util.List;
import java.util.stream.Collectors;
public class LipatVsTampung {
public static void main(String[] args) {
List<String> item = List.of("Kopi", "Teh", "Roti", "Madu");
// reduce: tiap penggabungan menghasilkan String BARU (immutable)
String viaReduce = item.stream()
.reduce("", (a, b) -> a + b);
System.out.println("reduce : " + viaReduce);
// collect bersama joining: container diisi bertahap
String viaCollect = item.stream()
.collect(Collectors.joining());
System.out.println("collect : " + viaCollect);
// collect bentuk paling mentah: supplier + accumulator + combiner
StringBuilder builder = item.stream()
.collect(StringBuilder::new,
(sb, s) -> sb.append(s),
(sbLain, sbBerikut) -> sbLain.append(sbBerikut));
System.out.println("builder : " + builder);
}
}
MutabilitasToList.java
Unduh berkasimport java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class MutabilitasToList {
public static void main(String[] args) {
// collect(Collectors.toList()): hasil MUTABEL (implementasi ArrayList)
List<String> lewatCollect = Stream.of("Budi", "Siti")
.collect(Collectors.toList());
lewatCollect.add("Dewi");
System.out.println("collect(Collectors.toList()) : " + lewatCollect);
// Stream.toList(): hasil TIDAK MUTABEL (sejak Java 16)
List<String> lewatMethod = Stream.of("Budi", "Siti")
.toList();
System.out.println("stream.toList() : " + lewatMethod);
try {
lewatMethod.add("Dewi");
} catch (UnsupportedOperationException e) {
System.out.println("stream.toList() menolak mutasi -> " + e.getClass().getSimpleName());
}
}
}