Hampir setiap olah data bermula dari pertanyaan yang sama: dari sekian banyak baris, mana saja yang kita inginkan? Operasi filter menjawabnya di dalam pipeline stream — menyaring elemen dengan sebuah kondisi boolean tanpa loop manual dan tanpa List sementara. Operasi antara ini adalah yang paling sering dipakai sehari-hari, sekaligus pintu masuk paling natural untuk menggabungkan kekuatan stream dengan Predicate yang sudah kita kenal di Section Functional Programming. Pada Chapter ini kita akan memakai filter pada record Produk, membandingkan satu kondisi majemuk dengan beberapa filter beruntun, lalu menyusun kriteria yang layak diberi nama.

Menyaring dengan Predicate #

Method filter pada Stream<T> menerima satu parameter bertipe Predicate<? super T> — fungsi dari elemen ke boolean. Setiap elemen diperiksa berurutan: yang menghasilkan true melanjutkan perjalanan ke operasi berikutnya, yang false terhenti di tahap tersebut. Karena sifat pipeline yang lazy, pemeriksaan hanya terjadi ketika operasi akhir memicu eksekusi, dan — seperti dibuktikan pada Chapter Intermediate vs Terminal Operations — elemen setelah titik berhenti dini tidak akan diperiksa sama sekali.

Bentuk paling ringkas memakai lambda langsung di dalam pipeline. Contoh berikut menyaring produk dengan dua kriteria sekaligus, lalu membandingkannya dengan versi dua-filter-beruntun yang kriterianya identik:

import java.util.List;

public class SaringProduk {

    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)
        );

        // Satu filter dengan kondisi majemuk &&
        List<Produk> sekaliSaring = produk.stream()
                .filter(p -> p.kategori().equals("minuman") && p.harga() <= 50000)
                .toList();
        System.out.println("Sekali saring : " + sekaliSaring);

        // Dua filter beruntun, kriteria yang sama
        List<Produk> duaTahap = produk.stream()
                .filter(p -> p.kategori().equals("minuman"))
                .filter(p -> p.harga() <= 50000)
                .toList();
        System.out.println("Dua tahap     : " + duaTahap);

        System.out.println("Hasil sama?   : " + sekaliSaring.equals(duaTahap));
    }
}

Output yang Dihasilkan:

text
Sekali saring : [Produk[nama=Teh Hijau, kategori=minuman, harga=45000], Produk[nama=Air Mineral, kategori=minuman, harga=8000]]
Dua tahap     : [Produk[nama=Teh Hijau, kategori=minuman, harga=45000], Produk[nama=Air Mineral, kategori=minuman, harga=8000]]
Hasil sama?   : true

Mari kita bedah kedua pipeline tersebut:

  1. Baris 17-19 (sekaliSaring): satu filter dengan lambda yang menggabungkan dua syarat memakai && — kategori harus "minuman" dan harga tidak boleh melebihi 50000. Dari lima produk, hanya Teh Hijau dan Air Mineral yang lolos.
  2. Baris 23-26 (duaTahap): kriteria yang sama dipecah menjadi dua filter beruntun — saring kategori dulu, baru saring harga. Urutan elemen hasil tetap mengikuti urutan sumber karena stream berurutan memelihara encounter order.
  3. Baris 29 (sekaliSaring.equals(duaTahap)): baris ini membuktikan kedua gaya menghasilkan List yang setara — true tercetak. Record Produk menghasilkan equals otomatis, sehingga perbandingan isi List record juga berlangsung elemen demi elemen.
  4. Cetakan Produk[...] pada baris 20 dan 27: representasi teks record yang dihasilkan otomatis oleh compiler menampilkan nama field lengkap dengan nilainya — berguna untuk memeriksa hasil saringan saat mengembangkan kode.

Satu Kondisi Majemuk atau Beberapa Filter Beruntun #

Kedua gaya pada contoh di atas setara secara hasil, dan keduanya tetap satu kali putaran — filter beruntun tidak membaca ulang data, melainkan menyusun dua tahap penyaringan yang dilewati tiap elemen secara berurutan. Perbedaannya bergaya, bukan fungsional. Lambda tunggal dengan && memeriksa dua syarat dalam satu pemanggilan; filter beruntun memanggil dua Predicate berbeda per elemen. Selisih performa untuk data ukuran wajar nyaris tidak terasa — pilihlah bentuk yang paling terbaca oleh tim kita.

Beberapa pertimbangan praktis membantu memilih:

  • Gunakan filter beruntun ketika tiap kriteria punya makna bisnis yang jelas dan ingin dibaca seperti daftar langkah — .filter(kategoriTerpilih).filter(hargaWajar) terbaca sebagai dua kebijakan yang diterapkan berurutan.
  • Gunakan satu lambda dengan &&/|| ketika kriteria merupakan satu konsep yang tidak layak dipecah, misalnya rentang harga p.harga() >= 10000 && p.harga() <= 100000.
  • Hindari filter yang sama dipanggil dua kali pada pipeline berbeda — ekstrak Predicate ke variabel atau method agar kriteria hidup di satu tempat dan bisa diuji terpisah.

Kriteria Bernama dan Kombinasinya #

Ketika kriteria menyaring muncul berulang di berbagai pipeline, lambda anonim mulai kehilangan keunggulannya: logika yang sama tersalin berkali-kali tanpa nama. Solusinya adalah menjadikan kriteria sebagai nilai kelas satu — variabel Predicate atau method static yang mengembalikan boolean — lalu menggabungkannya secara deklaratif dengan and, or, negate, serta Predicate.not. Program berikut merangkum pola tersebut:

import java.util.List;
import java.util.function.Predicate;

public class SaringLanjutan {

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

    // Kriteria bernama: mudah dibaca, diuji, dan dipakai ulang
    static boolean murah(Produk p) {
        return p.harga() <= 50000;
    }

    static List<String> namaSaja(List<Produk> sumber, Predicate<Produk> syarat) {
        return sumber.stream().filter(syarat).map(Produk::nama).toList();
    }

    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)
        );

        Predicate<Produk> minuman = p -> p.kategori().equals("minuman");
        Predicate<Produk> hargaWajar = SaringLanjutan::murah;

        System.out.println("minuman dan murah     : " + namaSaja(produk, minuman.and(hargaWajar)));
        System.out.println("minuman atau murah    : " + namaSaja(produk, minuman.or(hargaWajar)));
        System.out.println("tidak murah           : " + namaSaja(produk, Predicate.not(SaringLanjutan::murah)));
    }
}

Output yang Dihasilkan:

text
minuman dan murah     : [Teh Hijau, Air Mineral]
minuman atau murah    : [Kopi Arabika, Teh Hijau, Roti Gandum, Air Mineral]
tidak murah           : [Kopi Arabika, Madu Hutan]

Mari kita amati susunan program tersebut:

  1. Baris 9-11 (method murah): kriteria harga ditulis sekali sebagai method static yang mengembalikan boolean. Method seperti ini otomatis cocok dengan tipe Predicate<Produk> lewat method reference.
  2. Baris 13-15 (namaSaja): satu helper untuk semua saringan — menerima Predicate apa pun, menjalankan filter + map + toList, dan mengembalikan daftar nama. Tiga laporan di main memakai helper yang sama dengan kriteria berbeda.
  3. Baris 26-27: dua Predicate bernama — minuman dari lambda, hargaWajar dari method reference SaringLanjutan::murah. Keduanya setara secara tipe; pilih bentuk yang paling ringkas untuk konteksnya.
  4. Baris 29 (minuman.and(hargaWajar)): kombinasi dan — hanya Teh Hijau dan Air Mineral yang merupakan minuman sekaligus murah.
  5. Baris 30 (minuman.or(hargaWajar)): kombinasi atau — semua minuman ditambah Roti Gandum yang murah; Madu Hutan tereliminasi karena mahal dan bukan minuman.
  6. Baris 31 (Predicate.not(...)): pembalikan kriteria dengan Predicate.not, padanan murah.negate() yang terbaca lebih natural saat argumennya berupa method reference. Hasilnya produk dengan harga di atas 50000: Kopi Arabika dan Madu Hutan.

filter menjawab pertanyaan elemen mana yang melanjutkan perjalanan. Pertanyaan berikutnya adalah dalam bentuk apa elemen tersebut berlanjut — dan di situlah map bekerja. Chapter map dan flatMap menunjukkan cara mengubah tiap elemen, memetakan record ke salah satu fieldnya, hingga meratakan struktur bersarang dengan flatMap.

Referensi #

Berkas Sumber

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

SaringLanjutan.java

Unduh berkas
java
import java.util.List;
import java.util.function.Predicate;

public class SaringLanjutan {

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

    // Kriteria bernama: mudah dibaca, diuji, dan dipakai ulang
    static boolean murah(Produk p) {
        return p.harga() <= 50000;
    }

    static List<String> namaSaja(List<Produk> sumber, Predicate<Produk> syarat) {
        return sumber.stream().filter(syarat).map(Produk::nama).toList();
    }

    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)
        );

        Predicate<Produk> minuman = p -> p.kategori().equals("minuman");
        Predicate<Produk> hargaWajar = SaringLanjutan::murah;

        System.out.println("minuman dan murah     : " + namaSaja(produk, minuman.and(hargaWajar)));
        System.out.println("minuman atau murah    : " + namaSaja(produk, minuman.or(hargaWajar)));
        System.out.println("tidak murah           : " + namaSaja(produk, Predicate.not(SaringLanjutan::murah)));
    }
}

SaringProduk.java

Unduh berkas
java
import java.util.List;

public class SaringProduk {

    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)
        );

        // Satu filter dengan kondisi majemuk &&
        List<Produk> sekaliSaring = produk.stream()
                .filter(p -> p.kategori().equals("minuman") && p.harga() <= 50000)
                .toList();
        System.out.println("Sekali saring : " + sekaliSaring);

        // Dua filter beruntun, kriteria yang sama
        List<Produk> duaTahap = produk.stream()
                .filter(p -> p.kategori().equals("minuman"))
                .filter(p -> p.harga() <= 50000)
                .toList();
        System.out.println("Dua tahap     : " + duaTahap);

        System.out.println("Hasil sama?   : " + sekaliSaring.equals(duaTahap));
    }
}