Menggabungkan OOP dengan gaya fungsional: lambda expression, functional interfaces Function, Predicate, Supplier dan Consumer, Stream API dengan map/filter/collect, Optional untuk menghindari null, serta method references yang membuat pipeline data ringkas dan deklaratif.

Java sering dicap "terlalu banyak boilerplate" — dan untuk Java lama, itu benar. Namun sejak Java 8, Java punya dua senjata yang mengubah gaya penulisannya: lambdas dan Stream API. Episode ini menggabungkan OOP yang sudah kalian kuasai dengan gaya fungsional — bukan menggantikan, melainkan memperkaya.
Mengapa penting? Karena hampir semua kode produksi Java modern memproses data dalam pipeline: ambil list, filter, ubah, kumpulkan. Stream API membuat pipeline ini deklaratif — kalian menyatakan apa yang ingin dicapai, bukan bagaimana caranya dengan loop dan variable sementara. Hasilnya: lebih pendek, lebih sedikit bug, lebih mudah dibaca.
Sebelum Java 8, memberi perilaku berarti menulis class anonim — verbose. Perbandingan langsung:
Runnable tugas = new Runnable() {
@Override
public void run() {
System.out.println("Menjalankan tugas");
}
};Runnable tugas = () -> System.out.println("Menjalankan tugas");Lambda () -> ... adalah singkatan untuk class anonim yang hanya punya satu method. Simbol -> memisahkan parameter (kiri) dari badan (kanan):
(int a, int b) -> a + b // dua parameter
x -> x * 2 // satu parameter (tanpa tanda kurung)
() -> System.out.println("halo") // tanpa parameterLambda hanya bisa dipakai dengan functional interface — interface dengan tepat satu method abstract. Java menyediakan yang paling sering dipakai di java.util.function:
| Interface | Method | Peran |
|---|---|---|
Function<T, R> | R apply(T) | Ubah T menjadi R |
Predicate<T> | boolean test(T) | Uji kondisi |
Supplier<T> | T get() | Produksi nilai tanpa input |
Consumer<T> | void accept(T) | Konsumsi nilai, tanpa hasil |
import java.util.function.*;
Function<String, Integer> panjang = s -> s.length();
Predicate<Integer> genap = n -> n % 2 == 0;
Supplier<String> waktu = () -> "2026-08-16";
Consumer<String> cetak = s -> System.out.println(s);
System.out.println(panjang.apply("Java")); // 4
System.out.println(genap.test(10)); // true
System.out.println(waktu.get()); // 2026-08-16
cetak.accept("Selesai");Konsep kunci yang sering mengejutkan: behavior bisa disimpan sebagai nilai. Predicate<Integer> adalah object yang membawa logika n % 2 == 0. Inilah cara OOP dan functional bertemu — lambda adalah object, dan bisa dipakai untuk polymorphism (episode 8) serta strategi (episode 15).
Masalah klasik yang diselesaikan Streams. Sebelumnya:
import java.util.ArrayList;
import java.util.List;
List<String> hasil = new ArrayList<>();
for (String nama : namaSemua) {
if (nama.startsWith("A")) {
hasil.add(nama.toUpperCase());
}
}Dengan Stream API:
import java.util.List;
import java.util.stream.Collectors;
List<String> hasil = namaSemua.stream()
.filter(nama -> nama.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());Bandingkan: yang kedua menyatakan apa (filter yang diawali A, lalu ubah besar) — bukan bagaimana (loop, variabel hasil, add manual). Komponen pipeline:
filter — menyaring dengan Predicate.map — mengubah setiap elemen dengan Function.collect — mengumpulkan hasil ke struktur (biasanya List/Set/Map).Pipeline Stream adalah lazy: operasi tidak dieksekusi sampai ada operasi terminal (seperti collect, forEach, count). Operasi antara (filter, map) hanya dicatat sebagai rencana.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> angka = List.of(1, 2, 3, 4, 5, 6);
int jumlahGenapKuadrat = angka.stream()
.filter(n -> n % 2 == 0) // Predicate
.map(n -> n * n) // Function
.mapToInt(Integer::intValue) // ke int stream
.sum();
System.out.println(jumlahGenapKuadrat); // 4 + 16 + 36 = 56
}
}Karena lazy, Java bisa memproses hanya elemen yang benar-benar dibutuhkan — misal dengan limit(3) dan findFirst(), stream tidak memproses seluruh data. Ini yang membuat Stream efisien untuk koleksi besar.
Optional adalah wadah yang bisa berisi nilai atau kosong — menggantikan cara lama mengembalikan null (dan menunggu NullPointerException):
import java.util.Optional;
public class Penyimpanan {
public static Optional<String> cariNama(String kunci) {
if ("alya".equals(kunci)) {
return Optional.of("Alya");
}
return Optional.empty();
}
public static void main(String[] args) {
Optional<String> hasil = cariNama("budi");
System.out.println(hasil.orElse("Tidak ditemukan"));
hasil.ifPresent(nama -> System.out.println("Halo " + nama));
}
}orElse("Tidak ditemukan") memberikan default tanpa menulis if, dan ifPresent(...) menjalankan aksi hanya jika nilai ada. Contract-nya eksplisit: method yang mengembalikan Optional menyatakan bahwa hasil bisa kosong — pemanggil dipaksa berpikir.
Kadang lambda cuma memanggil method yang sudah ada — persis String::toUpperCase. Method references memendekkannya:
| Bentuk | Arti |
|---|---|
String::length | s -> s.length() |
System.out::println | x -> System.out.println(x) |
Nama::new | x -> new Nama(x) |
namaSemua.stream()
.filter(nama -> nama.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);String::toUpperCase memanggil method tanpa argumen pada setiap elemen; System.out::println adalah Consumer. Method references lebih pendek sekaligus lebih jelas — gunakan saat lambda hanya mendelegasikan satu panggilan.
Warning
Jangan memakai Optional sebagai field atau parameter method — itu bukan tujuannya. Optional dirancang sebagai return type untuk menyatakan "bisa kosong". Menggunakannya di tempat lain menambah kompleksitas tanpa manfaat (detail di episode 18).
Stream bukan pengganti loop universal. Panduannya:
break/continue kompleks, mengubah elemen sambil iterasi, melacak indeks dengan cara yang tidak nyaman di stream, atau kode lebih jelas dengan state berubah.Jangan fanatik — yang penting kode bisa dibaca. Episode 21 akan mengajarkan rasa kapan gaya mana yang paling jelas.
Tip
Stream terasa aneh pertama kali justru karena ia mengubah cara berpikir: dari "bagaimana memindahkan data" ke "transformasi apa yang saya inginkan". Latihan terbaik: tulis satu logika dengan loop, lalu tulis ulang dengan stream, dan bandingkan mana yang lebih mudah dibaca.
Inti yang harus dibawa pulang:
-> memisahkan parameter dan badan.Function, Predicate, Supplier, Consumer — behavior sebagai nilai.filter → map → collect; lazy hingga operasi terminal.null yang rawan error.String::toUpperCase) memendekkan lambda yang hanya mendelegasikan.Di episode 14 selanjutnya kita akan membahas SOLID principles — Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, dan Dependency Inversion plus dependency injection. Sampai jumpa di episode 14!