Operasi Terminal Streams

Mengakhiri pipeline dengan forEach, collect, reduce, count, dan anyMatch.

Daftar isi

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.

java DemoTerminal.java
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);
    }
}
text Keluaran
forEach pada stream hasil filter:
  lulus: 72
  lulus: 85
  lulus: 91
count  : 3
anyMatch nilai 100 : false
allMatch >= 40     : true

Bentuk 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.

java DemoReduceCollect.java
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);
    }
}
text Keluaran
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.

OperasiTipe kembalianCocok untuk
forEachtidak adaaksi per elemen seperti mencetak atau menulis
countlongmenghitung banyaknya elemen
anyMatch, allMatch, noneMatchbooleanmemeriksa syarat dengan berhenti lebih awal
reducenilai atau Optionalmelipat elemen menjadi satu nilai
collectkoleksi atau wadah lainmenampung elemen ke List, Map, dan bentuk lainnya
findFirstOptionalmengambil 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 forEach dengan daftar di luar lambda. Pola ini menyisakan efek samping dan hasilnya tergantung urutan pemrosesan, terlebih saat stream paralel. Gunakan collect yang 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 reduce yang tergantung urutan. Fungsi semacam pengurangan menghasilkan jawaban yang berbeda-beda bila urutan pemrosesannya berubah. Pilih penggabung yang asosiatif seperti penjumlahan atau pengambilan maksimum.
  • Mengira forEach memiliki nilai kembalian. Operasi ini bertipe void, sehingga tidak bisa dirangkai dan tidak bisa ditampung. Bila hasilnya perlu dipakai, pilih collect atau reduce.

Ringkasan#

  • Operasi terminal menggerakkan seluruh pipeline, dan satu stream hanya boleh dikonsumsi satu kali.
  • forEach menjalankan aksi tanpa kembalian, count menghitung elemen, sedangkan anyMatch, allMatch, dan noneMatch menjawab pemeriksaan dengan berhenti lebih awal.
  • reduce melipat aliran menjadi satu nilai, dan bentuk tanpa nilai awal menjawab Optional agar stream kosong tetap aman.
  • collect menampung elemen ke koleksi, dengan Stream.toList() sebagai penyingkat bentuk yang paling lazim sejak Java 16.

Referensi#