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:
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? : trueMari kita bedah kedua pipeline tersebut:
- Baris 17-19 (
sekaliSaring): satufilterdengan 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. - Baris 23-26 (
duaTahap): kriteria yang sama dipecah menjadi duafilterberuntun — saring kategori dulu, baru saring harga. Urutan elemen hasil tetap mengikuti urutan sumber karena stream berurutan memelihara encounter order. - Baris 29 (
sekaliSaring.equals(duaTahap)): baris ini membuktikan kedua gaya menghasilkanListyang setara —truetercetak. RecordProdukmenghasilkanequalsotomatis, sehingga perbandingan isiListrecord juga berlangsung elemen demi elemen. - 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
filterberuntun 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 hargap.harga() >= 10000 && p.harga() <= 100000. - Hindari
filteryang sama dipanggil dua kali pada pipeline berbeda — ekstrakPredicateke 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:
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:
- Baris 9-11 (
method murah): kriteria harga ditulis sekali sebagai methodstaticyang mengembalikanboolean. Method seperti ini otomatis cocok dengan tipePredicate<Produk>lewat method reference. - Baris 13-15 (
namaSaja): satu helper untuk semua saringan — menerimaPredicateapa pun, menjalankanfilter+map+toList, dan mengembalikan daftar nama. Tiga laporan dimainmemakai helper yang sama dengan kriteria berbeda. - Baris 26-27: dua
Predicatebernama —minumandari lambda,hargaWajardari method referenceSaringLanjutan::murah. Keduanya setara secara tipe; pilih bentuk yang paling ringkas untuk konteksnya. - Baris 29 (
minuman.and(hargaWajar)): kombinasi dan — hanya Teh Hijau dan Air Mineral yang merupakan minuman sekaligus murah. - Baris 30 (
minuman.or(hargaWajar)): kombinasi atau — semua minuman ditambah Roti Gandum yang murah; Madu Hutan tereliminasi karena mahal dan bukan minuman. - Baris 31 (
Predicate.not(...)): pembalikan kriteria denganPredicate.not, padananmurah.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 berkasimport 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 berkasimport 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));
}
}