Saat pipeline diakhiri collect(Collectors.toList()), hasil List selalu tampil rapi sesuai urutan tertentu — tetapi urutan itu datang dari mana? Jawabannya adalah encounter order: urutan tempat stream bertemu elemen-elemennya, yang ditentukan oleh sumber data dan bisa dimodifikasi oleh operasi tertentu. Topik ini terasa abstrak sampai stream berjalan paralel dan urutan cetakan forEach mulai berubah tidak menentu. Pada Chapter ini kita bedah encounter order pada List versus Set, peran flag unordered(), lalu cara menjaga determinisme hasil lewat forEachOrdered dan collect.
Apa itu Encounter Order #
Setiap stream membawa kumpulan karakteristik internal; yang paling relevan di sini adalah ORDERED. Stream dari ArrayList dan ArrayDeque selalu ORDERED karena struktur datanya memang berurutan. Stream dari HashSet tidak — HashSet hanya menjanjikan keanggotaan, bukan urutan. LinkedHashSet berada di tengah: urutan penyisipan dijaga lewat rantai tautan tambahan, sehingga stream-nya ORDERED juga.
Karakteristik ini dibaca oleh operasi pilihan. sorted memaksa urutan baru yang sama sekali tidak peduli sumbernya. Sebaliknya forEach pada stream sekuensial kebetulan mengikuti encounter order, findFirst mengembalikan elemen pertama menurut encounter order, dan limit/skip menghitung posisi dari depan. Operasi unordered() menghapus flag ORDERED — nanti kita lihat untuk apa.
Encounter Order pada List, HashSet, dan LinkedHashSet #
Eksperimen pertama mengalirkan empat nama yang sama melalui tiga jenis koleksi berbeda, lalu mengumpulkan hasilnya menjadi List agar urutannya terlihat:
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class UrutanPertemuan {
public static void main(String[] args) {
List<String> urutanMasuk = List.of("Budi", "Siti", "Dewi", "Ani");
// List: encounter order = urutan penyimpanan
System.out.println("List = " + urutanMasuk.stream().collect(Collectors.toList()));
// HashSet: tidak punya encounter order
Set<String> himpunan = new HashSet<>(urutanMasuk);
System.out.println("HashSet = " + himpunan.stream().collect(Collectors.toList()));
// LinkedHashSet: encounter order = urutan penyisipan
Set<String> sisipan = new LinkedHashSet<>(urutanMasuk);
System.out.println("LinkedHashSet = " + sisipan.stream().collect(Collectors.toList()));
// sorted tetap mengurutkan apa pun sumbernya
System.out.println("HashSet sorted = " + himpunan.stream().sorted().collect(Collectors.toList()));
}
}Output yang Dihasilkan:
List = [Budi, Siti, Dewi, Ani]
HashSet = [Dewi, Budi, Siti, Ani]
LinkedHashSet = [Budi, Siti, Dewi, Ani]
HashSet sorted = [Ani, Budi, Dewi, Siti]Mari kita telaah baris-baris kode di atas:
- Baris 10-13 (
List→[Budi, Siti, Dewi, Ani]):List.ofmenyimpan urutan penulisan apa adanya; encounter order stream-nya sama dengan urutan penyimpanan, dancollectmengembalikannya tanpa perubahan. - Baris 16-17 (
HashSet→[Dewi, Budi, Siti, Ani]): konstruktorHashSetmemindahkan elemen ke keranjang hash, sehingga iterasinya mengikuti perhitungan hash. Urutan ini stabil untuk isi yang sama pada JVM yang sama — urutanDewilebih dulu bukan kebetulan, tetapi juga bukan urutan penyisipan dan tidak boleh diandalkan sebagai kontrak. - Baris 20-21 (
LinkedHashSet→[Budi, Siti, Dewi, Ani]):LinkedHashSetmenambahkan rantai tautan yang mengingat urutan penyisipan, sehingga encounter order-nya kembali sama sepertiList. Koleksi ini adalah jawaban standar saat kita butuhSettanpa duplikat namun urutan tetap stabil. - Baris 24 (
sorted()→[Ani, Budi, Dewi, Siti]):sortedmenegaskan urutan alamiStringdi atas sumber apa pun. Urutan hasilsortedtidak pernah bergantung pada encounter order sumber; dua baris terakhir output justru menunjukkan urutan hash dan urutan alfabetik yang berbeda dari himpunan yang sama.
forEachOrdered dan Hasil Terkumpul pada Stream Paralel #
Pada stream paralel, elemen dipecah ke beberapa worker dan diproses serentak — urutan penyelesaian tidak bisa dijamin. forEach membiarkan aksi berjalan sesuai urutan penyelesaian itu, sehingga hasil cetakannya berpotensi tampil acak. forEachOrdered memisahkan dua hal: pemrosesan tetap paralel, tetapi eksekusi aksi dijadwalkan satu per satu mengikuti encounter order. Dua mekanisme lain, collect dan findFirst, juga tetap menghormati urutan pada stream paralel:
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class UrutanParalel {
public static void main(String[] args) {
List<Integer> data = IntStream.rangeClosed(1, 12).boxed().toList();
// forEachOrdered: elemen diproses paralel, aksi dijalankan
// mengikuti encounter order satu per satu
System.out.print("forEachOrdered = ");
data.parallelStream().forEachOrdered(n -> System.out.print(n + " "));
System.out.println();
// collect pada stream paralel tetap menjaga urutan hasil
List<Integer> kuadrat = data.parallelStream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println("collect = " + kuadrat);
// findFirst pada stream paralel tetap menghormati encounter order
System.out.println("findFirst = " + data.parallelStream().filter(n -> n > 7).findFirst());
}
}Output yang Dihasilkan:
forEachOrdered = 1 2 3 4 5 6 7 8 9 10 11 12
collect = [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144]
findFirst = Optional[8]Mari kita telaah baris-baris kode di atas:
- Baris 13 (
parallelStream().forEachOrdered(...)): dua belas angka diproses di beberapa thread sekaligus, namunSystem.out.printhanya dijalankan bergiliran menurut encounter order — outputnya1 2 3 ... 12yang rapi. Konsekuensinya,forEachOrderedmemperkenalkan sinkronisasi antar worker dan bisa mengurangi tingkat paralelisme; gunakan hanya saat urutan aksi benar-benar dibutuhkan. - Baris 17-19 (
collectsetelahmap): tiap worker mengumpulkan potongan hasil di wadah lokalnya, lalu combiner menggabungkan potongan-potongan itu berurutan. Hasil akhirnya[1, 4, 9, ..., 144]— terurut sempurna walau pekerjaan dilakukan serentak. - Baris 23 (
findFirstsetelahfilter): pada stream paralelORDERED,findFirsttetap mengembalikan elemen pertama menurut encounter order yang lolos filter —Optional[8], bukan sekadar “salah satu elemen yang lolos”.
Flag unordered dan Operasi yang Peka Urutan #
Method unordered() menghapus karakteristik ORDERED dari stream. Pada stream sekuensial efeknya nyaris tak terlihat — elemen tetap mengalir dari depan. Nilainya muncul pada stream paralel: dengan flag hilang, operasi seperti distinct, limit, dan findAny diizinkan memilih elemen mana pun yang paling murah dijangkau, termasuk melewati potongan data yang tidak perlu diproses.
Perlu dicatat bahwa unordered() tidak mengacak data — method ini sekadar mencabut janji urutan agar mesin stream boleh bekerja lebih bebas. Bila hasil akhir berupa List terurut yang akan dikumpulkan dengan collect, flag unordered justru tidak berguna karena combiner tetap menyusun ulang menurut urutan sumber. Pola unordered() paling masuk akal pada pipeline yang berakhir di findAny, anyMatch, atau pengurutan global dengan sorted yang mengabaikan urutan sumber.
Dengan pemahaman urutan ini, kita siap membahas stream paralel secara penuh: bagaimana data dipecah oleh ForkJoinPool.commonPool, kapan paralelisasi benar-benar membantu, dan pola lambda stateful yang harus dihindari agar hasil tetap benar.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
UrutanParalel.java
Unduh berkasimport java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class UrutanParalel {
public static void main(String[] args) {
List<Integer> data = IntStream.rangeClosed(1, 12).boxed().toList();
// forEachOrdered: elemen diproses paralel, aksi dijalankan
// mengikuti encounter order satu per satu
System.out.print("forEachOrdered = ");
data.parallelStream().forEachOrdered(n -> System.out.print(n + " "));
System.out.println();
// collect pada stream paralel tetap menjaga urutan hasil
List<Integer> kuadrat = data.parallelStream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println("collect = " + kuadrat);
// findFirst pada stream paralel tetap menghormati encounter order
System.out.println("findFirst = " + data.parallelStream().filter(n -> n > 7).findFirst());
}
}
UrutanPertemuan.java
Unduh berkasimport java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class UrutanPertemuan {
public static void main(String[] args) {
List<String> urutanMasuk = List.of("Budi", "Siti", "Dewi", "Ani");
// List: encounter order = urutan penyimpanan
System.out.println("List = " + urutanMasuk.stream().collect(Collectors.toList()));
// HashSet: tidak punya encounter order
Set<String> himpunan = new HashSet<>(urutanMasuk);
System.out.println("HashSet = " + himpunan.stream().collect(Collectors.toList()));
// LinkedHashSet: encounter order = urutan penyisipan
Set<String> sisipan = new LinkedHashSet<>(urutanMasuk);
System.out.println("LinkedHashSet = " + sisipan.stream().collect(Collectors.toList()));
// sorted tetap mengurutkan apa pun sumbernya
System.out.println("HashSet sorted = " + himpunan.stream().sorted().collect(Collectors.toList()));
}
}