Pada Chapter Stream Fundamentals kita sempat menyebut bahwa pipeline baru benar-benar bekerja ketika operasi akhir dipanggil. Sekarang saatnya membuktikan klaim tersebut dan memahami pembagian kerja di baliknya. Operasi stream terbagi menjadi dua keluarga besar: operasi antara (intermediate operations) yang menyusun pipeline tanpa memproses satu elemen pun, dan operasi akhir (terminal operations) yang memicu eksekusi seluruh rantai. Pada Chapter ini kita akan melihat bukti nyata sifat lazy tersebut, memakai peek sebagai jendela inspeksi, lalu mengenal operasi akhir yang pandai berhenti lebih awal.
Dua Keluarga Operasi #
Operasi antara selalu mengembalikan objek Stream baru — itulah tanda pengenalnya di dokumentasi maupun di editor. Karena kembaliannya masih Stream, operasi antara dapat dirantai: .filter(...).map(...).sorted(). Operasi antara hanya mencatat apa yang harus dilakukan; tidak satu elemen pun diambil dari sumber pada saat pemanggilan. Operasi akhir sebaliknya mengembalikan sesuatu yang bukan Stream — List, long, boolean, Optional, atau tidak sama sekali untuk forEach — dan pemanggilannya menandai stream sebagai terpakai, seperti aturan sekali pakai yang sudah kita buktikan.
Tabel berikut merangkum kedua keluarga operasi yang akan kita bahas sepanjang Section ini:
| Operasi antara | Tugas |
|---|---|
filter(predikat) | menyaring elemen yang lolos kondisi |
map(fungsi) | mengubah tiap elemen menjadi nilai baru |
flatMap(fungsi) | mengubah lalu meratakan struktur bersarang |
sorted() / sorted(komparator) | mengurutkan elemen |
distinct() | membuang elemen duplikat |
limit(n) / skip(n) | memotong atau melompati sebagian elemen |
takeWhile / dropWhile | memotong berdasarkan kondisi berurutan |
peek(aksi) | mengintip elemen yang melintas |
| Operasi akhir | Hasil |
|---|---|
forEach(aksi) | menjalankan aksi per elemen, tanpa nilai kembali |
toList() / toArray() | List / array berisi elemen hasil |
count() | jumlah elemen sebagai long |
min(komparator) / max(komparator) | Optional<T> elemen terkecil / terbesar |
reduce(...) | satu nilai gabungan dari semua elemen |
collect(...) | menampung elemen ke collection atau container lain |
anyMatch / allMatch / noneMatch | boolean hasil uji kondisi |
findFirst() / findAny() | Optional<T> elemen pertama yang ditemukan |
Bukti Nyata bahwa Pipeline itu Lazy #
Teori paling meyakinkan adalah demonstrasi. Program berikut menyusun pipeline lengkap — peek, filter, map — tetapi sengaja tidak memanggil operasi akhir pada tahap pertama. Jika stream bekerja seperti loop biasa, lambda di dalam peek akan mencetak tiga baris. Kenyataannya tidak ada satu pun baris tercetak sampai toList() hadir pada tahap kedua:
import java.util.List;
public class BuktiLazy {
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)
);
System.out.println("Tahap 1: menyusun pipeline TANPA operasi akhir");
produk.stream()
.peek(p -> System.out.println(" filter melihat: " + p.nama()))
.filter(p -> p.harga() >= 50000)
.map(Produk::nama);
System.out.println(" (tidak ada baris 'filter melihat' di atas -- belum ada yang dieksekusi)");
System.out.println("Tahap 2: pipeline serupa DENGAN operasi akhir");
List<String> hasil = produk.stream()
.filter(p -> p.harga() >= 50000)
.peek(p -> System.out.println(" lolos saringan: " + p.nama()))
.map(Produk::nama)
.toList();
System.out.println(" hasil: " + hasil);
}
}Output yang Dihasilkan:
Tahap 1: menyusun pipeline TANPA operasi akhir
(tidak ada baris 'filter melihat' di atas -- belum ada yang dieksekusi)
Tahap 2: pipeline serupa DENGAN operasi akhir
lolos saringan: Kopi Arabika
hasil: [Kopi Arabika]Mari kita bedah dua tahap tersebut:
- Baris 15-18 (tahap 1): tiga operasi antara dipanggil berurutan —
peek,filter,map— lalu hasilnya dibuang begitu saja. JVM hanya membangun deskripsi pipeline (objekReferencePipelinebertingkat); tidak ada elemen yang dibaca dariproduk. - Baris 19: satu-satunya keluaran tahap 1 adalah baris penjelasan dari
printlnluar. Lambdap -> System.out.println("filter melihat: ...")tidak pernah tereksekusi — bukti sifat lazy. - Baris 22-26 (tahap 2): pipeline serupa ditutup
toList(). Barulah JVM menarik elemen dari sumber, menjalankanfilter, lalupeekmencetak produk yang lolos — hanyaKopi Arabikayang harganya minimal 50000. - Baris 24 (posisi
peek): karenapeekdiletakkan setelahfilter, lambda-nya hanya melihat elemen yang lolos saringan. Memindahkanpeekke posisi lain mengubah apa yang terlihat — alat diagnosis yang berguna saat merakit pipeline.
Dua perilaku penting mengikuti dari sifat lazy ini. Pertama, pipeline tanpa operasi akhir adalah kode diam-diam yang tidak bekerja — compiler menerima kode tersebut tanpa keluhan, sehingga bug “lupa menutup pipeline” hanya terlihat dari hasil yang tidak pernah muncul. Kedua, JVM dapat menggabungkan operasi menjadi satu putaran yang efisien: pada contoh tahap 2, satu elemen mengalir melewati filter dan map sebelum elemen berikutnya diambil, bukan filter menyelesaikan semua elemen dulu lalu map mengulang dari awal.
Operasi Akhir yang Berhenti Dini #
Sebagian operasi akhir tidak perlu membaca seluruh stream untuk menghasilkan jawaban — keluarga ini disebut short-circuiting. anyMatch cukup menemukan satu kecocokan, findFirst cukup mengambil elemen pertama, dan limit memotong aliran di tengah. Operasi seperti ini berhenti secepat jawabannya sudah pasti, sehingga stream tak berujung dari iterate atau generate tetap aman dipakai. Program berikut membuktikan bahwa elemen setelah titik berhenti tidak pernah disentuh:
import java.util.List;
public class BerhentiDini {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Air Mineral", "minuman", 8000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Madu Hutan", "makanan", 120000)
);
System.out.println("anyMatch: apakah ada produk dengan harga >= 80000?");
boolean ada = produk.stream()
.peek(p -> System.out.println(" diperiksa: " + p.nama()))
.anyMatch(p -> p.harga() >= 80000);
System.out.println(" jawaban: " + ada);
System.out.println("limit: hanya memproses dua elemen pertama");
List<String> dua = produk.stream()
.peek(p -> System.out.println(" masuk pipeline: " + p.nama()))
.limit(2)
.map(Produk::nama)
.toList();
System.out.println(" hasil: " + dua);
}
}Output yang Dihasilkan:
anyMatch: apakah ada produk dengan harga >= 80000?
diperiksa: Air Mineral
diperiksa: Teh Hijau
diperiksa: Kopi Arabika
jawaban: true
limit: hanya memproses dua elemen pertama
masuk pipeline: Air Mineral
masuk pipeline: Teh Hijau
hasil: [Air Mineral, Teh Hijau]Mari kita amati kapan masing-masing pipeline berhenti:
- Baris 16-18 (
anyMatch):peekmencatat tiap elemen yang diperiksa. Urutannya berhenti diKopi Arabika— begituanyMatchmenemukan harga 85000 yang memenuhi syarat,Madu Hutantidak pernah diambil dariList. Jawabantruesudah pasti; memeriksa sisanya hanya membuang waktu. - Baris 22-26 (
limit(2)): meski sumber berisi empat produk,peekhanya mencetak dua baris.limitmemotong aliran setelah dua elemen, sehinggamapdantoListpun hanya memproses dua nama. - Kombinasi keduanya: pola
stream tak berujung + limityang kita pakai pada Chapter Creating Streams bekerja karenalimitadalah operasi antara berhenti dini — sumberiteratebaru menghasilkan nilai berikutnya selama masih dibutuhkan.
Dengan peta dua keluarga operasi di tangan, kita siap mengulas anggota-anggotanya satu per satu. Chapter berikutnya membahas filter — operasi antara pertama dan paling sering dipakai — mulai dari bentuk Predicate sederhananya sampai strategi menyaring record dengan banyak kriteria.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BerhentiDini.java
Unduh berkasimport java.util.List;
public class BerhentiDini {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Air Mineral", "minuman", 8000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Madu Hutan", "makanan", 120000)
);
System.out.println("anyMatch: apakah ada produk dengan harga >= 80000?");
boolean ada = produk.stream()
.peek(p -> System.out.println(" diperiksa: " + p.nama()))
.anyMatch(p -> p.harga() >= 80000);
System.out.println(" jawaban: " + ada);
System.out.println("limit: hanya memproses dua elemen pertama");
List<String> dua = produk.stream()
.peek(p -> System.out.println(" masuk pipeline: " + p.nama()))
.limit(2)
.map(Produk::nama)
.toList();
System.out.println(" hasil: " + dua);
}
}
BuktiLazy.java
Unduh berkasimport java.util.List;
public class BuktiLazy {
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)
);
System.out.println("Tahap 1: menyusun pipeline TANPA operasi akhir");
produk.stream()
.peek(p -> System.out.println(" filter melihat: " + p.nama()))
.filter(p -> p.harga() >= 50000)
.map(Produk::nama);
System.out.println(" (tidak ada baris 'filter melihat' di atas -- belum ada yang dieksekusi)");
System.out.println("Tahap 2: pipeline serupa DENGAN operasi akhir");
List<String> hasil = produk.stream()
.filter(p -> p.harga() >= 50000)
.peek(p -> System.out.println(" lolos saringan: " + p.nama()))
.map(Produk::nama)
.toList();
System.out.println(" hasil: " + hasil);
}
}