Laporan yang rapi biasanya menuntut dua penataan: urutan yang benar dan data yang tidak tercatat dua kali. Dalam pipeline stream, keduanya adalah operasi antara sorted dan distinct — sorted menyusun ulang aliran elemen menurut urutan natural atau Comparator yang kita tentukan, sedangkan distinct membuang elemen yang dianggap sama menurut equals(). Keduanya beroperasi tanpa menyentuh collection sumber. Pada Chapter ini kita akan mengurutkan produk dengan beberapa strategi pembanding, lalu membersihkan stok gabungan dari duplikat, termasuk trik TreeSet ketika kunci kembaran hanya satu field.

Mengurutkan dengan sorted #

sorted() tanpa argumen memakai urutan natural — aturan pembandingan yang didefinisikan tipe elemen lewat interface Comparable. String mengurutkan alfabetis dan Integer mengurutkan numerik, jadi pipeline Stream<String> dapat langsung memakai sorted(). Konsekuensinya: elemen yang tidak mengimplementasikan Comparable akan menolak dijalankan. Record Produk kita, misalnya, tidak otomatis Comparable — memanggil sorted() pada Stream<Produk> membuat JVM melempar ClassCastException dengan pesan inti class Produk cannot be cast to class java.lang.Comparable saat operasi akhir dijalankan. Karena itu contoh pertama di bawah memetakan record ke namanya lebih dulu, sedangkan urutan berbasis field memakai Comparator:

import java.util.Comparator;
import java.util.List;

public class UrutkanProduk {

    record Produk(String nama, String kategori, int harga) {}

    public static void main(String[] args) {
        List<Produk> produk = List.of(
                new Produk("Teh Hijau", "minuman", 45000),
                new Produk("Kopi Arabika", "minuman", 85000),
                new Produk("Roti Gandum", "makanan", 30000),
                new Produk("Madu Hutan", "makanan", 30000)
        );

        // sorted(): urutan natural -- elemen harus Comparable
        List<String> namaTerurut = produk.stream()
                .map(Produk::nama)
                .sorted()
                .toList();
        System.out.println("Natural    : " + namaTerurut);

        // sorted(Comparator.comparingInt): urut berdasarkan field harga
        List<String> hargaNaik = produk.stream()
                .sorted(Comparator.comparingInt(Produk::harga))
                .map(p -> p.nama() + " (" + p.harga() + ")")
                .toList();
        System.out.println("Harga naik : " + hargaNaik);

        // reversed + thenComparing: harga turun, nama sebagai penentu
        List<Produk> hargaTurun = produk.stream()
                .sorted(Comparator.comparingInt(Produk::harga).reversed()
                        .thenComparing(Produk::nama))
                .toList();
        System.out.println("Harga turun:");
        hargaTurun.forEach(p -> System.out.println("  " + p.nama() + " - " + p.harga()));
    }
}

Output yang Dihasilkan:

text
Natural    : [Kopi Arabika, Madu Hutan, Roti Gandum, Teh Hijau]
Harga naik : [Roti Gandum (30000), Madu Hutan (30000), Teh Hijau (45000), Kopi Arabika (85000)]
Harga turun:
  Kopi Arabika - 85000
  Teh Hijau - 45000
  Madu Hutan - 30000
  Roti Gandum - 30000

Mari kita bedah tiga strategi pengurutan tersebut:

  1. Baris 17-20 (map lalu sorted()): record dipetakan menjadi nama lebih dulu sehingga elemennya bertipe String — tipe yang Comparable — lalu sorted() mengurutkan alfabetis. Urutan natural hanya menempel pada tipe elemen pipeline, bukan pada record asalnya.
  2. Baris 24-27 (Comparator.comparingInt(Produk::harga)): pembanding dibangun dari method reference ke field int. comparingInt bekerja tanpa pembungkusan Integer; untuk field String padanannya adalah Comparator.comparing(Produk::nama).
  3. Baris 31-34 (reversed() + thenComparing): pembanding harga dibalik arah menjadi menurun, lalu thenComparing(Produk::nama) menentukan urutan elemen yang harganya seri. Pada keluaran, Madu Hutan dan Roti Gandum yang sama-sama 30000 diurutkan menurut nama.
  4. Perhatikan keluaran “Harga naik”: Roti Gandum muncul sebelum Madu Hutan padahal keduanya 30000 — pengurutan stream bersifat stabil, elemen dengan kunci seri mempertahankan urutan kemunculannya di sumber (Roti Gandum lebih dulu di List).

Membuang Duplikat dengan distinct #

distinct membuang elemen yang dianggap sama dengan elemen yang sudah lewat sebelumnya. Patokan kesamaannya adalah method equals() (dibantu hashCode() untuk penampilan cepat) — dan di sinilah record unggul: compiler javac menghasilkan kedua method tersebut dari seluruh field secara otomatis, sehingga dua Produk dengan nama, kategori, dan harga identik dianggap duplikat tanpa satu baris kode tambahan:

import java.util.List;

public class BuangDuplikat {

    record Produk(String nama, String kategori, int harga) {}

    public static void main(String[] args) {
        // Duplikat muncul karena data digabung dari dua cabang toko
        List<Produk> stok = List.of(
                new Produk("Kopi Arabika", "minuman", 85000),
                new Produk("Teh Hijau", "minuman", 45000),
                new Produk("Kopi Arabika", "minuman", 85000),
                new Produk("Roti Gandum", "makanan", 30000),
                new Produk("Teh Hijau", "minuman", 45000)
        );

        // distinct(): membuang duplikat menurut equals()
        List<Produk> unik = stok.stream().distinct().toList();
        System.out.println("Stok unik : " + unik.size() + " jenis");
        unik.forEach(p -> System.out.println("  " + p.nama() + " - " + p.harga()));

        // distinct pada Stream<String>: membandingkan teks utuh
        long jumlahNama = stok.stream().map(Produk::nama).distinct().count();
        System.out.println("Nama unik : " + jumlahNama + " nama");
    }
}

Output yang Dihasilkan:

text
Stok unik : 3 jenis
  Kopi Arabika - 85000
  Teh Hijau - 45000
  Roti Gandum - 30000
Nama unik : 3 nama

Mari kita amati hasilnya:

  1. Baris 18 (stok.stream().distinct()): dari lima entri gabungan dua cabang, tersisa tiga jenis produk. distinct mempertahankan kemunculan pertama tiap kelompok dan membuang yang berikutnya — perhatikan bahwa urutan keluaran mengikuti urutan sumber.
  2. Baris 23-24 (map lalu distinct): pembandingan terjadi pada Stream<String>, jadi patokannya adalah equals milik String. Hasilnya sama-sama tiga karena pada data ini duplikat record selalu berpasangan dengan duplikat nama — pada data lain, keduanya bisa berbeda.

Dedupe Menurut Satu Kunci #

Kadang “duplikat” perlu didefinisikan lebih longgar: satu field saja sebagai kunci. Misalnya, dua entri Kopi Arabika dengan harga sedikit berbeda tetap harus dianggap satu jenis. distinct tidak bisa melakukan hal ini karena patokannya selalu equals utuh — trik yang lazim dipakai adalah menampung hasil ke TreeSet yang pembandingnya dibangun dari kunci tunggal: TreeSet menolak elemen yang “sama” menurut pembandingnya, sekaligus menjaga hasil tetap terurut menurut kunci tersebut.

java Dedupe menurut kunci tunggal dengan TreeSet
// Membuang duplikat menurut field nama saja, hasil akhir terurut nama.
TreeSet<Produk> unikPerNama = stok.stream()
        .collect(Collectors.toCollection(() ->
                new TreeSet<>(Comparator.comparing(Produk::nama))));

Kode di atas sudah diuji dengan data stok yang sama: hasilnya TreeSet berisi tiga produk yang terurut nama — Kopi Arabika, Roti Gandum, lalu Teh Hijau — dan entri Kopi Arabika kedua dibuang karena kuncinya sama dengan entri pertama. Pola toCollection ini adalah bocoran dari Chapter collect, tempat kita menggarap penampungan hasil secara penuh.

Dengan elemen yang kini terurut dan bersih dari kembaran, kita siap mengendalikan jumlah data yang diproses. Chapter limit, skip, takeWhile, dan dropWhile menunjukkan cara memotong aliran dari kedua ujungnya — termasuk pola klasik pagination halaman produk.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

BuangDuplikat.java

Unduh berkas
java
import java.util.List;

public class BuangDuplikat {

    record Produk(String nama, String kategori, int harga) {}

    public static void main(String[] args) {
        // Duplikat muncul karena data digabung dari dua cabang toko
        List<Produk> stok = List.of(
                new Produk("Kopi Arabika", "minuman", 85000),
                new Produk("Teh Hijau", "minuman", 45000),
                new Produk("Kopi Arabika", "minuman", 85000),
                new Produk("Roti Gandum", "makanan", 30000),
                new Produk("Teh Hijau", "minuman", 45000)
        );

        // distinct(): membuang duplikat menurut equals()
        List<Produk> unik = stok.stream().distinct().toList();
        System.out.println("Stok unik : " + unik.size() + " jenis");
        unik.forEach(p -> System.out.println("  " + p.nama() + " - " + p.harga()));

        // distinct pada Stream<String>: membandingkan teks utuh
        long jumlahNama = stok.stream().map(Produk::nama).distinct().count();
        System.out.println("Nama unik : " + jumlahNama + " nama");
    }
}

UrutkanProduk.java

Unduh berkas
java
import java.util.Comparator;
import java.util.List;

public class UrutkanProduk {

    record Produk(String nama, String kategori, int harga) {}

    public static void main(String[] args) {
        List<Produk> produk = List.of(
                new Produk("Teh Hijau", "minuman", 45000),
                new Produk("Kopi Arabika", "minuman", 85000),
                new Produk("Roti Gandum", "makanan", 30000),
                new Produk("Madu Hutan", "makanan", 30000)
        );

        // sorted(): urutan natural -- elemen harus Comparable
        List<String> namaTerurut = produk.stream()
                .map(Produk::nama)
                .sorted()
                .toList();
        System.out.println("Natural    : " + namaTerurut);

        // sorted(Comparator.comparingInt): urut berdasarkan field harga
        List<String> hargaNaik = produk.stream()
                .sorted(Comparator.comparingInt(Produk::harga))
                .map(p -> p.nama() + " (" + p.harga() + ")")
                .toList();
        System.out.println("Harga naik : " + hargaNaik);

        // reversed + thenComparing: harga turun, nama sebagai penentu
        List<Produk> hargaTurun = produk.stream()
                .sorted(Comparator.comparingInt(Produk::harga).reversed()
                        .thenComparing(Produk::nama))
                .toList();
        System.out.println("Harga turun:");
        hargaTurun.forEach(p -> System.out.println("  " + p.nama() + " - " + p.harga()));
    }
}