instanceof dan Pattern Matching

Memeriksa tipe objek dengan instanceof dan merapikannya dengan pattern matching Java 16.

Daftar isi

Operator instanceof memeriksa apakah sebuah objek bertipe tertentu. Sejak Java 16, pemeriksaan itu sekaligus dapat mengikat objek yang cocok ke sebuah variabel lewat pattern matching, sehingga cast manual setelah pemeriksaan tidak lagi diperlukan. Topik ini mempraktikkan bentuk klasiknya beserta celah yang ditimbulkannya, merapikannya dengan variabel pola (pattern variable), lalu menilai kapan pemeriksaan tipe memang tempatnya dan kapan kehadirannya adalah sinyal desain yang sebaiknya diselesaikan dengan polimorfisme.

Memeriksa tipe dengan instanceof#

Tanpa pattern matching, pemeriksaan tipe dan pemanfaatan hasilnya berjalan dalam dua langkah terpisah. Operator instanceof mengembalikan nilai boolean, lalu cast eksplisit pada baris berikutnya mengubah tipe variabelnya agar anggota khas kelas turunan dapat dipanggil. Kedua langkah itu dijaga tetap sinkron oleh penulisnya, karena di mata kompiler keduanya hanyalah dua pernyataan yang berdiri sendiri.

Program berikut menghitung total luas dari seluruh lingkaran di dalam array campuran. Pemeriksaan instanceof menyaring elemen yang bukan Lingkaran, dan cast di dalam blok if mengubah tipe variabelnya sebelum luas() dipanggil.

java DemoInstanceof.java
public class DemoInstanceof {
    public static void main(String[] args) {
        Bentuk[] daftar = {
            new Lingkaran(7),
            new Persegi(5),
            new Lingkaran(2)
        };

        System.out.printf("Total luas lingkaran: %.2f%n", totalLuasLingkaran(daftar));
    }

    static double totalLuasLingkaran(Bentuk[] daftar) {
        double total = 0;
        for (Bentuk b : daftar) {
            if (b instanceof Lingkaran) {
                Lingkaran l = (Lingkaran) b; // cast eksplisit setelah pemeriksaan
                total = total + l.luas();
            }
        }
        return total;
    }
}

class Bentuk {
    double luas() {
        return 0;
    }
}

class Lingkaran extends Bentuk {
    double jariJari;

    Lingkaran(double jariJari) {
        this.jariJari = jariJari;
    }

    @Override
    double luas() {
        return Math.PI * jariJari * jariJari;
    }
}

class Persegi extends Bentuk {
    double sisi;

    Persegi(double sisi) {
        this.sisi = sisi;
    }

    @Override
    double luas() {
        return sisi * sisi;
    }
}
text Keluaran
Total luas lingkaran: 166.50

Keluarannya benar, tetapi nama Lingkaran tertulis dua kali pada dua baris berturut-turut, sekali untuk pemeriksaan dan sekali untuk cast. Kedua kemunculan itu tidak saling mengikat di mata kompiler, sehingga menyaring satu tipe sambil me-cast tipe lain tetap lolos kompilasi dan baru meledak saat program berjalan. Pola dua langkah seperti ini juga menggemakan nama kelas ke banyak baris, dan setiap baris tambahan adalah satu peluang salah ketik.

Pattern matching untuk mengikat variabel#

Pattern matching menggabungkan kedua langkah itu menjadi satu ekspresi. Bentuk b instanceof Lingkaran l menguji tipe sekaligus mengikat hasilnya ke variabel baru bernama l ketika ujiannya bernilai true, sehingga cast manual hilang dan ketidakcocokan tipe tidak lagi punya tempat. Fitur ini distandardisasi lewat JEP 394 pada Java 16 dan sejak itu menjadi bentuk yang disarankan untuk pemeriksaan tipe.

Variabel pola paling sering dipakai dalam dua pola kerja. Pola pertama adalah pemakaian langsung di dalam blok if yang mencocokkan. Pola kedua adalah pola negasi yang diakhiri return lebih awal, sehingga variabelnya terikat untuk seluruh sisa metode. Program berikut memakai pola negasi sembari menunjukkan bahwa nilai null tidak pernah cocok dengan pola apa pun.

java DemoPolaInstanceof.java
public class DemoPolaInstanceof {
    public static void main(String[] args) {
        Bentuk[] daftar = {
            new Lingkaran(7),
            new Persegi(5),
            null,
            new Lingkaran(2)
        };

        for (Bentuk b : daftar) {
            ringkas(b);
        }
    }

    static void ringkas(Bentuk b) {
        if (!(b instanceof Lingkaran l)) {
            System.out.println("bukan lingkaran, dilewati");
            return;
        }
        System.out.printf("Lingkaran jari-jari %.0f, luas %.2f%n", l.jariJari, l.luas());
    }
}

class Bentuk {
    double luas() {
        return 0;
    }
}

class Lingkaran extends Bentuk {
    double jariJari;

    Lingkaran(double jariJari) {
        this.jariJari = jariJari;
    }

    @Override
    double luas() {
        return Math.PI * jariJari * jariJari;
    }
}

class Persegi extends Bentuk {
    double sisi;

    Persegi(double sisi) {
        this.sisi = sisi;
    }

    @Override
    double luas() {
        return sisi * sisi;
    }
}
text Keluaran
Lingkaran jari-jari 7, luas 153.94
bukan lingkaran, dilewati
bukan lingkaran, dilewati
Lingkaran jari-jari 2, luas 12.57

Elemen null dan elemen Persegi sama-sama jatuh ke cabang penolakan, karena instanceof mengembalikan false untuk null alih-alih melempar error. Setelah pola negasi itu lolos, variabel l dapat dipakai bebas sampai akhir metode tanpa satu pun cast, termasuk pada printf yang membaca l.jariJari. Pola kerja if (!(x instanceof T v)) return; memecah metode menjadi dua bagian yang jelas, yaitu penolakan cepat di atas dan pekerjaan sesungguhnya di bawah tanpa indentasi tambahan.

Cakupan variabel pola mengikuti aliran kontrol#

Variabel pola bukan variabel biasa yang hidup sejak deklarasinya, melainkan terikat hanya pada titik-titik tempat kompiler dapat membuktikan bahwa polanya cocok. Mekanisme ini disebut flow scoping, yaitu cakupan yang mengikuti aliran kontrol. Variabelnya pasti tersedia di dalam cabang yang membuktikan kecocokan, di sisi kanan && pada syarat yang sama, dan setelah pola negasi yang berakhir return.

Di luar titik-titik itu, misalnya pada cabang else atau sesudah blok if yang tidak return, kompiler menolak pemakaian variabelnya karena kecocokannya belum terbukti di titik pemakai. Diagram berikut menyandingkan dua pola pemakaian beserta wilayah tempat variabel pola berlaku.

Cabang positifl berlaku di dalam blok ifif (b instanceof Lingkaran l) {// l dapat dipakai di siniSystem.out.println(l.luas());}// di sini l sudah tidak berlakuPola negasi dengan returnl berlaku pada sisa metodeif (!(b instanceof Lingkaran l)) {return;}// l dapat dipakai dari siniSystem.out.println(l.luas());
Cakupan variabel pola mengikuti bukti kecocokan yang dapat dibaca kompiler

Pemeriksaan yang tidak mungkin cocok bahkan tidak sampai ke tahap eksekusi. Karena tipe statis String tidak dapat dikonversi ke Integer, pola di bawah ini ditolak sejak kompilasi.

java Contoh: pola yang mustahil cocok ditolak kompiler
// tidak-dijalankan
String teks = "halo";
if (teks instanceof Integer n) {
    System.out.println(n);
}
// error: incompatible types: String cannot be converted to Integer

Penolakan ini menahan kesalahan yang mustahil, misalnya salah mengingat hierarki tipe. Sebaliknya, pola pada hierarki yang sah seperti Bentuk dan Lingkaran tetap diterima karena konversinya memungkinkan, meskipun nilai yang berjalan belum tentu cocok, dan pemeriksaan semacam itulah yang menjadi alasan pola ini ada.

Downcast yang gagal saat program berjalan#

Cast eksplisit tanpa pemeriksaan tetap lolos kompilasi, karena kompiler hanya memastikan bahwa konversi itu memungkinkan pada hierarki tipe, bukan bahwa isi variabelnya memang cocok. Pemeriksaan yang sebenarnya baru terjadi saat program berjalan, dan kegagalannya berupa ClassCastException. Program berikut menjalankan downcast semacam itu, lalu menangkap errornya agar program contoh tetap berjalan sampai akhir tanpa stack trace.

java DemoCastTanpaCek.java
public class DemoCastTanpaCek {
    public static void main(String[] args) {
        Bentuk b = new Persegi(5);

        try {
            Lingkaran l = (Lingkaran) b; // downcast tanpa pemeriksaan tipe
            System.out.println("Baris ini tidak pernah tercapai");
        } catch (ClassCastException e) {
            System.out.println("Tertangkap: " + e.getClass().getSimpleName());
        }
    }
}

class Bentuk {
    double luas() {
        return 0;
    }
}

class Lingkaran extends Bentuk {
    double jariJari;

    Lingkaran(double jariJari) {
        this.jariJari = jariJari;
    }
}

class Persegi extends Bentuk {
    double sisi;

    Persegi(double sisi) {
        this.sisi = sisi;
    }
}
text Keluaran
Tertangkap: ClassCastException

ClassCastException adalah exception yang dilempar JVM ketika cast antar tipe referensi menemui objek yang bukan instance dari tipe tujuannya. Error ini tidak muncul pada waktu kompilasi untuk arah turun, karena kompiler memang tidak mengetahui isi variabel bertipe induk, dan pemeriksaannya hanya terjadi saat program berjalan. Di titik inilah pemeriksaan instanceof menjadi penjaga yang dilakukan sebelum cast dieksekusi.

Bentuk lama yang memeriksa terlebih dahulu punya jebakan yang lebih licik daripada cast polos. Baris if (b instanceof Lingkaran) dan baris (Lingkaran) b tidak saling mengunci, sehingga memeriksa Lingkaran sambil me-cast Persegi masih lolos kompilasi dan baru berbentuk error saat berjalan. Dengan variabel pola, pengikatan (binding) ditulis satu kali dan ditentukan kompiler, sehingga ketidakcocokan semacam itu tidak punya celah untuk muncul.

Kapan pemeriksaan tipe memang tempatnya#

Topik polimorfisme sudah menandai kode yang penuh if (b instanceof ...) sebagai sinyal desain, karena pengetahuan tentang jenis yang seharusnya ditangani dynamic dispatch justru tersebar ke banyak lokasi. Bila rantai pemeriksaan itu melayani hierarki yang dimiliki sendiri, jawaban yang lebih sehat biasanya berupa metode pada tipe dasar yang dioverride tiap turunan, dan jenis baru pun datang tanpa menyentuh kode pemakainya.

Pemeriksaan tipe tetap menjadi alat yang tepat pada beberapa situasi. Kasus yang paling sering adalah nilai bertipe Object yang datang dari luar hierarki milik sendiri, misalnya isi antrean serbaguna atau hasil parsing yang belum bertipe. Kasus lain adalah penerapan equals yang wajib menolak objek bertipe lain sebelum membandingkan field. Contoh kecil berikut menggarisbawahi pola itu, yaitu memeriksa nilai yang tipenya memang tidak diketahui sebelum diproses.

java Contoh: pemeriksaan tipe atas nilai bertipe Object
// tidak-dijalankan
static String rincian(Object nilai) {
    if (nilai instanceof String s) {
        return "teks " + s.length() + " karakter";
    } else if (nilai instanceof Integer n) {
        return "bilangan " + n;
    }
    return "jenis lain";
}

Pemeriksaan semacam ini makin dapat diandalkan ketika hierarkinya tertutup, karena daftar kasusnya berhenti pada jumlah turunan yang dinyatakan dan kompiler ikut menjaganya. Hierarki tertutup seperti itu adalah topik sealed class pada modul ini.

Kesalahan umum#

  • Pemeriksaan dan cast yang tidak saling terikat. Pasangan if (b instanceof Lingkaran) dengan (Persegi) b lolos kompilasi, lalu berakhir ClassCastException saat berjalan. Ganti kedua langkah itu dengan satu variabel pola agar pengikatan ditentukan kompiler.
  • Memakai variabel pola di luar cakupannya. Nama variabel ditolak pada cabang else dan sesudah blok if yang tidak return, karena kecocokannya belum terbukti di titik itu. Gunakan pola negasi dengan return bila variabel dibutuhkan untuk sisa metode.
  • Mengira instanceof pernah menghasilkan true untuk null. Nilai null bukan instance dari tipe apa pun, sehingga setiap pola menolaknya dan variabel pola tidak pernah terikat null. Perlakuan khusus untuk null ditulis sebelum pemeriksaan tipe.
  • Menumpuk rantai instanceof atas hierarki milik sendiri. Setiap jenis baru memaksa suntingan di banyak lokasi pemeriksaan. Pindahkan perilaku yang berbeda itu ke metode tipe dasar, lalu biarkan dynamic dispatch memilih versinya.

Ringkasan#

  • instanceof memeriksa tipe objek dan sejak Java 16 sekaligus mengikatnya ke variabel pola, sehingga cast manual setelah pemeriksaan tidak lagi diperlukan.
  • Cakupan variabel pola mengikuti aliran kontrol, yaitu aktif di cabang yang membuktikan kecocokan dan setelah pola negasi yang return.
  • Downcast tanpa pemeriksaan gagal dengan ClassCastException, dan pemeriksaan manual yang terpisah dari cast tidak saling mengunci di mata kompiler.
  • Pemeriksaan tipe wajar untuk nilai bertipe Object dari luar hierarki sendiri, sedangkan rantai instanceof atas hierarki sendiri adalah sinyal untuk beralih ke polimorfisme.

Referensi#