Sealed Class

Membatasi hierarki kelas dengan sealed class Java 17 agar switch dan analisis desain menjadi ekshaustif.

Daftar isi

Kelas abstrak dan kelas biasa sama-sama terbuka bagi turunan baru dari mana pun, sedangkan final menutup semuanya sekaligus. Sejak Java 17, kelas tersegel (sealed class) mengisi celah di antara keduanya, yaitu kelas yang menetapkan sendiri daftar turunan yang boleh mewarisinya melalui permits. Topik ini mempraktikkan deklarasi hierarki tertutup, menunjukkan penolakan kompiler terhadap turunan di luar daftar, lalu menutup dengan switch yang terbukti lengkap berkat daftar kasusnya yang diketahui kompiler.

Hierarki terbuka dan akibatnya#

Hierarki kelas yang tidak disegel bersifat terbuka, artinya kelas mana pun dapat menyatakan diri sebagai turunannya selama induknya tidak final. Kelas Bentuk pada topik polimorfisme adalah contohnya, dan siapa pun boleh menambahkan Segitiga tanpa memberi tahu perancang Bentuk. Keterbukaan itu sesuai semangat pewarisan, tetapi menimbulkan dua konsekuensi yang perlu diantisipasi.

Konsekuensi pertama menyangkut pemeliharaan API. Setiap turunan baru adalah jenis tambahan yang harus ditangani oleh semua kode yang memeriksa jenis bentuk, dan perancang induk tidak memiliki kendali apa pun atas kapan jenis itu muncul. Konsekuensi kedua menyangkut kompiler, yaitu himpunan turunannya tidak pernah diketahui lengkap, sehingga setiap pemeriksaan jenis selalu butuh cabang penolong seperti else atau default untuk menjawab kasus yang tak dikenal.

Kata kunci sealed mengubah himpunan terbuka itu menjadi himpunan tertutup yang dinyatakan secara eksplisit. Daftar turunannya menjadi bagian dari kontrak kelas induk, kompiler ikut membacanya, dan setiap pelanggaran berhenti di layar kompilasi alih-alih menyusup ke masa depan.

Deklarasi sealed class dan permits#

Kelas disegel dengan kata kunci sealed di depan deklarasinya, diikuti daftar permits yang menyebut turunan langsung yang diizinkan. Setiap kelas pada daftar itu wajib memilih satu dari tiga penanda, yaitu final untuk menutup cabangnya sepenuhnya, sealed untuk melanjutkan penyegelan ke tingkat berikutnya dengan daftar permits miliknya sendiri, atau non-sealed untuk membuka cabangnya kembali kepada siapa pun. Kelas yang disegel boleh abstrak, sehingga kombinasi sealed abstract class lazim untuk hierarki yang objek induknya memang tidak perlu dibuat.

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

        for (Bentuk b : daftar) {
            System.out.println(nama(b) + " seluas " + b.luas());
        }
    }

    static String nama(Bentuk b) {
        return switch (b) {
            case Lingkaran l -> "lingkaran";
            case Persegi p -> "persegi";
        };
    }
}

sealed abstract class Bentuk permits Lingkaran, Persegi {
    abstract double luas();
}

final class Lingkaran extends Bentuk {
    double jariJari;

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

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

final class Persegi extends Bentuk {
    double sisi;

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

    @Override
    double luas() {
        return sisi * sisi;
    }
}
text Keluaran
lingkaran seluas 153.93804002589985
persegi seluas 25.0

Metode nama() menulis switch bertipe tanpa satu pun default, dan kompiler menerimanya karena Bentuk menjanjikan tidak akan ada turunan lain. Bila Persegi diganti non-sealed, jaminan itu lenyap, karena cabangnya terbuka bagi turunan tak dikenal, dan switch yang sama langsung ditolak dengan pesan the switch expression does not cover all possible input values. Kedua turunan pada contoh ini sengaja final, pilihan yang umum untuk daun hierarki yang perilakunya sudah lengkap.

Gerbang permits bekerja pada arah masuk. Kelas dari luar daftar yang mencoba mewarisi ditolak kompiler sebagaimana potongan berikut.

java Contoh: turunan di luar daftar permits ditolak
// tidak-dijalankan
class Segitiga extends Bentuk {
    @Override
    double luas() {
        return 0;
    }
}
// error: class is not allowed to extend sealed class: Bentuk
//        (as it is not listed in its 'permits' clause)

Pembatasan lokasi juga perlu diketahui. Tanpa modul bernama, kelas pada daftar permits wajib berada dalam package yang sama dengan induknya, sedangkan di dalam modul bernama semua pihak cukup berbagi modul yang sama. Bentuk satu berkas seperti contoh memenuhi kedua aturan itu sekaligus, dan bila seluruh turunan langsungnya berada di dalam berkas yang sama dengan induknya, deklarasi permits bahkan boleh dilepas karena kompiler menurunkan daftar itu sendiri.

Switch yang lengkap berkat hierarki tertutup#

Switch expression wajib exhaustive, yaitu mencakup semua kemungkinan nilai yang dapat masuk kepadanya. Pada tipe terbuka, kompiler tidak bisa membuktikan kelengkapan itu tanpa cabang default, sedangkan pada tipe yang disegel, daftar permits cukup sebagai bukti. Switch pada DemoSealed diterima tanpa default justru karena hierarkinya tertutup dan kedua turunannya terhitung lengkap.

Kelengkapan ini berubah menjadi jaring pengaman saat hierarki berkembang. Bila kelak Segitiga sah masuk ke daftar permits, setiap switch yang belum menanganinya langsung gagal kompilasi, dan daftar lokasi yang perlu disunting diberikan oleh kompiler, bukan ditemukan dari laporan bug. Menambah jenis tidak lagi menyebarkan perubahan diam-diam ke semua pemeriksaan, melainkan memunculkan error di setiap tempat yang perlu diperbarui, dan langkah perbaikannya tinggal mengikuti daftar itu.

java Contoh: switch yang kehilangan satu kasus ditolak
// tidak-dijalankan
static String nama(Bentuk b) {
    return switch (b) {
        case Lingkaran l -> "lingkaran";
    };
}
// error: the switch expression does not cover all possible input values

Pola tipe pada switch seperti case Lingkaran l distandardisasi pada Java 21 dengan mekanisme pengikatan yang sama seperti pola instanceof, sehingga aturan cakupan variabelnya mengikuti aliran kontrol yang sama. Yang berbeda hanyalah tempatnya, yaitu label switch, dan yang bertambah adalah kewajiban kelengkapannya yang dijaga hierarki tertutup.

Penyegelan paling menguntungkan ketika himpunan jenisnya memang tertutup secara domain, misalnya status pesanan, hasil evaluasi, atau jenis bentuk dalam aplikasi gambar. Sebaliknya, titik perluasan yang memang dibuka bagi pengguna framework, seperti plugin atau strategi, sebaiknya tetap berupa interface terbuka, karena daftar permits akan menghalangi perluasan yang justru diharapkan.

final, non-sealed, dan pilihan pewarisan turunan#

Setiap turunan langsung pada daftar permits memilih nasib cabangnya sendiri, dan tiga penandanya membentuk kotak alat yang saling melengkapi. Tabel berikut merangkum pilihan itu beserta akibatnya.

Penanda pada turunanAkibatnyaCocok untuk
finalcabang tertutup, tidak ada turunan lagidaun hierarki yang perilakunya sudah lengkap
sealedpenyegelan berlanjut dengan daftar permits barutingkat menengah yang masih mengatur perluasannya
non-sealedcabang dibuka kembali untuk siapa punbagian yang memang disengaja terbuka
tanpa penandaditolak kompilertidak ada, pilih salah satu dari tiga di atas

Perhatikan bahwa non-sealed bukan sekadar kembali ke keadaan awal, melainkan keputusan yang terdokumentasi pada deklarasi kelas. Induk yang disegel tetap menjaga tingkat atasnya, sedangkan turunan non-sealed miliknya dapat diperluas bebas, dan daerah mana yang terbuka tetap terlihat jelas dari deklarasinya. Kendali bertingkat seperti ini tidak dimiliki oleh pasangan terbuka-versus-final yang hanya mengenal dua keadaan.

Diagram berikut menyandingkan hierarki terbuka dan tertutup pada hierarki bentuk yang sama, dengan turunan di luar daftar yang bebas masuk di sisi kiri dan ditolak di sisi kanan.

Hierarki terbukaBentukLingkaranPersegiSegitiga (kelas apa pun)boleh masukHierarki sealedBentukpermits: Lingkaran, Persegifinal Lingkaranfinal PersegiSegitiga (di luar permits)ditolak
Hierarki terbuka menerima turunan dari mana pun, hierarki sealed menolak yang di luar permits

Mekanisme yang sama berlaku untuk sealed interface beserta permits-nya, sehingga kontrak perilaku pun dapat dibatasi daftar penerapnya. Kombinasi record, sealed interface, dan switch yang lengkap menjadi rangkaian terkenal untuk memodelkan himpunan jenis data tertutup. Fondasi penyegelannya persis seperti yang dipraktikkan pada topik ini.

Kesalahan umum#

  • Melupakan penanda pada turunan. Kelas pada daftar permits yang ditulis polos ditolak dengan pesan sealed, non-sealed or final modifiers expected. Tambahkan salah satu dari tiga penanda tersebut sesuai niat cabangnya.
  • Menuliskan kelas yang bukan turunan langsung pada permits. Daftar itu hanya menerima kelas yang memang extends induknya, dan daftar yang memuat kelas tak bertalian ditolak dengan pesan invalid permits clause. Samakan isi permits dengan deklarasi extends yang nyata.
  • Mengira satu cabang non-sealed tidak memengaruhi switch. Begitu satu turunan dibuka, kompiler kehilangan bukti kelengkapan dan switch tanpa default ditolak. Tutup kembali cabangnya, atau sediakan default yang menjawab kasus tak dikenal.
  • Menyegel hierarki tanpa alasan domain. Daftar permits adalah keputusan API yang mengikat, dan setiap perluasan sah menuntut suntingan pada kelas induknya. Sisakan interface terbuka untuk titik perluasan yang memang dirancang bagi pihak lain.

Ringkasan#

  • sealed membatasi turunan langsung pada daftar permits, dan setiap kelas pada daftar wajib final, sealed, atau non-sealed.
  • Hierarki yang tertutup memberi kompiler bukti kelengkapan, sehingga switch expression diterima tanpa default dan kasus yang hilang berhenti di kompilasi.
  • non-sealed membuka satu cabang secara terdokumentasi, dan pembukaan itu langsung menghilangkan bukti kelengkapan pada switch di atasnya.
  • Penyegelan paling menguntungkan pada himpunan jenis yang tertutup secara domain, sedangkan titik perluasan terbuka sebaiknya tetap berupa interface biasa.

Referensi#