Stream

Stream #

Bayangkan kamu punya daftar 10.000 transaksi dan perlu menemukan total nilai transaksi yang terjadi bulan ini, dikategorikan sebagai “besar” (di atas Rp5 juta), dari pelanggan premium. Dengan loop tradisional, kamu butuh tiga atau empat loop bersarang dengan variabel sementara yang berserak. Dengan Stream API, ini bisa diselesaikan dalam satu ekspresi rantai yang terbaca seperti kalimat: ambil transaksi, filter bulan ini, filter nilai besar, filter pelanggan premium, jumlahkan nilainya. Stream API diperkenalkan di Java 8 sebagai cara baru memproses koleksi data secara deklaratif — kamu mendeskripsikan apa yang ingin dilakukan, bukan bagaimana cara melakukannya. Artikel ini membahas cara membuat stream, semua operasi intermediate dan terminal yang tersedia, penggunaan Collectors untuk pengumpulan hasil yang kompleks, Optional untuk penanganan null yang aman, parallel stream untuk pemrosesan paralel, dan pola-pola idiomatis yang sering muncul di kode produksi.

Konsep Dasar #

Stream bukan struktur data — ia tidak menyimpan elemen. Stream adalah pipeline yang mendeskripsikan serangkaian operasi yang akan diterapkan pada sumber data. Eksekusinya lazy: operasi intermediate tidak dijalankan sampai ada operasi terminal yang memicunya.

flowchart LR
    A["Sumber Data\n(Collection, Array,\nFile, Generator)"] -->|"stream()"| B["Stream"]
    B -->|"filter()"| C["Stream"]
    C -->|"map()"| D["Stream"]
    D -->|"sorted()"| E["Stream"]
    E -->|"collect() / forEach()\ncount() / reduce()"| F["Hasil Akhir\n(List, Map, nilai)"]

    style B stroke:#999,stroke-width:2px
    style C stroke:#999,stroke-width:2px
    style D stroke:#999,stroke-width:2px
    style E stroke:#999,stroke-width:2px

Tiga karakteristik penting Stream:

Lazy evaluation — operasi intermediate hanya dieksekusi saat terminal dipanggil, dan hanya untuk elemen yang benar-benar dibutuhkan. filter tidak memproses semua elemen dulu baru map dijalankan — keduanya diproses per elemen dalam satu pass.

Single-use — stream hanya bisa dikonsumsi sekali. Setelah operasi terminal dipanggil, stream tersebut tidak bisa dipakai lagi.

Tidak mengubah sumber — stream tidak memodifikasi koleksi aslinya. Ia menghasilkan hasil baru.

Jenis Operasi Contoh Return
Intermediate filter, map, sorted, distinct, limit Stream baru (lazy)
Terminal collect, forEach, count, reduce, findFirst Nilai konkret atau void

Membuat Stream #

Dari Koleksi dan Array #

import java.util.stream.*;
import java.util.*;

// Dari List
List<String> buah = List.of("Apel", "Mangga", "Jeruk", "Pisang");
Stream<String> streamBuah = buah.stream();

// Dari Set
Set<Integer> angka = Set.of(1, 2, 3, 4, 5);
Stream<Integer> streamAngka = angka.stream();

// Dari Map — stream entry, key, atau value
Map<String, Integer> harga = Map.of("Apel", 5000, "Mangga", 8000);
Stream<Map.Entry<String, Integer>> streamEntry = harga.entrySet().stream();
Stream<String>  streamKey   = harga.keySet().stream();
Stream<Integer> streamValue = harga.values().stream();

// Dari array
String[] arr = {"a", "b", "c"};
Stream<String> streamArr = Arrays.stream(arr);
Stream<String> streamSub = Arrays.stream(arr, 1, 3); // indeks 1 sampai 2: ["b", "c"]

Stream Statis dan Generator #

// Stream.of() — buat dari elemen langsung
Stream<String> langsung = Stream.of("Satu", "Dua", "Tiga");

// Stream.empty() — stream kosong (berguna sebagai nilai return)
Stream<String> kosong = Stream.empty();

// Stream.generate() — stream tak terbatas dari Supplier
// Selalu butuh limit() agar tidak infinite
Stream<Double> random = Stream.generate(Math::random).limit(5);
Stream<String> uuids  = Stream.generate(() -> UUID.randomUUID().toString()).limit(3);

// Stream.iterate() — stream tak terbatas dengan seed dan fungsi
Stream<Integer> genap = Stream.iterate(0, n -> n + 2).limit(10);
// [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

// Stream.iterate() dengan predikat penghenti (Java 9+)
Stream<Integer> kurangDari20 = Stream.iterate(1, n -> n < 20, n -> n * 2);
// [1, 2, 4, 8, 16]

// Stream.concat() — gabungkan dua stream
Stream<String> gabung = Stream.concat(
    Stream.of("a", "b"),
    Stream.of("c", "d")
); // ["a", "b", "c", "d"]

Stream Primitif #

Untuk performa tinggi, hindari boxing dengan menggunakan stream primitif:

// IntStream, LongStream, DoubleStream
IntStream angkaInt = IntStream.range(1, 6);        // [1, 2, 3, 4, 5]
IntStream angkaInt2 = IntStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5] (inklusif)
LongStream panjang  = LongStream.range(0L, 100L);
DoubleStream desimal = DoubleStream.of(1.1, 2.2, 3.3);

// Konversi antara stream objek dan primitif
Stream<Integer> boxed = angkaInt.boxed();          // IntStream → Stream<Integer>
IntStream unboxed = buah.stream().mapToInt(String::length); // Stream<String> → IntStream

Operasi Intermediate #

Operasi intermediate mengembalikan stream baru dan bersifat lazy. Kamu bisa merantainya sebanyak yang dibutuhkan.

filter — Menyaring Elemen #

List<String> nama = List.of("Budi", "Ani", "Citra", "Doni", "Eva");

// Filter nama yang panjangnya > 3
List<String> panjang = nama.stream()
    .filter(n -> n.length() > 3)
    .collect(Collectors.toList());
// [Budi, Citra, Doni]

// Beberapa filter bisa dirantai
List<Integer> angka = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> genapBesar = angka.stream()
    .filter(n -> n % 2 == 0)  // filter genap
    .filter(n -> n > 4)        // filter lebih dari 4
    .collect(Collectors.toList());
// [6, 8, 10]

map — Transformasi Elemen #

List<String> nama = List.of("budi", "ani", "citra");

// Ubah setiap elemen menjadi bentuk lain
List<String> kapital = nama.stream()
    .map(String::toUpperCase) // method reference
    .collect(Collectors.toList());
// [BUDI, ANI, CITRA]

// Transformasi tipe
record Produk(Long id, String nama, double harga) {}

List<Produk> produk = List.of(
    new Produk(1L, "Laptop", 12_000_000),
    new Produk(2L, "Mouse", 150_000),
    new Produk(3L, "Keyboard", 450_000)
);

// Ambil hanya nama
List<String> namaProduk = produk.stream()
    .map(Produk::nama)
    .collect(Collectors.toList());
// ["Laptop", "Mouse", "Keyboard"]

// Naikkan harga 10%
List<Produk> hargaNaik = produk.stream()
    .map(p -> new Produk(p.id(), p.nama(), p.harga() * 1.1))
    .collect(Collectors.toList());

// mapToInt/mapToDouble/mapToLong untuk stream primitif
IntStream panjangNama = nama.stream().mapToInt(String::length);
double totalHarga = produk.stream().mapToDouble(Produk::harga).sum();

flatMap — Meratakan Stream Bersarang #

flatMap berguna saat setiap elemen menghasilkan beberapa elemen — ia “meratakan” stream dua tingkat menjadi satu.

// Masalah: setiap order punya beberapa item
record Order(String id, List<String> items) {}

List<Order> orders = List.of(
    new Order("O1", List.of("Laptop", "Mouse")),
    new Order("O2", List.of("Keyboard")),
    new Order("O3", List.of("Monitor", "HDMI Cable", "Stand"))
);

// map menghasilkan Stream<List<String>> — bersarang, sulit diproses
// flatMap menghasilkan Stream<String> — rata, mudah diproses
List<String> semuaItem = orders.stream()
    .flatMap(order -> order.items().stream()) // setiap List<String> jadi Stream<String>
    .collect(Collectors.toList());
// ["Laptop", "Mouse", "Keyboard", "Monitor", "HDMI Cable", "Stand"]

// Hitung item unik dari semua order
long itemUnik = orders.stream()
    .flatMap(o -> o.items().stream())
    .distinct()
    .count();

// Memecah kalimat menjadi kata
List<String> kalimat = List.of("Halo dunia", "Java Stream API");
List<String> kata = kalimat.stream()
    .flatMap(k -> Arrays.stream(k.split(" ")))
    .collect(Collectors.toList());
// ["Halo", "dunia", "Java", "Stream", "API"]

sorted, distinct, limit, skip, peek #

List<Integer> angka = List.of(5, 2, 8, 1, 9, 3, 7, 4, 6);

// sorted — urut ascending (natural order)
List<Integer> terurut = angka.stream()
    .sorted()
    .collect(Collectors.toList());
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

// sorted dengan Comparator
List<Produk> produkMahal = produk.stream()
    .sorted(Comparator.comparingDouble(Produk::harga).reversed())
    .collect(Collectors.toList());

// distinct — hapus duplikat (menggunakan equals())
List<Integer> unik = List.of(1, 2, 2, 3, 3, 3, 4).stream()
    .distinct()
    .collect(Collectors.toList());
// [1, 2, 3, 4]

// limit — ambil n elemen pertama
List<Integer> tigaTeratas = angka.stream()
    .sorted()
    .limit(3)
    .collect(Collectors.toList());
// [1, 2, 3]

// skip — lewati n elemen pertama
List<Integer> tanpaEmpat = angka.stream()
    .sorted()
    .skip(4)
    .collect(Collectors.toList());
// [5, 6, 7, 8, 9]

// peek — debug tanpa mengubah stream (gunakan hanya untuk debugging)
List<String> hasil = nama.stream()
    .peek(n -> System.out.println("Sebelum filter: " + n))
    .filter(n -> n.length() > 3)
    .peek(n -> System.out.println("Setelah filter: " + n))
    .collect(Collectors.toList());

Operasi Terminal #

Operasi terminal memicu eksekusi seluruh pipeline dan menghasilkan nilai konkret.

forEach dan forEachOrdered #

// forEach — iterasi (urutan tidak dijamin di parallel stream)
produk.stream().forEach(p -> System.out.println(p.nama()));

// forEachOrdered — urutan selalu sesuai sumber (penting di parallel stream)
produk.parallelStream().forEachOrdered(p -> System.out.println(p.nama()));

count, min, max, sum, average #

List<Integer> angka = List.of(3, 1, 4, 1, 5, 9, 2, 6);

long jumlah = angka.stream().count(); // 8

// min dan max menggunakan Comparator, return Optional
Optional<Integer> minimum = angka.stream().min(Integer::compareTo); // Optional[1]
Optional<Integer> maximum = angka.stream().max(Integer::compareTo); // Optional[9]

// sum dan average hanya tersedia di stream primitif
int total    = angka.stream().mapToInt(Integer::intValue).sum(); // 31
double rata  = angka.stream().mapToInt(Integer::intValue).average().orElse(0); // 3.875

// IntSummaryStatistics — semua statistik sekaligus
IntSummaryStatistics stats = angka.stream()
    .mapToInt(Integer::intValue)
    .summaryStatistics();
System.out.println("Min: " + stats.getMin());     // 1
System.out.println("Max: " + stats.getMax());     // 9
System.out.println("Sum: " + stats.getSum());     // 31
System.out.println("Avg: " + stats.getAverage()); // 3.875
System.out.println("Count: " + stats.getCount()); // 8

findFirst, findAny, anyMatch, allMatch, noneMatch #

List<String> nama = List.of("Budi", "Ani", "Citra", "Doni");

// findFirst — elemen pertama yang cocok (deterministik)
Optional<String> pertama = nama.stream()
    .filter(n -> n.startsWith("C"))
    .findFirst(); // Optional["Citra"]

// findAny — elemen mana saja yang cocok (lebih cepat di parallel)
Optional<String> mana = nama.parallelStream()
    .filter(n -> n.length() > 3)
    .findAny(); // bisa Budi, Citra, atau Doni — tidak pasti

// anyMatch — apakah ada yang cocok? (short-circuit)
boolean adaYangPanjang = nama.stream().anyMatch(n -> n.length() > 4); // true (Citra)

// allMatch — apakah semua cocok?
boolean semuaPendek = nama.stream().allMatch(n -> n.length() <= 5); // true

// noneMatch — apakah tidak ada yang cocok?
boolean tidakAdaZ = nama.stream().noneMatch(n -> n.startsWith("Z")); // true

reduce — Akumulasi Elemen #

List<Integer> angka = List.of(1, 2, 3, 4, 5);

// reduce dengan identity (nilai awal) — selalu return nilai (bukan Optional)
int jumlah = angka.stream().reduce(0, Integer::sum);    // 15
int kali   = angka.stream().reduce(1, (a, b) -> a * b); // 120

// reduce tanpa identity — return Optional (stream bisa kosong)
Optional<Integer> maks = angka.stream().reduce(Integer::max); // Optional[5]

// reduce untuk membangun string
String gabung = Stream.of("Halo", "dunia", "Java")
    .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
// "Halo dunia Java"

// Tapi untuk join string, gunakan Collectors.joining() — lebih efisien
String gabung2 = Stream.of("Halo", "dunia", "Java")
    .collect(Collectors.joining(" ")); // "Halo dunia Java"

Collectors — Mengumpulkan Hasil #

Collectors menyediakan berbagai cara untuk mengumpulkan elemen stream menjadi koleksi atau nilai agregat.

Collectors Dasar #

import java.util.stream.Collectors;

List<Produk> produk = /* ... */;

// toList() — Java 16+ (unmodifiable), atau Collectors.toList() (modifiable)
List<Produk> asList  = produk.stream().collect(Collectors.toList());
List<Produk> asListv2 = produk.stream().toList(); // Java 16+, unmodifiable

// toSet() — hilangkan duplikat
Set<String> namaProduk = produk.stream()
    .map(Produk::nama)
    .collect(Collectors.toSet());

// toMap() — buat Map dari stream
Map<Long, Produk> byId = produk.stream()
    .collect(Collectors.toMap(Produk::id, p -> p));

// toMap() dengan merge function untuk kunci duplikat
Map<Integer, String> byPanjangNama = produk.stream()
    .collect(Collectors.toMap(
        p -> p.nama().length(),            // key: panjang nama
        Produk::nama,                       // value: nama
        (existing, baru) -> existing + ", " + baru // merge jika kunci sama
    ));

// joining — gabungkan string
String namaGabung = produk.stream()
    .map(Produk::nama)
    .collect(Collectors.joining(", ", "[", "]"));
// "[Laptop, Mouse, Keyboard]"

// counting — hitung jumlah
long jumlah = produk.stream().collect(Collectors.counting());

// summarizing — statistik numerik
DoubleSummaryStatistics statsHarga = produk.stream()
    .collect(Collectors.summarizingDouble(Produk::harga));

groupingBy — Mengelompokkan Elemen #

record Mahasiswa(String nama, String jurusan, double ipk) {}

List<Mahasiswa> mahasiswa = List.of(
    new Mahasiswa("Budi",  "Informatika", 3.8),
    new Mahasiswa("Ani",   "Informatika", 3.5),
    new Mahasiswa("Citra", "Matematika",  3.9),
    new Mahasiswa("Doni",  "Matematika",  3.2),
    new Mahasiswa("Eva",   "Fisika",      3.7)
);

// Kelompokkan berdasarkan jurusan
Map<String, List<Mahasiswa>> perJurusan = mahasiswa.stream()
    .collect(Collectors.groupingBy(Mahasiswa::jurusan));
// {Informatika=[Budi, Ani], Matematika=[Citra, Doni], Fisika=[Eva]}

// Kelompokkan dan hitung per grup
Map<String, Long> jumlahPerJurusan = mahasiswa.stream()
    .collect(Collectors.groupingBy(Mahasiswa::jurusan, Collectors.counting()));
// {Informatika=2, Matematika=2, Fisika=1}

// Kelompokkan dan hitung rata-rata IPK per jurusan
Map<String, Double> rataIpkPerJurusan = mahasiswa.stream()
    .collect(Collectors.groupingBy(
        Mahasiswa::jurusan,
        Collectors.averagingDouble(Mahasiswa::ipk)
    ));

// Kelompokkan dan ambil nama saja per jurusan (downstream collector)
Map<String, List<String>> namaPerJurusan = mahasiswa.stream()
    .collect(Collectors.groupingBy(
        Mahasiswa::jurusan,
        Collectors.mapping(Mahasiswa::nama, Collectors.toList())
    ));

// Double groupingBy — kelompokkan dua tingkat
Map<String, Map<String, List<Mahasiswa>>> nested = mahasiswa.stream()
    .collect(Collectors.groupingBy(
        Mahasiswa::jurusan,
        Collectors.groupingBy(m -> m.ipk() >= 3.5 ? "Cumlaude" : "Reguler")
    ));

partitioningBy — Membagi Dua Grup #

// partitioningBy selalu menghasilkan Map<Boolean, List<T>>
Map<Boolean, List<Mahasiswa>> partisi = mahasiswa.stream()
    .collect(Collectors.partitioningBy(m -> m.ipk() >= 3.5));

List<Mahasiswa> cumlaude  = partisi.get(true);  // IPK >= 3.5
List<Mahasiswa> reguler   = partisi.get(false); // IPK < 3.5

// partitioningBy dengan downstream collector
Map<Boolean, Long> jumlahPartisi = mahasiswa.stream()
    .collect(Collectors.partitioningBy(
        m -> m.ipk() >= 3.5,
        Collectors.counting()
    ));
// {true=4, false=1}

teeing — Dua Collector Sekaligus (Java 12+) #

// Hitung minimum dan maksimum dalam satu pass
record MinMax(double min, double max) {}

MinMax minmax = mahasiswa.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Comparator.comparingDouble(Mahasiswa::ipk)),
        Collectors.maxBy(Comparator.comparingDouble(Mahasiswa::ipk)),
        (min, max) -> new MinMax(
            min.map(Mahasiswa::ipk).orElse(0.0),
            max.map(Mahasiswa::ipk).orElse(0.0)
        )
    ));
// MinMax[min=3.2, max=3.9]

Optional — Nilai yang Mungkin Tidak Ada #

Optional<T> adalah pembungkus yang secara eksplisit menyatakan bahwa nilai mungkin tidak ada — pengganti yang lebih aman dari null. Stream sering mengembalikan Optional dari operasi terminal seperti findFirst(), min(), max().

Membuat dan Menggunakan Optional #

import java.util.Optional;

// Membuat Optional
Optional<String> ada    = Optional.of("nilai");         // pasti ada
Optional<String> mungkin = Optional.ofNullable(dapatkanDariDB()); // mungkin null
Optional<String> kosong  = Optional.empty();            // pasti kosong

// ANTI-PATTERN: pakai Optional seperti null check biasa
if (ada.isPresent()) {
    String nilai = ada.get();
    System.out.println(nilai);
}

// BENAR: pakai metode fungsional Optional
ada.ifPresent(System.out::println);

// orElse — nilai default jika kosong
String hasil = mungkin.orElse("default");

// orElseGet — nilai default dari Supplier (lazy, hanya dihitung jika kosong)
String hasilLazy = mungkin.orElseGet(() -> hitungDefault());

// orElseThrow — lempar exception jika kosong
String wajibAda = mungkin.orElseThrow(() -> new RuntimeException("Data tidak ditemukan"));

// map — transformasi jika ada
Optional<Integer> panjang = ada.map(String::length); // Optional[5]

// flatMap — transformasi yang juga return Optional
Optional<String> terformat = ada.flatMap(s -> s.isEmpty() ? Optional.empty() : Optional.of(s.trim()));

// filter — kosongkan jika tidak cocok
Optional<String> panjangSaja = ada.filter(s -> s.length() > 3);

// stream() — konversi Optional ke Stream (berguna dalam flatMap)
List<String> hasil2 = List.of(Optional.of("Ada"), Optional.empty(), Optional.of("Juga"))
    .stream()
    .flatMap(Optional::stream) // hanya yang ada
    .collect(Collectors.toList());
// ["Ada", "Juga"]

Parallel Stream #

Parallel stream membagi data menjadi beberapa bagian dan memprosesnya secara paralel menggunakan ForkJoinPool. Bisa mempercepat operasi pada dataset besar, tapi tidak selalu lebih cepat.

Menggunakan Parallel Stream #

List<Integer> angkaBesar = IntStream.rangeClosed(1, 10_000_000)
    .boxed()
    .collect(Collectors.toList());

// Sequential — satu thread
long jumlahSeq = angkaBesar.stream()
    .filter(n -> n % 2 == 0)
    .mapToLong(Long::valueOf)
    .sum();

// Parallel — banyak thread (ForkJoinPool.commonPool())
long jumlahPar = angkaBesar.parallelStream()
    .filter(n -> n % 2 == 0)
    .mapToLong(Long::valueOf)
    .sum();

// Konversi dari sequential ke parallel dan sebaliknya
angkaBesar.stream()
    .parallel()   // ubah ke parallel
    .sequential() // ubah kembali ke sequential
    .forEach(System.out::println);

Kapan Parallel Stream Lebih Cepat (dan Kapan Tidak) #

// COCOK untuk parallel stream:
// - Data sangat banyak (>10.000 elemen)
// - Operasi per elemen mahal secara komputasi (CPU-bound)
// - Urutan hasil tidak penting atau dipakai forEachOrdered

// TIDAK COCOK:
// - Data sedikit — overhead threading lebih besar dari keuntungannya
// - Operasi I/O bound (parallel tidak membantu I/O)
// - Operasi yang bergantung pada state bersama (race condition!)

// ANTI-PATTERN: modifikasi koleksi dari parallel stream
List<String> hasil = new ArrayList<>();
nama.parallelStream().forEach(hasil::add); // ✗ race condition!

// BENAR: kumpulkan dengan collect()
List<String> hasilAman = nama.parallelStream()
    .filter(n -> n.length() > 3)
    .collect(Collectors.toList()); // ✓ thread-safe

Pola Idiomatis #

Transformasi Daftar Objek #

// Ubah List<Entity> → List<DTO> (pola paling umum di aplikasi)
record ProdukDTO(String nama, String hargaFormat) {}

List<ProdukDTO> dtos = produk.stream()
    .map(p -> new ProdukDTO(
        p.nama(),
        "Rp" + String.format("%,.0f", p.harga())
    ))
    .collect(Collectors.toList());

Pencarian dengan Default #

// Cari produk termahal, atau produk default jika daftar kosong
Produk termahal = produk.stream()
    .max(Comparator.comparingDouble(Produk::harga))
    .orElse(new Produk(0L, "Tidak Ada", 0));

// Cari berdasarkan kondisi
Optional<Produk> laptop = produk.stream()
    .filter(p -> p.nama().equalsIgnoreCase("laptop"))
    .findFirst();

Deduplication dengan Key Tertentu #

// Hapus duplikat berdasarkan field spesifik (bukan equals() seluruh objek)
Map<String, Produk> byNama = produk.stream()
    .collect(Collectors.toMap(
        Produk::nama,
        p -> p,
        (existing, baru) -> existing // keep first
    ));
List<Produk> unik = new ArrayList<>(byNama.values());

Mempartisi dan Memproses Dua Grup #

var partisi = produk.stream()
    .collect(Collectors.partitioningBy(p -> p.harga() > 1_000_000));

List<Produk> mahal  = partisi.get(true);
List<Produk> murah  = partisi.get(false);

System.out.println("Produk mahal: " + mahal.size());
System.out.println("Produk murah: " + murah.size());

Membangun Laporan dari Stream #

// Buat laporan ringkas dalam satu ekspresi
String laporan = produk.stream()
    .sorted(Comparator.comparingDouble(Produk::harga).reversed())
    .map(p -> String.format("%-20s Rp%,.0f", p.nama(), p.harga()))
    .collect(Collectors.joining("\n",
        "=== DAFTAR PRODUK (Harga Termahal) ===\n",
        "\n=== AKHIR LAPORAN ==="));

System.out.println(laporan);

Stream dari File #

import java.nio.file.Files;
import java.nio.file.Path;

// Proses file besar baris per baris — lazy, efisien memori
long jumlahError = Files.lines(Path.of("server.log"))
    .filter(baris -> baris.contains("ERROR"))
    .count();

// Kumpulkan semua IP unik dari log
Set<String> ipUnik = Files.lines(Path.of("access.log"))
    .map(baris -> baris.split(" ")[0]) // kolom pertama adalah IP
    .collect(Collectors.toSet());

// Ingat: Files.lines() harus ditutup!
try (Stream<String> lines = Files.lines(Path.of("data.csv"))) {
    lines.skip(1) // lewati header
         .map(line -> line.split(","))
         .filter(col -> col.length >= 3)
         .forEach(col -> System.out.println(col[0] + " → " + col[2]));
}

Kapan Menggunakan Stream #

Gunakan STREAM jika:
  ✓ Perlu filter, map, reduce pada koleksi
  ✓ Perlu pengelompokan, partisi, atau agregasi data
  ✓ Perlu transformasi daftar objek (Entity → DTO)
  ✓ Ingin kode yang lebih deklaratif dan mudah dibaca
  ✓ Perlu memproses file besar baris per baris dengan lazy evaluation

Gunakan LOOP BIASA jika:
  ✗ Perlu break/continue berdasarkan kondisi kompleks
  ✗ Perlu memodifikasi elemen yang sedang diiterasi
  ✗ Operasi sangat sederhana dan loop lebih jelas
  ✗ Perlu akses ke indeks elemen secara langsung (gunakan IntStream.range)

Gunakan PARALLEL STREAM jika:
  ✓ Dataset sangat besar (>10.000 elemen) dan operasi CPU-bound
  ✓ Urutan hasil tidak penting
  ✗ Hindari jika ada shared mutable state
  ✗ Hindari untuk operasi I/O — bukan bottleneck yang diselesaikan paralel

Anti-pattern yang perlu dihindari:
  ✗ Jangan modifikasi koleksi dari dalam stream
  ✗ Jangan gunakan peek() untuk logika bisnis, hanya debugging
  ✗ Jangan lupa tutup stream yang dibuat dari I/O (Files.lines)
  ✗ Jangan chain terlalu banyak operasi dalam satu stream tanpa komentar

Ringkasan #

  • Stream adalah pipeline lazy — operasi intermediate tidak dieksekusi sampai ada terminal yang memicunya. Ini memungkinkan optimasi seperti short-circuit dan single-pass processing.
  • filter menyaring, map mentransformasi, flatMap meratakan — ketiganya adalah operasi yang paling sering dipakai. flatMap dipakai saat setiap elemen menghasilkan beberapa elemen (daftar dalam daftar).
  • collect(Collectors.toList()) atau .toList() (Java 16+) untuk mengumpulkan ke List. Gunakan Collectors.toMap(), groupingBy(), joining(), atau partitioningBy() untuk hasil yang lebih terstruktur.
  • groupingBy() adalah collector terpenting — mengelompokkan elemen berdasarkan key function dan mendukung downstream collector untuk agregasi lebih lanjut (counting, averaging, mapping).
  • Optional menggantikan null — gunakan orElse(), orElseGet(), map(), filter(), ifPresent() daripada isPresent() + get(). Jangan kembalikan null dari metode yang menghasilkan Optional.
  • Stream primitif (IntStream, LongStream, DoubleStream) menghindari boxing overhead — pakai untuk operasi numerik berat. mapToInt(), mapToDouble(), sum(), average(), summaryStatistics() tersedia di sini.
  • Parallel stream bukan solusi universal — hanya efektif untuk dataset besar dan operasi CPU-bound. Selalu ukur dengan benchmark sebelum mengaktifkan paralel di produksi.
  • Files.lines() butuh ditutup — stream dari file memegang resource OS. Selalu bungkus dalam try-with-resources.
  • Jangan modifikasi koleksi dari dalam stream — gunakan collect() untuk menghasilkan koleksi baru, bukan memodifikasi yang sedang diiterasi.

← Sebelumnya: Mocking   Berikutnya: JSON →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact