Kita sudah mengumpulkan seluruh peralatan section ini: operasi intermediate dan terminal, Collector, stream primitif, Optional, laziness, urutan, paralelisme, hingga cara menimbang performa. Peralatan lengkap justru membuka peluang kode yang berlebihan — pipeline sepuluh tahap yang tidak ada yang sanggup membacanya, atau lambda yang diam-diam mengubah koleksi luar. Chapter penutup ini merangkum kebiasaan-kebiasaan yang menjaga stream tetap menjadi alat yang menyenangkan: pipeline pendek bernama jelas, flatMap pengganti stream bersarang, kebebasan efek samping, method reference, dan stream primitif untuk angka.
Pipeline Pendek dengan Nama yang Bercerita #
Pipeline terbaik adalah pipeline yang muat dalam satu tatapan mata: satu pertanyaan bisnis dijawab satu rangkaian tahap, lalu diberi nama method yang menjelaskan maksudnya. Bila satu method berisi dua filter, dua map, satu sorted, dan satu collect, pembaca dipaksa menelusuri tujuh baris untuk menebak pertanyaan apa yang dijawab. Program berikut memecah laporan nilai menjadi method kecil yang masing-masing berdiri sendiri:
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class LaporanRapi {
record Siswa(String nama, String kelas, int nilai) {}
static final List<Siswa> ROMBONGAN = List.of(
new Siswa("Budi", "X-2", 78),
new Siswa("Siti", "X-2", 92),
new Siswa("Dewi", "XI-3", 85),
new Siswa("Ani", "X-2", 74)
);
// Satu pertanyaan, satu pipeline pendek, satu nama method yang jelas
static double rataRataKelas(String kelas) {
return ROMBONGAN.stream()
.filter(s -> s.kelas().equals(kelas))
.mapToInt(Siswa::nilai)
.average()
.orElse(0.0);
}
static List<String> peringkatTigaTeratas() {
return ROMBONGAN.stream()
.sorted(Comparator.comparingInt(Siswa::nilai).reversed())
.limit(3)
.map(Siswa::nama)
.collect(Collectors.toList());
}
public static void main(String[] args) {
System.out.printf("rata-rata X-2 = %.2f%n", rataRataKelas("X-2"));
System.out.println("tiga teratas = " + peringkatTigaTeratas());
}
}Output yang Dihasilkan:
rata-rata X-2 = 81.33
tiga teratas = [Siti, Dewi, Budi]Mari kita telaah baris-baris kode di atas:
- Baris 17-23 (
rataRataKelas): lima baris pipeline menjawab satu pertanyaan — rata-rata nilai satu kelas. Nama method menjadi dokumentasi; pemanggilmaintidak perlu membaca isinya untuk memahami maksudnya.mapToIntmenunjukkan kebiasaan stream primitif untuk angka, danorElse(0.0)menangani kelas tanpa siswa. - Baris 25-31 (
peringkatTigaTeratas): urutan tahapnya membaca seperti kalimat — urutkan menurut nilai terbalik, potong tiga, ambil nama, kumpulkan.Comparator.comparingInt(Siswa::nilai).reversed()lebih ringkas sekaligus lebih jelas daripada lambda(a, b) -> b.nilai() - a.nilai()yang mudah salah tulis. - Baris 34-35 (
mainyang tipis): methodmainhanya menyusun laporan dari method bernama. Struktur seperti ini membuat tiap pipeline bisa diuji dan diganti sendiri-sendiri tanpa menyentuh yang lain.
flatMap Menggantikan Stream Bersarang #
Data nyata sering bersarang: pesanan berisi daftar item, kelas berisi daftar siswa. Pola keliru yang sering muncul adalah stream di dalam forEach stream — dua aliran bercampur, biasanya disertai mutasi koleksi luar:
List<String> semuaItem = new ArrayList<>();
pesanan.stream()
.forEach(p -> p.items().stream()
.map(Item::nama)
.forEach(nama -> semuaItem.add(nama)));Kode di atas bekerja, tetapi strukturnya berbohong: operasi terminal forEach dipakai untuk membangun daftar, koleksi luar diubah dari dalam lambda, dan tingkat bersarang memaksa pembaca menyusun dua loop secara mental. flatMap meratakan dua tingkat menjadi satu pipeline datar:
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class RefactorToko {
record Item(String nama, int harga) {}
record Pesanan(String pembeli, List<Item> items) {}
public static void main(String[] args) {
List<Pesanan> pesanan = List.of(
new Pesanan("Budi", List.of(new Item("Pena", 2000), new Item("Buku", 5000))),
new Pesanan("Siti", List.of(new Item("Pensil", 1500))),
new Pesanan("Dewi", List.of(new Item("Bag", 75000), new Item("Penghapus", 1000)))
);
// flatMap meratakan dua tingkat koleksi menjadi satu stream
List<String> semuaItem = pesanan.stream()
.flatMap(p -> p.items().stream())
.map(Item::nama)
.collect(Collectors.toList());
System.out.println("semua item = " + semuaItem);
// collect membangun Map tanpa mutasi koleksi luar
Map<String, Integer> totalPerPembeli = pesanan.stream()
.collect(Collectors.toMap(
Pesanan::pembeli,
p -> p.items().stream().mapToInt(Item::harga).sum()));
System.out.println("total belanja = " + totalPerPembeli);
// method reference menjaga pipeline singkat dan terbaca
List<String> daftarPembeli = pesanan.stream()
.map(Pesanan::pembeli)
.collect(Collectors.toList());
System.out.println("pembeli = " + daftarPembeli);
}
}Output yang Dihasilkan:
semua item = [Pena, Buku, Pensil, Bag, Penghapus]
total belanja = {Dewi=76000, Budi=7000, Siti=1500}
pembeli = [Budi, Siti, Dewi]Mari kita telaah baris-baris kode di atas:
- Baris 19-22 (
flatMap(p -> p.items().stream())): setiapPesanandiubah menjadi stream item-nya, laluflatMapmeratakan semua stream itu menjadi satu aliranItem— dari 3 pesanan menjadi 5 item dalam satu perjalanan. Setelah rata, tahap berikutnya kembali sederhana:map(Item::nama)lalucollect. - Baris 26-29 (
toMap+mapToInt): total belanja per pembeli dibangun tanpa satu pun variabel sementara di luar pipeline.toMapmenjadi pemegang tunggal kebenaran hasil; tidak adaHashMapyang diisi dari dua tempat berbeda. - Baris 33-35 (
map(Pesanan::pembeli)): tiga method reference (Pesanan::pembeli,Item::nama,Item::harga) menggantikan lambdap -> p.pembeli()— sama persis secara semantik, tetapi nama elemen dan jenis aksinya terbaca sekali lihat.
Bebas Efek Samping dengan collect #
Efek samping adalah sumber bug yang paling sulit dicari pada stream: pipeline tampak murni padahal lambda-nya mengubah keadaan luar. Pola yang paling sering muncul adalah accumulator manual:
List<String> namaElektronik = new ArrayList<>();
etalase.stream()
.filter(p -> p.kategori().equals("elektronik"))
.map(Produk::nama)
.forEach(nama -> namaElektronik.add(nama));Versi sehatnya satu baris lebih pendek dan sepenuhnya deklaratif: .collect(Collectors.toList()). Semua Collector yang kita pelajari — toList, toMap, joining, groupingBy, counting — ada persis untuk menggantikan pola mutasi ini. forEach kemudian kembali ke peran aslinya: menampilkan hasil, bukan membangunnya.
Method Reference dan Stream Primitif untuk Angka #
Dua kebiasaan kecil ini menaikkan keterbacaan sekaligus performa tanpa biaya. Pertama, pilih method reference ketika lambda hanya memanggil satu method (Siswa::nama, Item::harga); gunakan lambda ketika logikanya benar-benar butuh ekspresi — misalnya s -> s.nilai() >= 75 — atau ketika variabel lokal membuat maksudnya lebih jelas. Kedua, begitu pipeline menyentuh angka dalam jumlah banyak, pindahkan ke stream primitif sedini mungkin: mapToInt, mapToLong, atau mapToDouble, lalu manfaatkan sum, average, max, dan summaryStatistics bawaan.
Dua contoh berkas di atas mempraktikkan keduanya: mapToInt(Item::harga).sum() menghitung total tanpa boxing, dan average() pada stream primitif langsung menghasilkan OptionalDouble yang ditangani orElse.
Daftar Periksa Pipeline #
Sebelum menggabungkan kode stream ke proyek, sapukan daftar periksa ringkas berikut:
| Aspek | Tanda pipeline sehat |
|---|---|
| Panjang | tiga sampai lima tahap; lebih dari itu, pecah menjadi method bernama |
| Efek samping | lambda tidak menulis ke variabel atau koleksi luar |
| Stream bersarang | tidak ada stream di dalam forEach; gunakan flatMap |
| Hasil akhir | dibangun collect/reduce/findFirst, bukan mutasi manual |
| Angka | mapToInt/mapToDouble begitu elemennya numerik |
| Nilai mungkin kosong | Optional ditangani map/orElse/ifPresent, bukan get() |
| Paralel | hanya setelah ukuran dan beban CPU terbukti membutuhkannya |
| Referensi | method reference untuk pemanggilan tunggal; lambda untuk logika |
Stream API menutup petualangan kita memproses data secara deklaratif di Java. Disiplin yang sama — menyatakan apa alih-alih bagaimana — akan muncul kembali pada section berikutnya ketika kita bekerja dengan Date-Time API: memanipulasi tanggal dan waktu lewat objek tak terubah yang dirangkai menjadi pipeline perhitungan waktu yang rapi.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
LaporanRapi.java
Unduh berkasimport java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class LaporanRapi {
record Siswa(String nama, String kelas, int nilai) {}
static final List<Siswa> ROMBONGAN = List.of(
new Siswa("Budi", "X-2", 78),
new Siswa("Siti", "X-2", 92),
new Siswa("Dewi", "XI-3", 85),
new Siswa("Ani", "X-2", 74)
);
// Satu pertanyaan, satu pipeline pendek, satu nama method yang jelas
static double rataRataKelas(String kelas) {
return ROMBONGAN.stream()
.filter(s -> s.kelas().equals(kelas))
.mapToInt(Siswa::nilai)
.average()
.orElse(0.0);
}
static List<String> peringkatTigaTeratas() {
return ROMBONGAN.stream()
.sorted(Comparator.comparingInt(Siswa::nilai).reversed())
.limit(3)
.map(Siswa::nama)
.collect(Collectors.toList());
}
public static void main(String[] args) {
System.out.printf("rata-rata X-2 = %.2f%n", rataRataKelas("X-2"));
System.out.println("tiga teratas = " + peringkatTigaTeratas());
}
}
RefactorToko.java
Unduh berkasimport java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class RefactorToko {
record Item(String nama, int harga) {}
record Pesanan(String pembeli, List<Item> items) {}
public static void main(String[] args) {
List<Pesanan> pesanan = List.of(
new Pesanan("Budi", List.of(new Item("Pena", 2000), new Item("Buku", 5000))),
new Pesanan("Siti", List.of(new Item("Pensil", 1500))),
new Pesanan("Dewi", List.of(new Item("Bag", 75000), new Item("Penghapus", 1000)))
);
// flatMap meratakan dua tingkat koleksi menjadi satu stream
List<String> semuaItem = pesanan.stream()
.flatMap(p -> p.items().stream())
.map(Item::nama)
.collect(Collectors.toList());
System.out.println("semua item = " + semuaItem);
// collect membangun Map tanpa mutasi koleksi luar
Map<String, Integer> totalPerPembeli = pesanan.stream()
.collect(Collectors.toMap(
Pesanan::pembeli,
p -> p.items().stream().mapToInt(Item::harga).sum()));
System.out.println("total belanja = " + totalPerPembeli);
// method reference menjaga pipeline singkat dan terbaca
List<String> daftarPembeli = pesanan.stream()
.map(Pesanan::pembeli)
.collect(Collectors.toList());
System.out.println("pembeli = " + daftarPembeli);
}
}