Operasi terminal adalah pemanggilan yang mengakhiri pipeline stream, menggerakkan data dari sumbernya, dan menghasilkan nilai, koleksi, atau efek samping yang dikehendaki. Tanpa satu pun operasi terminal, pipeline yang sudah tersusun rapi tidak pernah berjalan. Topik ini membedah operasi terminal yang paling sering dipakai, yaitu forEach, count, keluarga anyMatch, reduce untuk melipat aliran menjadi satu nilai, dan collect untuk menampung hasilnya, lengkap dengan pertimbangan memilih antara reduce dan collect.
forEach, count, dan keluarga pemeriksaan#
Operasi paling sederhana adalah forEach yang menjalankan satu aksi pada tiap elemen tanpa mengembalikan apa pun, sehingga posisinya selalu di ujung pipeline. Program berikut memakai forEach untuk mencetak hasil filter, lalu count, anyMatch, dan allMatch untuk menjawab pertanyaan tentang seluruh aliran tanpa perulangan manual.
import java.util.List;
public class DemoTerminal {
public static void main(String[] args) {
List<Integer> nilai = List.of(72, 85, 60, 91, 48);
System.out.println("forEach pada stream hasil filter:");
nilai.stream()
.filter(n -> n >= 70)
.forEach(n -> System.out.println(" lulus: " + n));
long banyakLulus = nilai.stream().filter(n -> n >= 70).count();
System.out.println("count : " + banyakLulus);
boolean adaSempurna = nilai.stream().anyMatch(n -> n == 100);
boolean semuaTerisi = nilai.stream().allMatch(n -> n >= 40);
System.out.println("anyMatch nilai 100 : " + adaSempurna);
System.out.println("allMatch >= 40 : " + semuaTerisi);
}
}forEach pada stream hasil filter:
lulus: 72
lulus: 85
lulus: 91
count : 3
anyMatch nilai 100 : false
allMatch >= 40 : trueBentuk forEach di atas pada dasarnya menyembunyikan perulangan for-each yang sudah dikenal, dan pemakaiannya yang wajar adalah mencetak, menulis, atau mengirim, bukan membangun koleksi. Sebaliknya, count, anyMatch, dan allMatch mengembalikan jawaban langsung atas seluruh aliran. Ketiganya termasuk keluarga pemeriksaan short-circuit, yang berarti pemeriksaan berhenti segera setelah jawabannya pasti, misalnya anyMatch berhenti pada elemen pertama yang lolos tanpa memeriksa sisanya. Pasangannya noneMatch menjawab benar bila tidak ada satu pun elemen yang lolos syarat.
reduce: melipat aliran menjadi satu nilai#
Operasi reduce melipat seluruh elemen menjadi satu nilai dengan mengulang satu fungsi penggabung dari ujung ke ujung. Bentuk pertamanya menerima nilai awal (identity) dan penggabung, sehingga hasilnya langsung bertipe elemen. Bentuk kedua, tanpa nilai awal, mengembalikan Optional karena stream bisa saja kosong. Program berikut memakai keduanya, ditambah dua cara menampung hasil ke List.
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class DemoReduceCollect {
public static void main(String[] args) {
List<Integer> nilai = List.of(72, 85, 60, 91);
int total = nilai.stream().reduce(0, Integer::sum);
System.out.println("Total (identity) : " + total);
Optional<Integer> maksimum = nilai.stream().reduce(Integer::max);
System.out.println("Maks (Optional) : " + maksimum);
List<Integer> tinggi = nilai.stream()
.filter(n -> n >= 80)
.collect(Collectors.toList());
System.out.println("Collect toList : " + tinggi);
List<Integer> sama = nilai.stream().filter(n -> n >= 80).toList();
System.out.println("Stream.toList() : " + sama);
}
}Total (identity) : 308
Maks (Optional) : Optional[91]
Collect toList : [85, 91]
Stream.toList() : [85, 91]Penjumlahan di atas bekerja dengan menggabungkan 0 ke elemen pertama, hasil itu ke elemen kedua, dan seterusnya, sehingga Integer::sum dipanggil empat kali. Pemanggilan reduce(Integer::max) tanpa nilai awal menjawab Optional[91] karena pembungkus Optional-nya melindungi stream yang kosong, dan orElse dari topik Optional tinggal dipakai bila nilai langsungnya dibutuhkan. Untuk fungsi penggabungnya, pilih yang asosiatif dan tidak bergantung urutan. Stream berhak mengelompokkan pekerjaan saat paralel, sehingga pengurangan yang hasilnya berubah bila urutannya diacak bukan penggabung yang aman.
Bentuk ketiga reduce melengkapi dua bentuk di atas dengan fungsi penggabung hasil yang beroperasi saat stream paralel. Argumennya terdiri atas nilai awal, akumulator antarelemen, dan penggabung antarhasil sebagian, misalnya reduce(0, Integer::sum, Integer::sum) yang menggabungkan subtotal tiap bagian menjadi total akhir. Pada stream sekuensial, penggabung hasil itu hampir tidak pernah dipanggil, sedangkan pada stream paralel justru di sanalah fungsinya, sehingga bentuk tiga argumen ini terutama relevan untuk pekerjaan paralel.
Pada bagian collect, Collectors.toList() adalah cara klasik menampung aliran ke daftar, sedangkan Stream.toList() yang hadir sejak Java 16 menyingkat penulisannya. Hasil Stream.toList() tidak dapat diubah isinya, sehingga keduanya perlu dibedakan bila daftar hasilnya akan dimutasi. Pada contoh di atas, isi keduanya identik karena hanya dibaca kembali.
Memilih operasi terminal yang tepat#
Tabel berikut merangkum operasi terminal yang paling sering dipakai beserta tipe kembaliannya, sehingga pilihan operasinya dapat disesuaikan dengan bentuk jawaban yang diinginkan.
| Operasi | Tipe kembalian | Cocok untuk |
|---|---|---|
forEach | tidak ada | aksi per elemen seperti mencetak atau menulis |
count | long | menghitung banyaknya elemen |
anyMatch, allMatch, noneMatch | boolean | memeriksa syarat dengan berhenti lebih awal |
reduce | nilai atau Optional | melipat elemen menjadi satu nilai |
collect | koleksi atau wadah lain | menampung elemen ke List, Map, dan bentuk lainnya |
findFirst | Optional | mengambil elemen pertama yang lolos |
Aturan memilihnya cukup sederhana. Bila jawabannya satu angka atau satu nilai kebenaran, pakai count, reduce, atau keluarga Match, karena ketiganya langsung menjawab pertanyaan atas seluruh aliran. Bila jawabannya sekumpulan elemen, pakai collect, yang variasi-variasinya diperdalam pada topik Collector. Bila tujuannya efek samping seperti pencetakan, forEach yang tepat, dan sebaiknya tidak dipakai untuk membangun koleksi karena collect menyediakan caranya yang benar.
Kesalahan umum#
- Membangun koleksi lewat
forEachdengan daftar di luar lambda. Pola ini menyisakan efek samping dan hasilnya tergantung urutan pemrosesan, terlebih saat stream paralel. Gunakancollectyang menampung hasilnya dengan benar. - Memakai stream yang sudah dikonsumsi. Setelah operasi terminal dipanggil, stream itu tertutup, dan pemakaian ulangnya melempar
IllegalStateException. Bangun pipeline baru dari sumbernya bila jawaban lain dibutuhkan. - Memakai penggabung
reduceyang tergantung urutan. Fungsi semacam pengurangan menghasilkan jawaban yang berbeda-beda bila urutan pemrosesannya berubah. Pilih penggabung yang asosiatif seperti penjumlahan atau pengambilan maksimum. - Mengira
forEachmemiliki nilai kembalian. Operasi ini bertipevoid, sehingga tidak bisa dirangkai dan tidak bisa ditampung. Bila hasilnya perlu dipakai, pilihcollectataureduce.
Ringkasan#
- Operasi terminal menggerakkan seluruh pipeline, dan satu stream hanya boleh dikonsumsi satu kali.
forEachmenjalankan aksi tanpa kembalian,countmenghitung elemen, sedangkananyMatch,allMatch, dannoneMatchmenjawab pemeriksaan dengan berhenti lebih awal.reducemelipat aliran menjadi satu nilai, dan bentuk tanpa nilai awal menjawabOptionalagar stream kosong tetap aman.collectmenampung elemen ke koleksi, denganStream.toList()sebagai penyingkat bentuk yang paling lazim sejak Java 16.
Referensi#
- Stream (Java SE 21 & JDK 21) — Oracle — kontrak operasi terminal seperti forEach, reduce, dan collect pada dokumentasi resmi JDK 21.
- The Java Tutorials: Aggregate Operations — Oracle — penjelasan reduce dan collect beserta contohnya pada tutorial resmi Java.