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 antaraTugas
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 / dropWhilememotong berdasarkan kondisi berurutan
peek(aksi)mengintip elemen yang melintas
Operasi akhirHasil
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 / noneMatchboolean 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:

java BuktiLazy.java
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:

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

  1. Baris 15-18 (tahap 1): tiga operasi antara dipanggil berurutan — peek, filter, map — lalu hasilnya dibuang begitu saja. JVM hanya membangun deskripsi pipeline (objek ReferencePipeline bertingkat); tidak ada elemen yang dibaca dari produk.
  2. Baris 19: satu-satunya keluaran tahap 1 adalah baris penjelasan dari println luar. Lambda p -> System.out.println("filter melihat: ...") tidak pernah tereksekusi — bukti sifat lazy.
  3. Baris 22-26 (tahap 2): pipeline serupa ditutup toList(). Barulah JVM menarik elemen dari sumber, menjalankan filter, lalu peek mencetak produk yang lolos — hanya Kopi Arabika yang harganya minimal 50000.
  4. Baris 24 (posisi peek): karena peek diletakkan setelah filter, lambda-nya hanya melihat elemen yang lolos saringan. Memindahkan peek ke 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:

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

  1. Baris 16-18 (anyMatch): peek mencatat tiap elemen yang diperiksa. Urutannya berhenti di Kopi Arabika — begitu anyMatch menemukan harga 85000 yang memenuhi syarat, Madu Hutan tidak pernah diambil dari List. Jawaban true sudah pasti; memeriksa sisanya hanya membuang waktu.
  2. Baris 22-26 (limit(2)): meski sumber berisi empat produk, peek hanya mencetak dua baris. limit memotong aliran setelah dua elemen, sehingga map dan toList pun hanya memproses dua nama.
  3. Kombinasi keduanya: pola stream tak berujung + limit yang kita pakai pada Chapter Creating Streams bekerja karena limit adalah operasi antara berhenti dini — sumber iterate baru 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 berkas
java
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);
    }
}

BuktiLazy.java

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