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:

text
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:

  1. Baris 20-22 (Collectors.toList()): hasil berupa List<String> yang memelihara urutan sumber — kelima nama tercetak sesuai urutan List asli. Implementasi di baliknya adalah ArrayList.
  2. Baris 26-30 (Collectors.toSet()): lima produk hanya menyimpan dua kategori unik. Hasil Set sengaja tidak kami cetak isinya: HashSet tidak menjamin urutan iterasi antar konteks JVM, sehingga program contoh mencetak size() dan contains() — dua operasi yang tidak bergantung urutan.
  3. Baris 33-36 (Collectors.toCollection(LinkedList::new)): saat implementasi bawaan kurang pas, toCollection menerima Supplier collection pilihan kita — di sini LinkedList yang efisien untuk operasi ujung, terlihat dari getClass().getSimpleName() yang tercetak.
  4. Baris 40-43 (Collectors.joining(", ", "[", "]")): pabrik teks dengan tiga parameter — pemisah antar elemen, prefiks, dan sufiks. Hasilnya satu String [Kopi Arabika, Teh Hijau, Air Mineral]: pengganti lima baris StringBuilder manual, jauh lebih ringkas daripada reduce((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:

text
collect(Collectors.toList()) : [Budi, Siti, Dewi]
stream.toList()              : [Budi, Siti]
stream.toList() menolak mutasi -> UnsupportedOperationException

Mari kita amati perbedaan perilakunya:

  1. Baris 9-12 (collect(Collectors.toList())): lewatCollect menerima add("Dewi") tanpa keluhan — hasil cetaknya tiga nama. Implementasi yang dikembalikan adalah ArrayList mutabel.
  2. Baris 15-17 (stream.toList()): penampulan berjalan sama, hasil cetaknya dua nama. Tipe deklarasinya tetap List<String> — perbedaan kontrak tidak terlihat dari tipe.
  3. Baris 18-22 (try/catch): pemanggilan add pada hasil toList() melempar UnsupportedOperationException yang 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:

text
reduce  : KopiTehRotiMadu
collect : KopiTehRotiMadu
builder : KopiTehRotiMadu

Ketiga jalur mencetak hasil identik — pembedanya ada di mekanisme:

  1. Baris 10-11 (reduce): "" + "Kopi" menghasilkan "Kopi", lalu "Kopi" + "Teh" menghasilkan "KopiTeh" — empat String baru diciptakan berurutan, yang lama menjadi sampah memori. Untuk puluhan ribu elemen, pola ini mahal.
  2. Baris 15-16 (collect(Collectors.joining())): joining() membungkus pola StringBuilder di dalam Collectors — container diisi dengan append di tempat lalu diringkas menjadi String sekali di akhir. Bentuk paling ringkas untuk merangkai teks.
  3. Baris 20-23 (collect mentah): ketiga bahan tertulis terbuka — StringBuilder::new sebagai pemasok, lambda append sebagai akumulator, dan penggabung yang menyatukan dua builder (dipakai saat stream paralel). Inilah bentuk yang sama yang disembunyikan oleh semua factory di class Collectors.
  4. Kaidah praktisnya: gunakan reduce bila hasilnya satu nilai sederhana yang tidak berupa container — angka, boolean, record ringkasan; gunakan collect bila 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 berkas
java
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);
    }
}

LipatVsTampung.java

Unduh berkas
java
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);
    }
}

MutabilitasToList.java

Unduh berkas
java
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());
        }
    }
}