Streams: Operasi Perantara

Membangun pipeline dengan filter, map, sorted, dan distinct sambil memahami sifat lazy streams.

Daftar isi

Operasi perantara (intermediate operation) adalah langkah di tengah pipeline stream yang mengubah aliran data, misalnya menyaring, mengubah, mengurutkan, atau membuang yang kembar, dan selalu mengembalikan stream baru agar langkah berikutnya dapat dirangkai. Topik ini membangun pipeline dengan filter, map, sorted, dan distinct, lalu membuktikan sifat lazy yang membuat seluruh rangkaian baru bekerja ketika operasi terminalnya dipanggil. Setelah membacanya, susunan pipeline yang efisien sekaligus perilaku eksekusinya dapat diprediksi dengan tepat.

Membangun pipeline dari operasi perantara#

Pipeline dibaca dari kiri ke kanan, dimulai dari sumber data, lalu setiap operasi perantara mengembalikan stream baru yang membawa catatan langkah-langkah sebelumnya. Sumbernya tidak pernah berubah, dan tidak ada satu pun elemen yang diproses ketika rangkaiannya ditulis, karena seluruh baris itu hanyalah deklarasi bentuk aliran data. Contoh berikut menyaring nilai ujian, membuang yang kembar, mengurutkannya, lalu menampung hasilnya.

java DemoPerantara.java
import java.util.List;

public class DemoPerantara {

    public static void main(String[] args) {
        List<Integer> nilai = List.of(72, 85, 60, 91, 72, 48, 77);

        List<Integer> lulus = nilai.stream()
                .filter(n -> n >= 70)   // buang nilai di bawah ambang
                .distinct()             // buang yang kembar
                .sorted()               // urutkan menaik
                .toList();

        System.out.println("Nilai lulus unik terurut: " + lulus);

        List<String> label = nilai.stream()
                .filter(n -> n >= 70)
                .distinct()
                .sorted()
                .map(n -> "nilai " + n)
                .toList();
        System.out.println("Label: " + label);
    }
}
text Keluaran
Nilai lulus unik terurut: [72, 77, 85, 91]
Label: [nilai 72, nilai 77, nilai 85, nilai 91]

Nilai 72 yang muncul dua kali tersisa satu karena distinct, dan hasil akhirnya berurut menaik karena sorted. Rangkaian kedua menambahkan map untuk menunjukkan bahwa operasi perantara boleh dirangkai lebih dari satu, dengan tiap langkah mengubah aliran menjadi bentuk yang dibutuhkan langkah berikutnya. Adapun sorted tanpa argumen memakai urutan alami Comparable, sedangkan urutan khusus cukup diberikan lewat pembanding Comparator yang sudah dikenal pada modul Collections.

Saat mengubah angka, tiga turunan map bernama mapToInt, mapToLong, dan mapToDouble menghasilkan stream primitif yang menghemat autoboxing dan membuka operasi semacam sum serta average tanpa collector. Sebaliknya, operasi peek menerima aksi tanpa mengubah aliran sama sekali, sehingga tempatnya yang wajar adalah menyelidiki urutan pemrosesan saat pipeline terasa misterius, bukan membangun logika di dalamnya. Keduanya menyempurnakan kotak peralatan perantara tanpa mengubah kontrak utamanya, yaitu mengembalikan aliran yang siap dirangkai lagi.

java Contoh: pengurutan dengan pembanding khusus
// tidak-dijalankan
List<Mahasiswa> terurut = kelas.stream()
        .sorted(Comparator.comparingInt(Mahasiswa::nilai).reversed())
        .toList();

Gambar berikut merangkum bentuk pipeline itu beserta batas antara operasi perantara dan operasi terminalnya.

sumber data72 85 60 9172 48 77filtern >= 7072 85 91 72 77distinct72 85 91 77sorted72 77 85 91toList()terminaloperasi perantara: baru menyiapkan langkah, belum menarik satu pun datasaat terminal dipanggil, data mengalir satu per satu melewati seluruh langkah
Pipeline stream dari sumber data sampai operasi terminal

Sifat lazy dan peran operasi terminal#

Operasi perantara bersifat lazy, yang berarti hanya mencatat langkah tanpa menarik satu pun elemen dari sumbernya. Pemicu kerjanya adalah operasi terminal, yaitu pemanggilan yang mengembalikan nilai selain stream dan menuntut aliran data bergerak. Program berikut menangkap buktinya dengan mencetak sesuatu dari dalam filter.

java BuktiLazy.java
import java.util.List;
import java.util.stream.Stream;

public class BuktiLazy {

    public static void main(String[] args) {
        List<Integer> nilai = List.of(80, 55, 90, 62);

        System.out.println("1) Pipeline dibangun tanpa operasi terminal:");
        Stream<Integer> siapFilter = nilai.stream()
                .filter(n -> {
                    System.out.println("   filter memeriksa " + n);
                    return n >= 70;
                });
        System.out.println("   (tidak ada satu pun baris filter di atas)");

        System.out.println("2) Operasi terminal count() dipanggil:");
        long lolos = siapFilter.count();
        System.out.println("   Lolos filter: " + lolos);
    }
}
text Keluaran
1) Pipeline dibangun tanpa operasi terminal:
   (tidak ada satu pun baris filter di atas)
2) Operasi terminal count() dipanggil:
   filter memeriksa 80
   filter memeriksa 55
   filter memeriksa 90
   filter memeriksa 62
   Lolos filter: 2

Tidak ada baris filter memeriksa yang muncul pada tahap pertama, padahal pipeline-nya sudah terbentuk utuh di memori. Begitu count() dipanggil, baris itu bermunculan seiring elemen ditarik satu per satu dari sumbernya. Sifat lazy membuat stream hemat untuk pekerjaan yang hasilnya belum tentu dipakai. Pada rangkaian mahal yang hanya perlu elemen pertamanya, langkah-langkah di belakangnya tidak pernah dijalankan bila tidak diperlukan.

Peraga interaktif berikut meniru mekanisme di atas, susun operasi perantaranya, jalankan terminalnya, lalu perhatikan urutan baris yang muncul pada catatan eksekusinya.

Aliran data satu per satu#

Bukti lazy lainnya lebih halus, yaitu urutan pemrosesan yang berjalan per elemen, bukan per tahap. Intuisi dari perulangan biasa menyarankan filter memproses semua elemen dahulu, baru map memproses semuanya, dan kenyataannya berbeda. Program berikut mencetak jejak di kedua operasi agar urutannya terbaca.

java AlurElemen.java
import java.util.List;

public class AlurElemen {

    public static void main(String[] args) {
        List<Integer> nilai = List.of(80, 55, 90);

        List<Integer> hasil = nilai.stream()
                .filter(n -> {
                    System.out.println("filter: " + n);
                    return n >= 70;
                })
                .map(n -> {
                    System.out.println("  map : " + n);
                    return n + 5;
                })
                .toList();

        System.out.println("Hasil: " + hasil);
    }
}
text Keluaran
filter: 80
  map : 80
filter: 55
filter: 90
  map : 90
Hasil: [85, 95]

Keluarannya memperlihatkan pola filter lalu map untuk angka 80, kemudian filter saja untuk 55 yang dibuang sebelum sempat menyentuh map, dan seterusnya. Satu elemen mengalir menembus seluruh pipeline sebelum elemen berikutnya mulai, dan itulah alasan stream dapat berhenti lebih awal pada operasi semacam findFirst. Konsekuensi praktisnya, langkah yang murah dan sering membuang data sebaiknya diletakkan di depan agar langkah-langkah berikutnya hanya bekerja pada sisa yang lebih sedikit.

Kesalahan umum#

  • Lupa operasi terminal. Pipeline tanpa terminal tidak pernah dijalankan dan tidak memunculkan error apa pun, sehingga kesalahannya sulit terlihat. Pastikan setiap stream diakhiri operasi terminal.
  • Memakai ulang stream yang sudah dikonsumsi. Setelah terminal dipanggil, stream itu tertutup dan pemakaian ulangnya melempar IllegalStateException. Bangun pipeline baru dari sumbernya bila hasil lain dibutuhkan.
  • Menuliskan lambda yang mengubah keadaan di luar dirinya. Lambda yang menambahkan ke daftar atau menghitung sesuatu pada variabel luar membuat hasil tergantung urutan pemrosesan. Buat tiap lambda mandiri tanpa efek samping.
  • Mengira sorted() mengurutkan sumbernya. Urutan hanya berlaku pada aliran yang diteruskan ke langkah berikutnya, sedangkan List sumbernya tetap pada urutan semula. Kumpulkan hasilnya ke koleksi baru bila urutan itu ingin disimpan.

Ringkasan#

  • Operasi perantara mengembalikan stream baru dan hanya mencatat langkah, sedangkan pekerjaannya dipicu operasi terminal.
  • filter menyaring, map mengubah, sorted mengurutkan, dan distinct membuang kembar, dan semuanya dapat dirangkai sesuai kebutuhan.
  • Data mengalir satu per satu menembus seluruh pipeline, bukan tahap demi tahap, sehingga filter di depan memangkas kerja langkah-langkah belakang.
  • Sumber data tidak pernah berubah oleh pipeline, dan urutan yang dihasilkan sorted hanya hidup di dalam aliran sampai ditampung.

Referensi#