Setelah terbiasa menulis lambda expression dan method reference pada Section Functional Programming, kita memegang satu set alat baru untuk memproses data: Stream API. Stream menggeser cara kita berpikir dari bagaimana memproses data menjadi apa yang ingin kita dapatkan dari data — kita cukup menyatakan saring, ubah, urutkan, lalu kumpulkan, dan JVM yang mengeksekusi langkah-langkahnya. Hadir sejak Java 8, stream kini menjadi gaya baku memproses collection di kode modern. Pada Chapter ini kita membongkar anatomi pipeline, membandingkannya dengan perulangan imperatif yang sudah kita kuasai, lalu membuktikan aturan yang paling sering membingungkan pemula: stream hanya boleh dipakai sekali.
Stream adalah Pipeline, Bukan Struktur Data #
Kesalahpahaman paling umum adalah menganggap stream sebagai jenis collection yang baru. Stream tidak menyimpan elemen apa pun; stream adalah aliran pemrosesan yang mengambil elemen dari sumber — List, array, berkas, generator — lalu mengalirkannya melewati rangkaian operasi. Stream juga tidak mengubah sumbernya: operasi filter tidak menghapus elemen dari List asli, dan sorted tidak menyusun ulang List asli. Setiap operasi menghasilkan stream baru; hasil akhir ditampung secara eksplisit, misalnya lewat toList().
Stream adalah pipeline: data mengalir dari sumber melalui serangkaian operasi, lalu keluar sebagai hasil di ujungnya — alur yang dapat digambar begini:
Pipeline terdiri dari tiga bagian: sumber data, operasi antara (intermediate operations) yang selalu mengembalikan Stream baru, serta operasi akhir (terminal operation) yang memicu eksekusi. Program berikut memperlihatkan ketiganya secara eksplisit — satu variabel per tahap:
import java.util.List;
import java.util.stream.Stream;
public class AnatomiPipeline {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Roti Gandum", "makanan", 30000),
new Produk("Madu Hutan", "makanan", 120000)
);
// Tahap 1: sumber data -- List berubah menjadi Stream
Stream<Produk> sumber = produk.stream();
// Tahap 2: operasi antara -- menyaring elemen
Stream<Produk> yangMahal = sumber.filter(p -> p.harga() >= 50000);
// Tahap 3: operasi antara lagi -- memetakan Produk menjadi String
Stream<String> namaMahal = yangMahal.map(Produk::nama);
// Tahap 4: operasi akhir -- memicu eksekusi dan menghasilkan List
List<String> hasil = namaMahal.toList();
System.out.println("Hasil pipeline : " + hasil);
System.out.println("List asli : " + produk.size() + " produk, tidak berubah");
}
}Output yang Dihasilkan:
Hasil pipeline : [Kopi Arabika, Madu Hutan]
List asli : 3 produk, tidak berubahMari kita bedah pipeline di atas tahap demi tahap:
- Baris 6 (
record Produk(...)): model data contoh di seluruh Section ini —nama,kategori, danhargadalam satu tipe ringkas denganequals,hashCode, dantoStringotomatis. - Baris 16 (
produk.stream()): sumber pipeline —ListmenjadiStream<Produk>yang membaca elemen satu per satu saat dibutuhkan. - Baris 19 (
sumber.filter(...)): operasi antara pertama —filtermengembalikanStream<Produk>baru yang hanya melewatkan produk bermaharga minimal 50000. - Baris 22 (
yangMahal.map(Produk::nama)): operasi antara kedua —mapmengubah tiapProdukmenjadiString; tipe pipeline berganti dariStream<Produk>keStream<String>. - Baris 25 (
namaMahal.toList()): operasi akhir — sebelum baris ini belum ada elemen yang diproses;toList()memicu eksekusi pipeline dan menampung hasilnya. - Baris 27-28: cetak hasil —
Listasli tetap utuh tiga produk; stream memproses aliran, bukan mengubah sumber.
Imperatif vs Stream #
Cara paling cepat memahami nilai stream adalah membandingkan dua gaya yang menyelesaikan tugas identik: mengambil nama semua produk bermaharga minimal 50000, mengurutkannya, lalu menggabungkannya menjadi satu baris teks. Versi imperatif memakai loop, List sementara, Collections.sort, dan StringBuilder; versi stream menyatakan maksudnya sebagai rantai operasi:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class RingkasanToko {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Roti Gandum", "makanan", 30000),
new Produk("Madu Hutan", "makanan", 120000),
new Produk("Air Mineral", "minuman", 8000)
);
// Gaya imperatif: kita menuliskan BAGAIMANA, langkah demi langkah
List<String> mahal = new ArrayList<>();
for (Produk p : produk) {
if (p.harga() >= 50000) {
mahal.add(p.nama());
}
}
Collections.sort(mahal);
StringBuilder teks = new StringBuilder();
for (int i = 0; i < mahal.size(); i++) {
teks.append(mahal.get(i));
if (i < mahal.size() - 1) {
teks.append(", ");
}
}
System.out.println("Imperatif : " + teks);
// Gaya stream: kita menyatakan APA yang ingin dicapai
String hasil = produk.stream()
.filter(p -> p.harga() >= 50000)
.map(Produk::nama)
.sorted()
.collect(Collectors.joining(", "));
System.out.println("Stream : " + hasil);
}
}Output yang Dihasilkan:
Imperatif : Kopi Arabika, Madu Hutan
Stream : Kopi Arabika, Madu HutanKedua gaya mencetak baris yang persis sama, dan itu memang intinya — stream bukan fitur yang menghasilkan sesuatu yang mustahil dilakukan loop, melainkan cara menuliskan niat dengan lebih ringkas dan aman. Mari kita amati perbedaan strukturnya:
- Baris 19-33 (blok imperatif): empat langkah manual — sisihkan, urutkan, gabung dengan koma, cetak.
ArrayListsementara, indeksi, dan kondisi batas koma dikelola tangan; setiap detail bagaimana tertulis di kode. - Baris 37-41 (pipeline stream): maksudnya terbaca seperti daftar keinginan — saring yang mahal, ambil namanya, urutkan, gabung dengan
", ". Variabel sementara, indeks, dan mutasi state tidak ada lagi. - Baris 41 (
Collectors.joining(", ")): operasi akhir yang menghasilkanStringjadi — pengganti baris-barisStringBuilderdi atasnya.
Gaya imperatif tetap sah untuk kasus sederhana. Stream unggul saat pipeline bertingkat: satu rantai deklaratif menggantikan loop bersarang beserta variabel pembantunya, terbaca dari atas ke bawah.
Stream Hanya Bisa Dipakai Sekali #
Aturan kedua yang wajib diingat: satu objek Stream hanya boleh dijalankan satu operasi akhir. Begitu operasi akhir selesai, stream ditandai terpakai dan tertutup; pipeline lain yang memakai objek Stream yang sama ditolak JVM dengan IllegalStateException. Program berikut sengaja memicu penolakan tersebut agar kita mengenali pesannya:
import java.util.List;
import java.util.stream.Stream;
public class StreamSekaliPakai {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Roti Gandum", "makanan", 30000)
);
Stream<Produk> aliran = produk.stream();
long jumlahPertama = aliran.filter(p -> p.harga() >= 50000).count();
System.out.println("Eksekusi pertama, produk mahal: " + jumlahPertama);
try {
long jumlahKedua = aliran.filter(p -> p.kategori().equals("minuman")).count();
System.out.println("Eksekusi kedua: " + jumlahKedua);
} catch (IllegalStateException e) {
System.out.println("Eksekusi kedua ditolak: " + e.getMessage());
}
}
}Output yang Dihasilkan:
Eksekusi pertama, produk mahal: 1
Eksekusi kedua ditolak: stream has already been operated upon or closedMari kita telusuri apa yang sebenarnya terjadi:
- Baris 15 (
produk.stream()): satu objekStream<Produk>dibuat dan disimpan ke variabelaliran. Data tetap hidup diproduk;aliranhanya pegangan pipeline. - Baris 17: pipeline pertama —
filterlalucount()— berjalan mulus dan mencetak1: hanya Kopi Arabika yang bermaharga minimal 50000.count()menandaialiransebagai terpakai. - Baris 21: pipeline kedua mencoba memakai
aliranyang sama. JVM menolak sebelum satu elemen pun dibaca. - Baris 23-25 (
catch IllegalStateException): penolakan ditangkap dan pesan verbatim dari eksekusi nyata tercetak:stream has already been operated upon or closed.
Solusinya sederhana: panggil produk.stream() lagi, atau tampung hasil pertama ke List lalu buat stream dari hasil itu. Membuat stream murah — yang mahal adalah datanya, dan data tetap aman di collection.
Dengan anatomi pipeline, kontras imperatif-deklaratif, dan aturan sekali pakai di tangan, kita siap memperluas sumber data. Chapter Creating Streams menunjukkan cara membangun stream dari nilai lepas, array, collection kosong, rentang angka, hingga generator iterate dan generate.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AnatomiPipeline.java
Unduh berkasimport java.util.List;
import java.util.stream.Stream;
public class AnatomiPipeline {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Roti Gandum", "makanan", 30000),
new Produk("Madu Hutan", "makanan", 120000)
);
// Tahap 1: sumber data -- List berubah menjadi Stream
Stream<Produk> sumber = produk.stream();
// Tahap 2: operasi antara -- menyaring elemen
Stream<Produk> yangMahal = sumber.filter(p -> p.harga() >= 50000);
// Tahap 3: operasi antara lagi -- memetakan Produk menjadi String
Stream<String> namaMahal = yangMahal.map(Produk::nama);
// Tahap 4: operasi akhir -- memicu eksekusi dan menghasilkan List
List<String> hasil = namaMahal.toList();
System.out.println("Hasil pipeline : " + hasil);
System.out.println("List asli : " + produk.size() + " produk, tidak berubah");
}
}
RingkasanToko.java
Unduh berkasimport java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class RingkasanToko {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Roti Gandum", "makanan", 30000),
new Produk("Madu Hutan", "makanan", 120000),
new Produk("Air Mineral", "minuman", 8000)
);
// Gaya imperatif: kita menuliskan BAGAIMANA, langkah demi langkah
List<String> mahal = new ArrayList<>();
for (Produk p : produk) {
if (p.harga() >= 50000) {
mahal.add(p.nama());
}
}
Collections.sort(mahal);
StringBuilder teks = new StringBuilder();
for (int i = 0; i < mahal.size(); i++) {
teks.append(mahal.get(i));
if (i < mahal.size() - 1) {
teks.append(", ");
}
}
System.out.println("Imperatif : " + teks);
// Gaya stream: kita menyatakan APA yang ingin dicapai
String hasil = produk.stream()
.filter(p -> p.harga() >= 50000)
.map(Produk::nama)
.sorted()
.collect(Collectors.joining(", "));
System.out.println("Stream : " + hasil);
}
}
StreamSekaliPakai.java
Unduh berkasimport java.util.List;
import java.util.stream.Stream;
public class StreamSekaliPakai {
record Produk(String nama, String kategori, int harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Kopi Arabika", "minuman", 85000),
new Produk("Teh Hijau", "minuman", 45000),
new Produk("Roti Gandum", "makanan", 30000)
);
Stream<Produk> aliran = produk.stream();
long jumlahPertama = aliran.filter(p -> p.harga() >= 50000).count();
System.out.println("Eksekusi pertama, produk mahal: " + jumlahPertama);
try {
long jumlahKedua = aliran.filter(p -> p.kategori().equals("minuman")).count();
System.out.println("Eksekusi kedua: " + jumlahKedua);
} catch (IllegalStateException e) {
System.out.println("Eksekusi kedua ditolak: " + e.getMessage());
}
}
}