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.
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);
}
}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.
// 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.
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.
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);
}
}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: 2Tidak 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.
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);
}
}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.
filtermenyaring,mapmengubah,sortedmengurutkan, dandistinctmembuang 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
sortedhanya hidup di dalam aliran sampai ditampung.
Referensi#
- Stream (Java SE 21 & JDK 21) — Oracle — daftar operasi perantara dan terminal beserta kontrak masing-masing pada dokumentasi resmi JDK 21.
- The Java Tutorials: Aggregate Operations — Oracle — penjelasan pipeline, sifat lazy, dan operasi agregat pada tutorial resmi Java.