Optional

Menggantikan null dengan Optional, memakai map dan flatMap, serta menekan NullPointerException secara benar.

Daftar isi

Optional adalah wadah yang mungkin memuat satu nilai atau kosong sama sekali, dan kelas ini dirancang untuk menggantikan pengembalian null pada metode yang pencariannya bisa gagal. Alih-alih memaksa pemanggil memeriksa null, metode yang mengembalikan Optional menyatakan kemungkinan kosong itu secara eksplisit pada tipe kembaliannya. Topik ini membahas cara membuat dan memeriksa Optional, merangkai nilainya dengan map dan flatMap, serta mengekstraksi isinya dengan aman sehingga NullPointerException tidak lagi muncul dari hasil pencarian.

Membuat dan memeriksa Optional#

Terdapat tiga pabrik pembuatan yang paling sering dipakai. Optional.of(nilai) membungkus nilai yang pasti tidak kosong dan menolak null dengan exception, Optional.empty() membuat kotak kosong yang sah, dan Optional.ofNullable(nilai) menerima keduanya dengan mengubah null menjadi kotak kosong. Pemeriksaan keberadaannya memakai isPresent() dan isEmpty(), sedangkan ekstraksi amannya memakai orElse yang memberi nilai cadangan bila kotaknya kosong.

java DemoOptionalDasar.java
import java.util.Optional;

public class DemoOptionalDasar {

    public static void main(String[] args) {
        Optional<String> judulTerisi = Optional.of("Peta Jalur Pendakian");
        Optional<String> judulKosong = Optional.empty();

        System.out.println("Terisi  : " + judulTerisi.isPresent());
        System.out.println("Kosong  : " + judulKosong.isEmpty());

        // orElse memberi nilai cadangan bila kotaknya kosong
        String judul = judulKosong.orElse("(tanpa judul)");
        System.out.println("Cadangan: " + judul);

        // map mengubah isi tanpa pemeriksaan keberadaan secara manual
        Optional<Integer> panjang = judulTerisi.map(String::length);
        System.out.println("Panjang : " + panjang.orElse(0));
        System.out.println("Panjang dari kotak kosong: "
                + judulKosong.map(String::length).orElse(0));
    }
}
text Keluaran
Terisi  : true
Kosong  : true
Cadangan: (tanpa judul)
Panjang : 20
Panjang dari kotak kosong: 0

Baris terakhir memperlihatkan keunggulan utamanya. Pemanggilan map pada kotak kosong tidak menyentuh fungsi pengubahnya sama sekali dan langsung menghasilkan kotak kosong, sehingga pola “periksa dulu, baru olah” yang biasa ditulis dengan if tidak diperlukan. Seluruh cabang kosong mengalir ke orElse di ujungnya, dan nilai cadangan itu menjadi hasil akhir yang bertipe Integer, bukan Optional<Integer> lagi.

Satu detail menarik menyertai orElse, yaitu argumennya selalu dihitung meskipun kotaknya terisi, karena Java mengevaluasi argumen pemanggilan metode lebih dahulu. Bila nilai cadangan mahal untuk dibuat, pasangannya orElseGet menerima Supplier dan hanya memanggilnya saat kotak memang kosong. Keduanya mengembalikan hasil yang sama, sehingga pilihannya semata soal biaya pembuatan nilai cadangan itu.

Merangkai nilai dengan map dan flatMap#

Perbedaan map dan flatMap baru terasa ketika fungsi pengubahnya sendiri mengembalikan Optional. map membungkus ulang hasilnya secara otomatis, sehingga fungsi yang sudah mengembalikan Optional justru menghasilkan kotak di dalam kotak, yaitu Optional<Optional<T>>. Untuk situasi itu, flatMap dipakai karena tidak membungkus ulang dan langsung meratakannya menjadi satu kotak.

Contoh klasiknya adalah pencarian berlapis, misalnya mencari anggota perpustakaan lalu memeriksa kartu anggotanya. Metode pencarian yang baik mengembalikan Optional<Anggota>, dan field kartu pada kelas Anggota memang dideklarasikan sebagai Optional<KartuAnggota> karena tidak semua anggota memiliki kartu. Rangkaian berikut menelusuri dua lapis kemungkinan kosong itu dalam satu ekspresi.

java AlurOptional.java
import java.util.Optional;

public class AlurOptional {

    record KartuAnggota(String nomorSeri) {}

    record Anggota(String nama, Optional<KartuAnggota> kartu) {}

    static Optional<Anggota> cariAnggota(String nama) {
        if (nama.equals("Dewi")) {
            return Optional.of(new Anggota("Dewi",
                    Optional.of(new KartuAnggota("KA-021"))));
        }
        if (nama.equals("Budi")) {
            return Optional.of(new Anggota("Budi", Optional.empty()));
        }
        return Optional.empty();
    }

    static String nomorKartu(Optional<Anggota> anggota) {
        return anggota
                .flatMap(a -> a.kartu())
                .map(KartuAnggota::nomorSeri)
                .orElse("tidak tersedia");
    }

    public static void main(String[] args) {
        System.out.println("Dewi : " + nomorKartu(cariAnggota("Dewi")));
        System.out.println("Budi : " + nomorKartu(cariAnggota("Budi")));
        System.out.println("Rara : " + nomorKartu(cariAnggota("Rara")));
    }
}
text Keluaran
Dewi : KA-021
Budi : tidak tersedia
Rara : tidak tersedia

Alurnya digambarkan berikut ini, dan ketiga kasusnya mengalir ke satu pintu keluar yang sama.

Optional<Anggota>Optional<KartuAnggota>Optional<String>flatMapmapbila semua kotak terisi, isi berpindah dari kotak ke kotakbila ada kotak kosongorElse("tidak tersedia")menghasilkan String yang selalu ada nilainya
Alur rangkaian Optional dari Optional Anggota sampai nilai akhir bertipe String

Kasus Budi menunjukkan kegunaan flatMap secara nyata, karena a.kartu() sudah bertipe Optional<KartuAnggota> dan map hanya akan menggandakan pembungkusnya. Kasus Rara memperlihatkan bahwa kotak kosong di lapis pertama pun berhenti di jalur yang sama tanpa satu pun if. Rangkaian sepanjang ini tetap satu ekspresi yang bertipe String, dan tiap lapis kemungkinan kosongnya tertangani oleh struktur data itu sendiri.

Ekstraksi yang menolak diam-diam#

orElse menutup cabang kosong dengan nilai cadangan, tetapi ada kasus ketika kosong justru berarti kondisi abnormal yang tidak boleh diteruskan. Untuk itu tersedia orElseThrow yang melempar NoSuchElementException bila kotaknya kosong dan mengembalikan isinya bila terisi. Melempar exception yang jelas di titik pencarian lebih aman daripada membiarkan null berjalan jauh lalu meledak di tempat yang asalnya tidak berkaitan dengan pencarian itu.

java DemoOrElseThrow.java
import java.util.Optional;

public class DemoOrElseThrow {

    public static void main(String[] args) {
        Optional<String> tanpaNilai = Optional.empty();

        try {
            String nilai = tanpaNilai.orElseThrow();
            System.out.println("Baris ini tidak pernah tercapai: " + nilai);
        } catch (java.util.NoSuchElementException e) {
            System.out.println("Exception tertangkap: " + e.getMessage());
        }
    }
}
text Keluaran
Exception tertangkap: No value present

Exception yang dilempar orElseThrow menyatakan kegagalan pencarian dengan jelas, dan penangkapannya di contoh ini hanya untuk memperlihatkan pesannya. Pemakaian try/catch seperti di atas bukan pola sehari-hari, karena kegagalan Optional sebaiknya ditangani lewat orElse, orElseGet, atau cabang lain yang disediakan kelas ini. Dengan disiplin itu, NullPointerException akibat hasil pencarian yang ternyata kosong tidak lagi punya celah untuk muncul.

Kesalahan umum#

  • Memakai Optional.of pada nilai yang mungkin null. Bentuk Optional.of(nilai) melempar NullPointerException saat itu juga bila nilai yang dikirim kosong, sehingga persoalan hanya berpindah tempat. Gunakan Optional.ofNullable bila keberadaan nilai memang tidak pasti.
  • Membungkus ulang hasil fungsi yang sudah mengembalikan Optional dengan map. Hasilnya kotak di dalam kotak yang sulit diekstraksi. Pakai flatMap untuk fungsi yang sudah mengembalikan Optional.
  • Memanggil get() tanpa pemeriksaan. get() pada kotak kosong melempar NoSuchElementException dan membawa kebiasaan lama berupa pemeriksaan isPresent() sebelum get(). Pilih orElse, orElseGet, atau orElseThrow yang menyatakan maksudnya secara langsung.
  • Menyimpan field bertipe Optional lalu membandingkannya atau mengirimnya sebagai parameter. Optional bukan dirancang untuk kedua peran itu dan membuat kontrak kelas menjadi kabur. Simpan tipe langsungnya di field, dan kembalikan Optional hanya dari metode pencarian.

Ringkasan#

  • Optional membungkus nilai yang mungkin kosong dengan pabrik of, empty, dan ofNullable, serta memeriksanya lewat isPresent dan isEmpty.
  • map mengubah isi kotak dan merawat kotak kosong secara otomatis, sedangkan flatMap merangkai fungsi yang sudah mengembalikan Optional tanpa membungkus ulang.
  • Ekstraksi amannya memakai orElse atau orElseGet untuk nilai cadangan, dan orElseThrow bila kekosongan justru abnormal.
  • Optional yang dipakai disiplin sebagai tipe kembalian metode pencarian menghapus NullPointerException dari jalur hasil pencarian.

Referensi#