Stream memberi kita pipeline yang terbaca dan bebas variabel kendali manual, tetapi tiap tahap pipeline adalah objek dan tiap elemen adalah perjalanan melewati tahap-tahap itu. Pada data kecil logika sederhana, perulangan for sering tetap lebih ringkas jalurnya; pada data besar dengan short-circuit, stream menang otomatis. Chapter Performa Stream di section Stream API sudah membuktikan hal itu dengan menghitung elemen yang diproses dan biaya boxing. Kali ini kita menyisir struktur pipeline itu sendiri: berapa kali elemen dikunjungi tiap tahap, dan berapa pembandingan yang dibayar sorted() — semuanya dihitung dari eksekusi sungguhan atas data tetap.
Kunjungan Tahap dan Predikat #
Setiap intermediate operation membangun satu tahap pipeline. Dua filter berurutan berarti elemen yang lolos filter pertama dikunjungi lagi di filter kedua — predikat kedua hanya menerima output tahap pertama. Menggabungkan dua predikat yang saling terkait ke dalam satu filter memangkas satu lapis kunjungan sekaligus satu objek tahap. Prinsip penyaringan kasar-dulu juga membantu: letakkan predikat termurah dan paling banyak menyaring di depan, sehingga tahap-tahap sesudahnya menerima lebih sedikit elemen.
Mekanisme di balik layar tetap sama dengan yang kita bahas di section Stream API: laziness menyatukan seluruh tahap dalam satu perjalanan elemen, dan terminal operation menarik data lewat rantai tahap itu sekali saja.
Pembandingan yang Dibayar sorted #
sorted() adalah intermediate operation termahal di stream: seluruh elemen ditampung ke array internal sebelum satu pun hasil dikeluarkan, dan pengurutan membandingkan elemen berulang kali. Yang sering terlupakan: List.sort() pada JDK memakai TimSort, algoritma yang mendeteksi run — deretan elemen yang sudah terurut, naik maupun turun — dan memanfaatkannya alih-alih mengurutkan ulang dari nol. Data yang hampir terurut jauh lebih murah diurutkan daripada data acak, dan data yang sudah kita ketahui urutannya paling murah bila sorted() tidak ditambahkan sama sekali.
Program berikut menghitung keduanya secara nyata — kunjungan tahap filter dihitung di dalam lambda, pembandingan sort dihitung lewat Comparator pembungkus:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.stream.IntStream;
/**
* Penghitung beban pipeline stream pada data tetap: kunjungan tahap
* filter (terpisah vs gabungan) dan jumlah pembandingan sort sungguhan
* lewat Comparator penghitung. Semua angka deterministik.
*/
public class BebanPipeline {
public static void main(String[] args) {
int n = 1000;
// ---- Bagian 1: kunjungan tahap antara ----
long[] kunjungan = {0};
long hasilPisah = IntStream.rangeClosed(1, n)
.filter(x -> {
kunjungan[0]++;
return x % 2 == 0;
})
.filter(x -> {
kunjungan[0]++;
return x > n / 2;
})
.count();
long kunjunganPisah = kunjungan[0];
kunjungan[0] = 0;
long hasilGabung = IntStream.rangeClosed(1, n)
.filter(x -> {
kunjungan[0]++;
return x % 2 == 0 && x > n / 2;
})
.count();
long kunjunganGabung = kunjungan[0];
System.out.println("== Bagian 1: kunjungan tahap filter (n = " + n + ") ==");
System.out.println("filter terpisah : " + kunjunganPisah
+ " kunjungan tahap, hasil " + hasilPisah);
System.out.println("filter gabungan : " + kunjunganGabung
+ " kunjungan tahap, hasil " + hasilGabung);
System.out.println();
// ---- Bagian 2: pembandingan pengurutan ----
int[] banding = {0};
Comparator<Integer> penghitung = (a, b) -> {
banding[0]++;
return Integer.compare(a, b);
};
List<Integer> menurun = new ArrayList<>();
for (int i = n; i >= 1; i--) {
menurun.add(i);
}
List<Integer> acak = new ArrayList<>();
for (int i = 1; i <= n; i++) {
acak.add(i);
}
Collections.shuffle(acak, new Random(42));
banding[0] = 0;
List<Integer> salinanAcak = new ArrayList<>(acak);
salinanAcak.sort(penghitung);
int bandingAcak = banding[0];
banding[0] = 0;
List<Integer> salinanTurun = new ArrayList<>(menurun);
salinanTurun.sort(penghitung);
int bandingTurun = banding[0];
banding[0] = 0;
List<Integer> menaik = new ArrayList<>();
for (int i = 1; i <= n; i++) {
menaik.add(i);
}
List<Integer> salinanNaik = new ArrayList<>(menaik);
salinanNaik.sort(penghitung);
int bandingNaik = banding[0];
System.out.println("== Bagian 2: pembandingan pengurutan (n = " + n + ") ==");
System.out.println("sorted() data acak : " + bandingAcak + " pembandingan");
System.out.println("sorted() data terurut menurun : " + bandingTurun
+ " pembandingan <- run menurun dibalik, bukan diurut ulang");
System.out.println("sorted() data terurut menaik : " + bandingNaik
+ " pembandingan <- TimSort memanfaatkan urutan yang ada");
System.out.println("limit(5) tanpa sorted() : 0 pembandingan");
System.out.println();
// ---- Kesimpulan ----
System.out.println("== Kesimpulan ==");
System.out.println("Predikat yang saling terkait lebih hemat digabung");
System.out.println("dalam satu filter; sorted() paling murah ketika");
System.out.println("kita tidak menambahnya sama sekali.");
}
}Output yang Dihasilkan:
== Bagian 1: kunjungan tahap filter (n = 1000) ==
filter terpisah : 1500 kunjungan tahap, hasil 250
filter gabungan : 1000 kunjungan tahap, hasil 250
== Bagian 2: pembandingan pengurutan (n = 1000) ==
sorted() data acak : 8688 pembandingan
sorted() data terurut menurun : 999 pembandingan <- run menurun dibalik, bukan diurut ulang
sorted() data terurut menaik : 999 pembandingan <- TimSort memanfaatkan urutan yang ada
limit(5) tanpa sorted() : 0 pembandingan
== Kesimpulan ==
Predikat yang saling terkait lebih hemat digabung
dalam satu filter; sorted() paling murah ketika
kita tidak menambahnya sama sekali.Mari kita telaah bagian-bagian kuncinya:
- Baris 21-31 (filter terpisah): kedua lambda menaikkan penghitung
kunjungansetiap dipanggil. Filter pertama dipanggil 1000 kali; filter kedua hanya untuk 500 elemen genap — total 1500 kunjungan tahap. - Baris 33-40 (filter gabungan): satu lambda menguji
x % 2 == 0 && x > n / 2dengan short-circuit&&, sehingga ujian kedua hanya berjalan untuk elemen genap. Total 1000 kunjungan — hasil identik 250 elemen dengan kunjungan tahap 33% lebih sedikit. - Baris 51-54 (
penghitung):Comparatoryang menaikkan penghitungbandingtiap pembandingan memberi kita jendela ke dalam TimSort tanpa mengubah perilaku pengurutan. - Baris 67-84 (tiga kualitas data): data hasil
shuffleber-seed membutuhkan 8.688 pembandingan, sedangkan data terurut menaik maupun menurun hanya 999 — satu pembandingan per pasangan tetangga untuk mengenali satu run utuh. Urutan yang sudah ada adalah modal;sorted()di atas data sengaja diacak adalah pemborosan.
Kapan Parallel Masuk Akal #
parallelStream membagi sumber data lewat Spliterator, memproses potongan-potongan itu di fork-join pool bersama, lalu menggabungkan hasilnya. Seluruh mesin itu bertambah biaya: pembagian, antrean tugas, dan penggabungan. Pada 1000 elemen dengan pekerjaan ringan, overhead pembagian bisa melampaui pekerjaannya sendiri; pada jutaan elemen dengan komputasi berat per elemen dan sumber yang terbagi rapi (array, ArrayList), paralelisme berbalas untung. Aturan penting yang sudah kita bahas pada Chapter Parallel Streams tetap berlaku: jangan blokir common pool dengan I/O, dan ukur dulu — angka kunjungan dan banding di atas adalah contoh cara menghitung biaya struktural sebelum memutuskan.
| Pola pipeline | Biaya struktural | Tindakan |
|---|---|---|
Rantai filter bertingkat | kunjungan tahap berlipat | gabungkan predikat terkait |
sorted() data hampir terurut | pembandingan minim (TimSort) | pertahankan urutan sumber |
sorted().limit(k) | sort penuh untuk sebagian kecil hasil | PriorityQueue berukuran k |
Stream<Integer> numerik | boxing per elemen | IntStream/DoubleStream |
Pipeline berurutan kita sudah ramping; sisa daya komputer modern ada di banyak core. Chapter berikutnya membahas performa concurrency: kontensi lock, rumus ukuran thread pool, dan kapan virtual threads mengubah perhitungan.
Referensi #
- Oracle: package java.util.stream (Java SE 25)
- Oracle: Interface Stream (Java SE 25)
- OpenJDK: JMH — Java Microbenchmark Harness
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BebanPipeline.java
Unduh berkasimport java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.stream.IntStream;
/**
* Penghitung beban pipeline stream pada data tetap: kunjungan tahap
* filter (terpisah vs gabungan) dan jumlah pembandingan sort sungguhan
* lewat Comparator penghitung. Semua angka deterministik.
*/
public class BebanPipeline {
public static void main(String[] args) {
int n = 1000;
// ---- Bagian 1: kunjungan tahap antara ----
long[] kunjungan = {0};
long hasilPisah = IntStream.rangeClosed(1, n)
.filter(x -> {
kunjungan[0]++;
return x % 2 == 0;
})
.filter(x -> {
kunjungan[0]++;
return x > n / 2;
})
.count();
long kunjunganPisah = kunjungan[0];
kunjungan[0] = 0;
long hasilGabung = IntStream.rangeClosed(1, n)
.filter(x -> {
kunjungan[0]++;
return x % 2 == 0 && x > n / 2;
})
.count();
long kunjunganGabung = kunjungan[0];
System.out.println("== Bagian 1: kunjungan tahap filter (n = " + n + ") ==");
System.out.println("filter terpisah : " + kunjunganPisah
+ " kunjungan tahap, hasil " + hasilPisah);
System.out.println("filter gabungan : " + kunjunganGabung
+ " kunjungan tahap, hasil " + hasilGabung);
System.out.println();
// ---- Bagian 2: pembandingan pengurutan ----
int[] banding = {0};
Comparator<Integer> penghitung = (a, b) -> {
banding[0]++;
return Integer.compare(a, b);
};
List<Integer> menurun = new ArrayList<>();
for (int i = n; i >= 1; i--) {
menurun.add(i);
}
List<Integer> acak = new ArrayList<>();
for (int i = 1; i <= n; i++) {
acak.add(i);
}
Collections.shuffle(acak, new Random(42));
banding[0] = 0;
List<Integer> salinanAcak = new ArrayList<>(acak);
salinanAcak.sort(penghitung);
int bandingAcak = banding[0];
banding[0] = 0;
List<Integer> salinanTurun = new ArrayList<>(menurun);
salinanTurun.sort(penghitung);
int bandingTurun = banding[0];
banding[0] = 0;
List<Integer> menaik = new ArrayList<>();
for (int i = 1; i <= n; i++) {
menaik.add(i);
}
List<Integer> salinanNaik = new ArrayList<>(menaik);
salinanNaik.sort(penghitung);
int bandingNaik = banding[0];
System.out.println("== Bagian 2: pembandingan pengurutan (n = " + n + ") ==");
System.out.println("sorted() data acak : " + bandingAcak + " pembandingan");
System.out.println("sorted() data terurut menurun : " + bandingTurun
+ " pembandingan <- run menurun dibalik, bukan diurut ulang");
System.out.println("sorted() data terurut menaik : " + bandingNaik
+ " pembandingan <- TimSort memanfaatkan urutan yang ada");
System.out.println("limit(5) tanpa sorted() : 0 pembandingan");
System.out.println();
// ---- Kesimpulan ----
System.out.println("== Kesimpulan ==");
System.out.println("Predikat yang saling terkait lebih hemat digabung");
System.out.println("dalam satu filter; sorted() paling murah ketika");
System.out.println("kita tidak menambahnya sama sekali.");
}
}