1. Pengenalan Streams API
Java Streams API diperkenalkan di Java 8 dan memungkinkan kita memproses data secara deklaratif dan functional. Alih-alih menulis loop manual, kita mendeskripsikan apa yang ingin dilakukan terhadap data menggunakan pipeline operasi.
Stream bukan collection β Stream adalah representasi lazy dari sequence elemen yang mendukung operasi aggregate. Stream tidak mengubah data sumber (non-mutating) dan bisa hanya digunakan sekali (single-use).
Mengapa Menggunakan Streams?
| Keunggulan | Penjelasan |
|---|---|
| Kode Lebih Ringkas | Operasi kompleks dalam satu baris pipeline |
| Declarative | Fokus pada "apa" bukan "bagaimana" |
| Composable | Bisa dirangkai (chaining) menjadi pipeline |
| Lazy Evaluation | Operasi intermediate tidak dijalankan sampai terminal |
| Parallelizable | Mudah dijalankan secara paralel dengan parallel stream |
| Type-Safe | Compile-time type checking dengan Generics |
Stream vs Loop Tradisional
import java.util.*;
import java.util.stream.*;
public class PerbandinganStream {
public static void main(String[] args) {
List<String> nama = List.of("Budi", "Ani", "Citra", "Dina", "Eko", "Fajar");
// ===== LOOP TRADISIONAL (Imperative) =====
List<String> hasil1 = new ArrayList<>();
for (String n : nama) {
if (n.length() > 3) { // filter
hasil1.add(n.toUpperCase()); // transform
}
}
Collections.sort(hasil1); // sort
System.out.println("Loop: " + hasil1);
// ===== STREAM (Declarative) =====
List<String> hasil2 = nama.stream()
.filter(n -> n.length() > 3) // filter
.map(String::toUpperCase) // transform
.sorted() // sort
.collect(Collectors.toList()); // collect
System.out.println("Stream: " + hasil2);
// Keduanya menghasilkan: [BUDI, CITRA, DINA, FAJAR]
}
}
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β STREAM PIPELINE β β β β Source βββΆ Intermediate βββΆ Intermediate βββΆ Terminal β β (data) Operations Operations Operation β β β β βββββββ ββββββββββββ ββββββββββββ βββββββββββββ β β βList ββββΆβ .filter()ββββΆβ .map() ββββΆβ.collect() β β β β.strmβ β .sorted()β β .limit()β β .reduce() β β β βββββββ ββββββββββββ ββββββββββββ β .forEach()β β β βββββββββββββ β β Intermediate: mengembalikan Stream baru (lazy) β β Terminal: menghasilkan hasil akhir (trigger execution) β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Membuat Stream
Ada banyak cara untuk membuat Stream di Java. Berikut yang paling umum:
import java.util.*;
import java.util.stream.*;
public class MembuatStream {
public static void main(String[] args) {
// 1. Dari Collection
List<String> buah = List.of("Apel", "Mangga", "Jeruk");
Stream<String> stream1 = buah.stream();
Set<Integer> angkaSet = Set.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = angkaSet.stream();
// 2. Dari Array
String[] array = {"Java", "Python", "Go"};
Stream<String> stream3 = Arrays.stream(array);
// 3. Dari Array β sebagian (range)
int[] numbers = {10, 20, 30, 40, 50};
IntStream stream4 = Arrays.stream(numbers, 1, 4); // index 1 s/d 3
// 4. Stream.of() β langsung dari nilai
Stream<String> stream5 = Stream.of("A", "B", "C", "D");
Stream<Integer> stream6 = Stream.of(1, 2, 3);
// 5. Stream.generate() β infinite stream
Stream<Double> random = Stream.generate(Math::random).limit(5);
random.forEach(System.out::println);
// 6. Stream.iterate() β infinite stream dengan pola
Stream<Integer> genap = Stream.iterate(0, n -> n + 2).limit(10);
genap.forEach(n -> System.out.print(n + " ")); // 0 2 4 6 8 10 12 14 16 18
System.out.println();
// 7. IntStream.range() dan rangeClosed()
IntStream.range(1, 6).forEach(n -> System.out.print(n + " ")); // 1 2 3 4 5
System.out.println();
IntStream.rangeClosed(1, 6).forEach(n -> System.out.print(n + " ")); // 1 2 3 4 5 6
System.out.println();
// 8. Stream dari String (karakter)
IntStream chars = "Hello".chars();
chars.forEach(c -> System.out.print((char) c + " ")); // H e l l o
System.out.println();
// 9. Stream.concat() β menggabung dua stream
Stream<String> s1 = Stream.of("A", "B");
Stream<String> s2 = Stream.of("C", "D");
Stream<String> combined = Stream.concat(s1, s2);
combined.forEach(System.out::print); // ABCD
System.out.println();
// 10. Stream.empty() β stream kosong
Stream<String> empty = Stream.empty();
System.out.println("Empty count: " + empty.count()); // 0
}
}
Stream hanya bisa digunakan satu kali. Jika kamu mencoba mengoperasikan Stream yang sudah pernah dipakai terminal operation, kamu akan mendapat IllegalStateException: stream has already been operated upon or closed. Buat Stream baru jika perlu memproses ulang data yang sama.
3. Filter dan Map
filter menyaring elemen berdasarkan kondisi, map mengubah setiap elemen menjadi bentuk lain. Keduanya adalah intermediate operation yang paling sering digunakan.
import java.util.*;
import java.util.stream.*;
public class FilterMapDemo {
public static void main(String[] args) {
List<Integer> angka = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// ===== FILTER β saring elemen =====
// Ambil hanya angka genap
List<Integer> genap = angka.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("Genap: " + genap); // [2, 4, 6, 8, 10]
// Ambil angka > 5
List<Integer> besar = angka.stream()
.filter(n -> n > 5)
.collect(Collectors.toList());
System.out.println("> 5: " + besar); // [6, 7, 8, 9, 10]
// ===== MAP β transformasi elemen =====
// Kuadratkan semua angka
List<Integer> kuadrat = angka.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println("Kuadrat: " + kuadrat);
// [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
// ===== COMBINE filter + map =====
List<String> nama = List.of("Budi", "Ani", "Citra", "Dina", "Eko");
// Filter panjang > 3, ubah ke uppercase, tambah prefix
List<String> hasil = nama.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.map(n -> "Hi, " + n + "!")
.collect(Collectors.toList());
System.out.println("Sapaan: " + hasil);
// [Hi, BUDI!, Hi, CITRA!, Hi, DINA!, Hi, FAJAR!]
// ===== FLATMAP β flatten nested streams =====
List<List<Integer>> nested = List.of(
List.of(1, 2, 3),
List.of(4, 5),
List.of(6, 7, 8, 9)
);
// Tanpa flatmap β hanya bisa stream per list
// Dengan flatmap β gabungkan semua jadi satu stream
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println("Flat: " + flat); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// ===== DISTINCT β hapus duplikat =====
List<String> warna = List.of("Merah", "Biru", "Merah", "Hijau", "Biru");
List<String> unik = warna.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("Unik: " + unik); // [Merah, Biru, Hijau]
// ===== SORTED =====
List<Integer> acak = List.of(5, 2, 8, 1, 9, 3);
List<Integer> urut = acak.stream()
.sorted()
.collect(Collectors.toList());
System.out.println("Urut: " + urut); // [1, 2, 3, 5, 8, 9]
// ===== LIMIT & SKIP =====
List<Integer> pertama3 = angka.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println("3 pertama: " + pertama3); // [1, 2, 3]
List<Integer> skip5 = angka.stream()
.skip(5)
.collect(Collectors.toList());
System.out.println("Skip 5: " + skip5); // [6, 7, 8, 9, 10]
// ===== PEEK β debug/observe tanpa mengubah =====
List<Integer> peekDemo = angka.stream()
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("Filtered: " + n))
.map(n -> n * 10)
.peek(n -> System.out.println("Mapped: " + n))
.collect(Collectors.toList());
System.out.println("Peek result: " + peekDemo);
}
}
4. Reduce dan Akumulasi
reduce adalah terminal operation yang mengkombinasikan semua elemen stream menjadi satu nilai hasil. Ini disebut aggregation atau fold dalam functional programming.
import java.util.*;
import java.util.stream.*;
public class ReduceDemo {
public static void main(String[] args) {
List<Integer> angka = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// ===== SUM β Jumlah total =====
int total = angka.stream()
.reduce(0, Integer::sum);
System.out.println("Total: " + total); // 55
// Atau gunakan IntStream
int totalAlt = angka.stream()
.mapToInt(Integer::intValue)
.sum();
System.out.println("Total (alt): " + totalAlt); // 55
// ===== PRODUCT β Perkalian semua =====
int product = angka.stream()
.reduce(1, (a, b) -> a * b);
System.out.println("Product: " + product); // 3628800
// ===== MAX / MIN =====
Optional<Integer> max = angka.stream()
.reduce(Integer::max);
max.ifPresent(m -> System.out.println("Max: " + m)); // 10
Optional<Integer> min = angka.stream()
.reduce(Integer::min);
min.ifPresent(m -> System.out.println("Min: " + m)); // 1
// ===== CONCATENATE strings =====
List<String> kata = List.of("Java", "Streams", "API");
String gabungan = kata.stream()
.reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
System.out.println("Gabungan: " + gabungan); // "Java Streams API"
// Atau lebih mudah dengan joining
String joinSimple = kata.stream()
.collect(Collectors.joining(" "));
System.out.println("Joining: " + joinSimple);
// ===== COUNT =====
long count = angka.stream()
.filter(n -> n > 5)
.count();
System.out.println("Count > 5: " + count); // 5
// ===== STATISTICS =====
IntSummaryStatistics stats = angka.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Average: " + stats.getAverage());
// Count: 10, Sum: 55, Min: 1, Max: 10, Average: 5.5
// ===== MATCHING β anyMatch, allMatch, noneMatch =====
boolean adaGenap = angka.stream().anyMatch(n -> n % 2 == 0);
boolean semuaPositif = angka.stream().allMatch(n -> n > 0);
boolean tidakAdaNol = angka.stream().noneMatch(n -> n == 0);
System.out.println("Ada genap? " + adaGenap); // true
System.out.println("Semua positif? " + semuaPositif); // true
System.out.println("Tidak ada nol? " + tidakAdaNol); // true
// ===== FIND β findFirst, findAny =====
Optional<Integer> pertama = angka.stream()
.filter(n -> n > 5)
.findFirst();
pertama.ifPresent(n -> System.out.println("Pertama > 5: " + n)); // 6
// Reduce dengan 3 argumen (untuk parallel stream)
int totalParallel = angka.parallelStream()
.reduce(0, Integer::sum, Integer::sum);
System.out.println("Parallel sum: " + totalParallel);
}
}
5. Collectors β Terminal Operations
Collectors adalah class utility yang menyediakan berbagai method untuk mengumpulkan elemen stream menjadi collection, string, map, atau nilai agregat.
import java.util.*;
import java.util.stream.*;
public class CollectorsDemo {
// Class data untuk contoh
record Mahasiswa(String nama, String jurusan, double ipk) {}
public static void main(String[] args) {
List<Mahasiswa> mhs = List.of(
new Mahasiswa("Budi", "TI", 3.50),
new Mahasiswa("Ani", "SI", 3.89),
new Mahasiswa("Citra", "TI", 3.75),
new Mahasiswa("Dina", "SI", 3.92),
new Mahasiswa("Eko", "TI", 3.20),
new Mahasiswa("Fajar", "SI", 3.65)
);
// ===== toList() =====
List<String> namaList = mhs.stream()
.map(Mahasiswa::nama)
.collect(Collectors.toList());
System.out.println("Nama: " + namaList);
// ===== toSet() β hapus duplikat =====
Set<String> jurusan = mhs.stream()
.map(Mahasiswa::jurusan)
.collect(Collectors.toSet());
System.out.println("Jurusan: " + jurusan); // [TI, SI]
// ===== toMap() =====
Map<String, Double> namaIpk = mhs.stream()
.collect(Collectors.toMap(Mahasiswa::nama, Mahasiswa::ipk));
System.out.println("Map: " + namaIpk);
// ===== joining() β gabung String =====
String semuaNama = mhs.stream()
.map(Mahasiswa::nama)
.collect(Collectors.joining(", "));
System.out.println("Joining: " + semuaNama);
// "Budi, Ani, Citra, Dina, Eko, Fajar"
String denganPrefix = mhs.stream()
.map(Mahasiswa::nama)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("Joining: " + denganPrefix); // [Budi, Ani, ...]
// ===== counting() =====
long jumlah = mhs.stream()
.filter(m -> m.ipk() > 3.5)
.collect(Collectors.counting());
System.out.println("IPK > 3.5: " + jumlah);
// ===== summingDouble / averagingDouble =====
double totalIpk = mhs.stream()
.collect(Collectors.summingDouble(Mahasiswa::ipk));
System.out.println("Total IPK: " + totalIpk);
double rataIpk = mhs.stream()
.collect(Collectors.averagingDouble(Mahasiswa::ipk));
System.out.println("Rata-rata IPK: " + rataIpk);
// ===== groupingBy() β kelompokkan data =====
Map<String, List<Mahasiswa>> perJurusan = mhs.stream()
.collect(Collectors.groupingBy(Mahasiswa::jurusan));
System.out.println("\nPer Jurusan:");
perJurusan.forEach((j, list) -> {
System.out.println(" " + j + ": " + list.stream()
.map(Mahasiswa::nama).collect(Collectors.joining(", ")));
});
// Grouping dengan counting
Map<String, Long> countJurusan = mhs.stream()
.collect(Collectors.groupingBy(Mahasiswa::jurusan, Collectors.counting()));
System.out.println("Count per jurusan: " + countJurusan); // {TI=3, SI=3}
// Grouping dengan averaging
Map<String, Double> avgJurusan = mhs.stream()
.collect(Collectors.groupingBy(Mahasiswa::jurusan,
Collectors.averagingDouble(Mahasiswa::ipk)));
System.out.println("Avg IPK per jurusan: " + avgJurusan);
// ===== partitioningBy() β bagi jadi true/false =====
Map<Boolean, List<Mahasiswa>> partisi = mhs.stream()
.collect(Collectors.partitioningBy(m -> m.ipk() >= 3.7));
System.out.println("\nIPK >= 3.7: " + partisi.get(true).stream()
.map(Mahasiswa::nama).collect(Collectors.joining(", ")));
System.out.println("IPK < 3.7: " + partisi.get(false).stream()
.map(Mahasiswa::nama).collect(Collectors.joining(", ")));
// ===== maxBy / minBy =====
mhs.stream()
.collect(Collectors.maxBy(Comparator.comparingDouble(Mahasiswa::ipk)))
.ifPresent(m -> System.out.println("\nIPK tertinggi: " + m.nama() + " (" + m.ipk() + ")"));
}
}
6. Advanced Operations
import java.util.*;
import java.util.stream.*;
public class AdvancedStreamDemo {
record Produk(String nama, String kategori, double harga) {}
public static void main(String[] args) {
List<Produk> produk = List.of(
new Produk("Laptop", "Elektronik", 12000000),
new Produk("Mouse", "Elektronik", 150000),
new Produk("Baju", "Fashion", 250000),
new Produk("Celana", "Fashion", 300000),
new Produk("HP", "Elektronik", 5000000),
new Produk("Sepatu", "Fashion", 450000),
new Produk("Tablet", "Elektronik", 3500000)
);
// ===== Pipeline Kompleks =====
// Cari 3 produk Elektronik termurah, format harganya
System.out.println("=== 3 Elektronik Termurah ===");
produk.stream()
.filter(p -> p.kategori().equals("Elektronik"))
.sorted(Comparator.comparingDouble(Produk::harga))
.limit(3)
.map(p -> String.format("%s: Rp %,.0f", p.nama(), p.harga()))
.forEach(System.out::println);
// ===== toMap dengan merge function =====
// Jika ada key duplikat, pilih yang harga lebih tinggi
Map<String, Double> maxPerKategori = produk.stream()
.collect(Collectors.toMap(
Produk::kategori,
Produk::harga,
Math::max // merge function
));
System.out.println("\nMax per kategori: " + maxPerKategori);
// ===== Custom Collector dengan teeing =====
// (Java 12+)
// Gabungkan 2 collector sekaligus
// Contoh: hitung total dan rata-rata sekaligus
// ===== Map operations pada Stream =====
Map<String, List<Produk>> byKategori = produk.stream()
.collect(Collectors.groupingBy(Produk::kategori));
// Stream dari Map entrySet
System.out.println("\n=== Kategori Analysis ===");
byKategori.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> {
double avg = entry.getValue().stream()
.mapToDouble(Produk::harga)
.average().orElse(0);
System.out.printf("%s: %d produk, rata-rata Rp %,.0f%n",
entry.getKey(), entry.getValue().size(), avg);
});
// ===== Method reference berbagai bentuk =====
List<String> namaList = List.of("java", "PYTHON", "Go");
// Static method reference
List<String> upper = namaList.stream()
.map(String::toUpperCase) // s -> s.toUpperCase()
.collect(Collectors.toList());
// Instance method reference pada parameter
List<Integer> lengths = namaList.stream()
.map(String::length) // s -> s.length()
.collect(Collectors.toList());
// Constructor reference
List<StringBuilder> builders = namaList.stream()
.map(StringBuilder::new) // s -> new StringBuilder(s)
.collect(Collectors.toList());
System.out.println("\nUpper: " + upper);
System.out.println("Lengths: " + lengths);
// ===== toArray() =====
String[] namaArray = produk.stream()
.map(Produk::nama)
.toArray(String[]::new);
System.out.println("Array: " + Arrays.toString(namaArray));
// ===== reduce dengan identity =====
String csv = namaList.stream()
.reduce("", (a, b) -> a.isEmpty() ? b : a + ", " + b);
System.out.println("CSV: " + csv);
}
}
7. Parallel Streams
Parallel stream memproses elemen secara paralel menggunakan Fork/Join framework. Ini sangat berguna untuk dataset besar di mana operasi bisa dilakukan secara independen per elemen.
import java.util.*;
import java.util.stream.*;
public class ParallelStreamDemo {
public static void main(String[] args) {
// Membuat parallel stream
List<Integer> angka = IntStream.rangeClosed(1, 10000000)
.boxed()
.collect(Collectors.toList());
// ===== SEQUENTIAL =====
long start1 = System.currentTimeMillis();
long sum1 = angka.stream()
.mapToLong(Integer::longValue)
.sum();
long time1 = System.currentTimeMillis() - start1;
System.out.println("Sequential sum: " + sum1 + " (" + time1 + "ms)");
// ===== PARALLEL =====
long start2 = System.currentTimeMillis();
long sum2 = angka.parallelStream()
.mapToLong(Integer::longValue)
.sum();
long time2 = System.currentTimeMillis() - start2;
System.out.println("Parallel sum: " + sum2 + " (" + time2 + "ms)");
// ===== Contoh filter paralel =====
List<String> data = IntStream.rangeClosed(1, 100)
.mapToObj(i -> "Data-" + i)
.collect(Collectors.toList());
// Filter paralel
List<String> filtered = data.parallelStream()
.filter(s -> {
// Simulasi operasi berat per item
try { Thread.sleep(1); } catch (InterruptedException e) {}
return s.hashCode() % 2 == 0;
})
.collect(Collectors.toList());
System.out.println("Filtered (parallel): " + filtered.size() + " items");
// ===== Peringatan: Pastikan operasi THREAD-SAFE =====
// β JANGAN: mutable shared state
// List<Integer> hasil = new ArrayList<>();
// angka.parallelStream().filter(n -> n > 5).forEach(hasil::add); // UNSAFE!
// β
BENAR: gunakan collector
List<Integer> hasilAman = angka.parallelStream()
.filter(n -> n > 9999990)
.collect(Collectors.toList()); // collector sudah thread-safe
System.out.println("Aman: " + hasilAman);
// ===== Kapan menggunakan parallel? =====
// β
Dataset BESAR (jutaan elemen)
// β
Operasi INDEPENDEN per elemen
// β
Operasi CUKUP BERAT (bukan sekedar add/multiply)
// β
Tidak ada side effects (no mutation)
// β Dataset kecil β overhead thread lebih mahal
// β Operasi bergantung urutan (ordered)
// β I/O bound operations
// ===== Mengatur parallelism =====
// System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4");
}
}
Jangan otomatis menggunakan parallel stream untuk semua operasi. Untuk dataset kecil (<10.000 elemen), sequential stream seringkali lebih cepat karena tidak ada overhead manajemen thread. Gunakan parallel hanya untuk dataset besar dengan operasi yang benar-benar independen dan berat. Selalu benchmark!
8. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Java Streams API: