Method pencarian adalah sumber NullPointerException yang paling sering menyengat: method cariSiswa(nama) menemukan siswa atau tidak menemukan apa-apa, dan versi lama Java hanya punya satu cara menyatakan “tidak ada”, yaitu mengembalikan null. Compiler tidak memaksa pemanggil memeriksa null, sehingga kelalaian baru terlihat saat program berjalan. Kelas java.util.Optional<T> sejak Java 8 menyediakan wadah berisi-nol-atau-satu-elemen yang mengangkat ketiadaan hasil menjadi bagian dari tipe method — dan stream sendiri memakainya di findFirst, max, dan average.
Masalah null dan Lahirnya Optional #
Optional<T> adalah objek pembungkus yang menyimpan paling banyak satu nilai. Tiga method statis membuatnya: Optional.of(nilai) untuk nilai yang diyakini tidak null (mengembalikan NullPointerException bila ternyata null), Optional.ofNullable(nilai) untuk nilai yang boleh null (hasilnya Optional.empty()), dan Optional.empty() untuk wadah kosong secara eksplisit.
Kekuatan sebenarnya bukan pada pembungkusnya, melainkan pada tanda tangan method. Ketika cariSiswa mendeklarasikan kembalian Optional<Siswa>, setiap pemanggil melihat di editor bahwa hasil mungkin kosong — kelalaian berubah dari bug runtime menjadi keputusan yang harus diambil saat menulis kode. Bandingkan kedua versi method berikut:
import java.util.List;
import java.util.Optional;
public class PencarianSiswa {
record Siswa(String nama, String kelas, int nilai) {}
// Versi null: ketiadaan hasil hanya diketahui lewat kebiasaan memeriksa
static Siswa cariDenganNull(List<Siswa> daftar, String nama) {
for (Siswa s : daftar) {
if (s.nama().equals(nama)) {
return s;
}
}
return null;
}
// Versi Optional: ketiadaan hasil dinyatakan di tipe kembalian
static Optional<Siswa> cariSiswa(List<Siswa> daftar, String nama) {
return daftar.stream()
.filter(s -> s.nama().equals(nama))
.findFirst();
}
public static void main(String[] args) {
List<Siswa> rombongan = List.of(
new Siswa("Budi", "X-2", 78),
new Siswa("Siti", "X-2", 92),
new Siswa("Dewi", "XI-3", 85)
);
// Pola lama: pemanggil wajib ingat memeriksa null
Siswa hasilNull = cariDenganNull(rombongan, "Ani");
if (hasilNull != null) {
System.out.println("versi null = " + hasilNull.nama());
} else {
System.out.println("versi null = tidak ditemukan");
}
// Versi Optional: map lalu orElse sebagai nilai cadangan
String kelasBudi = cariSiswa(rombongan, "Budi")
.map(Siswa::kelas)
.orElse("tidak terdaftar");
System.out.println("kelas Budi = " + kelasBudi);
String kelasAni = cariSiswa(rombongan, "Ani")
.map(Siswa::kelas)
.orElse("tidak terdaftar");
System.out.println("kelas Ani = " + kelasAni);
// filter: pipeline hanya berlanjut bila syarat terpenuhi
cariSiswa(rombongan, "Siti")
.filter(s -> s.nilai() >= 90)
.ifPresent(s -> System.out.println("nilai Siti = " + s.nilai() + " (berprestasi)"));
cariSiswa(rombongan, "Budi")
.filter(s -> s.nilai() >= 90)
.ifPresent(s -> System.out.println("nilai Budi = " + s.nilai()));
}
}Output yang Dihasilkan:
versi null = tidak ditemukan
kelas Budi = X-2
kelas Ani = tidak terdaftar
nilai Siti = 92 (berprestasi)Mari kita telaah baris-baris kode di atas:
- Baris 9-16 (
cariDenganNull): versi lama mengembalikannullsaat nama tidak ketemu. Tidak ada satu pun konstruksi bahasa yang mengingatkan pemanggil; disiplin memeriksa!= nullsepenuhnya menjadi tanggung jawab manusia. - Baris 19-23 (
cariSiswaversiOptional): pipelinefilter(...).findFirst()secara natural menghasilkanOptional<Siswa>—findFirstmemang mengembalikan wadah ini karena elemen pertama yang cocok mungkin tidak pernah ada. - Baris 41-43 (
map(Siswa::kelas).orElse(...)): bila wadah berisi siswa,mapmengambil fieldkelas; bila kosong,mapdilewati danorElsemenyuplai teks cadangan"tidak terdaftar". Rantaimap-orElsemenggantikan blokif-elsemanual secara utuh. - Baris 52-58 (
filter+ifPresent):filterpadaOptionalmenjaga pipeline hanya bila isinya memenuhi predikat. Baris 56-58 membuktikan sebaliknya: nilai Budi 78 tidak memenuhi>= 90, sehinggaifPresenttidak mencetak apa pun — tanpa exception, tanpanullyang bocor.
orElse yang Eager dan orElseGet yang Lazy #
Dua method ini tampak sama: keduanya menyuplai nilai cadangan untuk Optional kosong. Perbedaannya terletak pada kapan nilai cadangan dihitung. orElse(x) selalu mengevaluasi x lebih dulu — eager — karena x adalah argumen method biasa. orElseGet(() -> x) menyimpan Supplier dan hanya menjalankannya saat wadah benar-benar kosong — lazy. Program berikut membuktikannya dengan penghitung panggilan:
import java.util.Optional;
public class PerilakuOrElse {
static int panggilan = 0;
// Supplier yang meninggalkan jejak setiap kali dieksekusi
static int nilaiPengganti() {
panggilan++;
System.out.println(" -> nilaiPengganti() dieksekusi, panggilan ke-" + panggilan);
return 60;
}
public static void main(String[] args) {
Optional<Integer> nilai = Optional.of(85);
System.out.println("orElse dengan nilai tersedia:");
int a = nilai.orElse(nilaiPengganti());
System.out.println(" hasil = " + a + ", total panggilan = " + panggilan);
System.out.println("orElseGet dengan nilai tersedia:");
int b = nilai.orElseGet(() -> nilaiPengganti());
System.out.println(" hasil = " + b + ", total panggilan = " + panggilan);
}
}Output yang Dihasilkan:
orElse dengan nilai tersedia:
-> nilaiPengganti() dieksekusi, panggilan ke-1
hasil = 85, total panggilan = 1
orElseGet dengan nilai tersedia:
hasil = 85, total panggilan = 1Mari kita telaah baris-baris kode di atas:
- Baris 8-12 (
nilaiPengganti): setiap pemanggilan menaikkan penghitungpanggilandan mencetak jejak, sehingga eksekusinya terlihat di output berapa kali pun terjadi. - Baris 18 (
orElse(nilaiPengganti())): wadah berisi 85, hasil akhir memang 85, tetapi jejakpanggilan ke-1tetap tercetak — argumenorElsedievaluasi sebelum method dipanggil, lalu dibuang karena tidak dibutuhkan. - Baris 22 (
orElseGet(() -> nilaiPengganti())): tidak ada jejak baru; total panggilan tetap 1.Suppliertidak pernah dijalankan karena wadah berisi nilai.
ifPresentOrElse, orElseThrow, dan flatMap #
Wadah Optional juga menyediakan method untuk tiga sikap berbeda terhadap ketiadaan. ifPresentOrElse menyediakan dua cabang eksplisit (sejak Java 9), orElseThrow menyatakan bahwa ketiadaan berarti bug, dan flatMap menyambung langkah yang sama-sama menghasilkan Optional tanpa menghasilkan Optional<Optional<T>>. Konfigurasi toko berikut memakai ketiganya:
import java.util.Map;
import java.util.Optional;
public class KonfigurasiAman {
static final Map<String, String> KONFIGURASI = Map.of(
"nama-toko", "Toko Serba Ada",
"mata-uang", "IDR",
"port", "8080",
"timeout-detik", "abc"
);
// ofNullable menampung hasil Map.get yang bisa menghasilkan null
static Optional<String> ambil(String kunci) {
return Optional.ofNullable(KONFIGURASI.get(kunci));
}
// Parsing yang gagal menghasilkan Optional.empty, bukan exception
static Optional<Integer> angka(String teks) {
try {
return Optional.of(Integer.parseInt(teks));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
public static void main(String[] args) {
System.out.println("nama-toko = " + ambil("nama-toko"));
System.out.println("logo-toko = " + ambil("logo-toko"));
// ifPresentOrElse: dua cabang eksplisit, hadir atau kosong
ambil("mata-uang").ifPresentOrElse(
uang -> System.out.println("mata-uang = " + uang),
() -> System.out.println("mata-uang = (tidak diatur)"));
// orElseThrow: nilai wajib hadir, ketiadaan berarti galat
String nama = ambil("nama-toko")
.orElseThrow(() -> new IllegalStateException("nama-toko wajib diatur"));
System.out.println("nama = " + nama);
// flatMap: menyambung dua langkah yang sama-sama Optional
Optional<Integer> port = ambil("port").flatMap(KonfigurasiAman::angka);
System.out.println("port = " + port);
Optional<Integer> timeout = ambil("timeout-detik").flatMap(KonfigurasiAman::angka);
System.out.println("timeout-detik = " + timeout + " (gagal di-parse)");
}
}Output yang Dihasilkan:
nama-toko = Optional[Toko Serba Ada]
logo-toko = Optional.empty
mata-uang = IDR
nama = Toko Serba Ada
port = Optional[8080]
timeout-detik = Optional.empty (gagal di-parse)Mari kita telaah baris-baris kode di atas:
- Baris 14-16 (
Optional.ofNullable(KONFIGURASI.get(kunci))):Map.getmengembalikannulluntuk kunci yang tidak ada, danofNullablemengubahnullmenjadiOptional.empty()— pola standar membungkus API lawas yang berbasisnull. - Baris 19-25 (
angka): parsing gagal ditangkapcatchlalu diterjemahkan menjadiOptional.empty(). Teknik ini mengubah exception yang diharapkan menjadi aliran data normal. - Baris 32-34 (
ifPresentOrElse): lambda pertama berjalan bila nilai ada,Runnablekedua berjalan bila kosong. Kedua cabang ditulis berdampingan sehingga tidak ada jalur yang terlupakan. - Baris 37-38 (
orElseThrow): konfigurasinama-tokodiperlakukan wajib; ketiadaannya layak menghentikan program denganIllegalStateExceptionyang menjelaskan penyebabnya. - Baris 42 dan 45 (
flatMap):ambil("port")menghasilkanOptional<String>,angkamenghasilkanOptional<Integer>.flatMapmeratakan kedua lapisan menjadi satuOptional<Integer>—"8080"berhasil menjadiOptional[8080], sedangkan"abc"meluruh menjadiOptional.empty(). Bila memakaimap, hasilnya akan bertipeOptional<Optional<Integer>>yang harus dibuka dua kali.
Anti-pattern yang Perlu Dihindari #
Optional memberi nilai penuh saat dipakai sebagai tipe kembalian method. Beberapa kebiasaan berikut justru memundurkan kualitas kode:
Optionalsebagai field class: fieldOptionalmembuat serialisasi dan alokasi objek lebih berat; gunakan field biasa plusOptional.ofNullable(field)saat mengembalikannya.Optionalsebagai parameter method: pemanggil dipaksa membungkus nilai, dan tubuh method tetap harus menanganiOptional.empty()— dua lapis pemeriksaan untuk satu keraguan.isPresent()diikutiget(): pola ini hanyalahif (x != null)versi mahal. Ganti denganmap,filter,ifPresent, atauorElse.Optional.get()tanpa pemeriksaan: melemparNoSuchElementException: No value presentsaat kosong; method ini bahkan ditandai@Deprecatedsejak Java 10 — gunakanorElseThrow()bila nilai memang wajib ada.Optionalberisi koleksi:Optional<List<T>>berarti tiga keadaan (kosong, berisi daftar kosong, berisi daftar terisi). KembalikanList<T>kosong saja.
Dengan Optional, findFirst, dan max, kita sudah sering melihat stream menunda keputusan sampai terminal operation berjalan. Chapter berikutnya membuktikan sifat lazy itu secara eksperimental: pipeline tanpa terminal operation tidak memproses satu elemen pun, dan findFirst menghentikan seluruh aliran begitu hasilnya ditemukan.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
KonfigurasiAman.java
Unduh berkasimport java.util.Map;
import java.util.Optional;
public class KonfigurasiAman {
static final Map<String, String> KONFIGURASI = Map.of(
"nama-toko", "Toko Serba Ada",
"mata-uang", "IDR",
"port", "8080",
"timeout-detik", "abc"
);
// ofNullable menampung hasil Map.get yang bisa menghasilkan null
static Optional<String> ambil(String kunci) {
return Optional.ofNullable(KONFIGURASI.get(kunci));
}
// Parsing yang gagal menghasilkan Optional.empty, bukan exception
static Optional<Integer> angka(String teks) {
try {
return Optional.of(Integer.parseInt(teks));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
public static void main(String[] args) {
System.out.println("nama-toko = " + ambil("nama-toko"));
System.out.println("logo-toko = " + ambil("logo-toko"));
// ifPresentOrElse: dua cabang eksplisit, hadir atau kosong
ambil("mata-uang").ifPresentOrElse(
uang -> System.out.println("mata-uang = " + uang),
() -> System.out.println("mata-uang = (tidak diatur)"));
// orElseThrow: nilai wajib hadir, ketiadaan berarti galat
String nama = ambil("nama-toko")
.orElseThrow(() -> new IllegalStateException("nama-toko wajib diatur"));
System.out.println("nama = " + nama);
// flatMap: menyambung dua langkah yang sama-sama Optional
Optional<Integer> port = ambil("port").flatMap(KonfigurasiAman::angka);
System.out.println("port = " + port);
Optional<Integer> timeout = ambil("timeout-detik").flatMap(KonfigurasiAman::angka);
System.out.println("timeout-detik = " + timeout + " (gagal di-parse)");
}
}
PencarianSiswa.java
Unduh berkasimport java.util.List;
import java.util.Optional;
public class PencarianSiswa {
record Siswa(String nama, String kelas, int nilai) {}
// Versi null: ketiadaan hasil hanya diketahui lewat kebiasaan memeriksa
static Siswa cariDenganNull(List<Siswa> daftar, String nama) {
for (Siswa s : daftar) {
if (s.nama().equals(nama)) {
return s;
}
}
return null;
}
// Versi Optional: ketiadaan hasil dinyatakan di tipe kembalian
static Optional<Siswa> cariSiswa(List<Siswa> daftar, String nama) {
return daftar.stream()
.filter(s -> s.nama().equals(nama))
.findFirst();
}
public static void main(String[] args) {
List<Siswa> rombongan = List.of(
new Siswa("Budi", "X-2", 78),
new Siswa("Siti", "X-2", 92),
new Siswa("Dewi", "XI-3", 85)
);
// Pola lama: pemanggil wajib ingat memeriksa null
Siswa hasilNull = cariDenganNull(rombongan, "Ani");
if (hasilNull != null) {
System.out.println("versi null = " + hasilNull.nama());
} else {
System.out.println("versi null = tidak ditemukan");
}
// Versi Optional: map lalu orElse sebagai nilai cadangan
String kelasBudi = cariSiswa(rombongan, "Budi")
.map(Siswa::kelas)
.orElse("tidak terdaftar");
System.out.println("kelas Budi = " + kelasBudi);
String kelasAni = cariSiswa(rombongan, "Ani")
.map(Siswa::kelas)
.orElse("tidak terdaftar");
System.out.println("kelas Ani = " + kelasAni);
// filter: pipeline hanya berlanjut bila syarat terpenuhi
cariSiswa(rombongan, "Siti")
.filter(s -> s.nilai() >= 90)
.ifPresent(s -> System.out.println("nilai Siti = " + s.nilai() + " (berprestasi)"));
cariSiswa(rombongan, "Budi")
.filter(s -> s.nilai() >= 90)
.ifPresent(s -> System.out.println("nilai Budi = " + s.nilai()));
}
}
PerilakuOrElse.java
Unduh berkasimport java.util.Optional;
public class PerilakuOrElse {
static int panggilan = 0;
// Supplier yang meninggalkan jejak setiap kali dieksekusi
static int nilaiPengganti() {
panggilan++;
System.out.println(" -> nilaiPengganti() dieksekusi, panggilan ke-" + panggilan);
return 60;
}
public static void main(String[] args) {
Optional<Integer> nilai = Optional.of(85);
System.out.println("orElse dengan nilai tersedia:");
int a = nilai.orElse(nilaiPengganti());
System.out.println(" hasil = " + a + ", total panggilan = " + panggilan);
System.out.println("orElseGet dengan nilai tersedia:");
int b = nilai.orElseGet(() -> nilaiPengganti());
System.out.println(" hasil = " + b + ", total panggilan = " + panggilan);
}
}