Interface dan class Java secara bawaan terbuka: siapa pun boleh menambah implementasi baru kapan saja. Keterbukaan itu tepat untuk pustaka yang memang dimaksudkan tumbuh, tetapi menjadi masalah saat kita memodelkan hierarki yang seharusnya tertutup — misalnya hasil operasi yang memang hanya ada tiga kemungkinan: sukses, gagal, atau sedang dimuat. Selama hierarki terbuka, compiler tidak pernah bisa memastikan bahwa sebuah switch sudah menangani semua kasus, sehingga default yang membungkus sisanya menjadi kebiasaan defensif. Sealed classes, difinalkan di Java 17 melalui JEP 409, membalik keadaan: pembuat tipe menentukan daftar lengkap pewaris yang diizinkan, dan compiler menegakkannya.

Aturan sealed, permits, dan non-sealed #

Kata kunci sealed pada class atau interface wajib diikuti klausa permits yang menyebutkan seluruh subtype langsung yang diizinkan — kecuali seluruh subtype dideklarasikan dalam berkas dan modul yang sama, sehingga permits boleh dihilangkan dan compiler menyimpulkannya. Setiap subtype yang tercantum wajib memilih salah satu dari tiga sikap: final (menutup cabang), sealed (melanjutkan penutupan dengan permits sendiri), atau non-sealed (membuka cabang itu kembali secara permanen). Pewaris tak terdaftar langsung ditolak saat kompilasi dengan pesan seperti berikut, yang kita verifikasi langsung pada JDK 25:

text
error: class is not allowed to extend sealed class: Hasil (as it is not listed in its 'permits' clause)

Kombinasi ketiganya memungkinkan arsitektur campuran: sebagian hierarki terkunci rapat, sebagian cabang sengaja dibuka untuk plugin pihak ketiga.

Hasil Operasi dengan Exhaustiveness #

Contoh teruji berikut memodelkan hasil operasi sebagai hierarki tertutup tiga subtype, lalu mengolahnya lewat switch tanpa default:

public class HirarkiTertutup {

    // Hierarki tertutup: hanya tiga implementasi berikut yang diizinkan.
    sealed interface Hasil permits Sukses, Gagal, Pemuatan { }

    record Sukses(String data) implements Hasil { }
    record Gagal(int kode, String pesan) implements Hasil { }
    record Pemuatan(long milidetik) implements Hasil { }

    static String ringkas(Hasil hasil) {
        // Tanpa default: compiler menjamin seluruh subtype tercakup.
        return switch (hasil) {
            case Sukses s -> "OK: " + s.data();
            case Gagal g -> "galat " + g.kode() + " (" + g.pesan() + ")";
            case Pemuatan p -> "durasi " + p.milidetik() + " ms";
        };
    }

    public static void main(String[] args) {
        Hasil[] kejadian = {
            new Sukses("profil termuat"),
            new Gagal(404, "berkas tidak ditemukan"),
            new Pemuatan(273)
        };
        for (Hasil h : kejadian) {
            System.out.println(ringkas(h));
        }
    }
}

Output yang Dihasilkan:

text
OK: profil termuat
galat 404 (berkas tidak ditemukan)
durasi 273 ms

Bedah baris-per-baris untuk bagian pentingnya:

  1. Baris sealed interface Hasil permits Sukses, Gagal, Pemuatan { }: daftar permits menjadi kontrak pewarisan; di luar tiga nama itu, compiler menolak implementasi mana pun.
  2. Tiga baris record ... implements Hasil: record otomatis final — pilihan natural bagi subtype tersegel yang hanya membawa data, sekaligus melengkapi pasangan sealed + record menjadi idiom standar Java modern.
  3. Baris case Sukses s -> ...: type pattern (dibahas tuntas pada chapter Pattern Matching) menguji tipe sekaligus mengikat variabel s tanpa instanceof manual.
  4. Absennya default: karena selector bertipe Hasil yang tersegel, compiler membuktikan ketiga subtype tercakup. Menambah case Baru pada permits tanpa memperbarui switch langsung memicu error kompilasi di semua titik yang perlu diperbarui.

Kapan Hierarki Perlu Ditutup #

SituasiPilihanAlasan
Tipe jumlah tetap (hasil operasi, status, AST)sealedExhaustiveness memindahkan pemeriksaan ke waktu kompilasi
Pustaka ekstensi publik (SPI, plugin)interface biasa atau non-sealedPihak ketiga perlu menambah implementasi
Satu implementasi tunggalfinal classTidak ada pewarisan sama sekali

Sebagai catatan batas: subtype yang tercantum harus berada dalam modul yang sama dengan induknya (atau package yang sama bila berjalan di modul tanpa nama), dan permits hanya mengatur pewarisan langsung — cucu dari subtype sealed tetap diatur oleh permits subtype itu sendiri. Reflection API juga ikut tertib: Class.getPermittedSubclasses() mengembalikan daftar pewaris yang diizinkan sehingga pustaka serialisasi dan binding dapat memanfaatkan informasi tersebut.

Hierarki tertutup memberi compiler daftar kandidat lengkap; pattern matching adalah mesin yang memanfaatkan daftar itu pada instanceof dan switch. Dua chapter berikutnya membedah mesin tersebut.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

HirarkiTertutup.java

Unduh berkas
java
public class HirarkiTertutup {

    // Hierarki tertutup: hanya tiga implementasi berikut yang diizinkan.
    sealed interface Hasil permits Sukses, Gagal, Pemuatan { }

    record Sukses(String data) implements Hasil { }
    record Gagal(int kode, String pesan) implements Hasil { }
    record Pemuatan(long milidetik) implements Hasil { }

    static String ringkas(Hasil hasil) {
        // Tanpa default: compiler menjamin seluruh subtype tercakup.
        return switch (hasil) {
            case Sukses s -> "OK: " + s.data();
            case Gagal g -> "galat " + g.kode() + " (" + g.pesan() + ")";
            case Pemuatan p -> "durasi " + p.milidetik() + " ms";
        };
    }

    public static void main(String[] args) {
        Hasil[] kejadian = {
            new Sukses("profil termuat"),
            new Gagal(404, "berkas tidak ditemukan"),
            new Pemuatan(273)
        };
        for (Hasil h : kejadian) {
            System.out.println(ringkas(h));
        }
    }
}