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.
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;
}
}lingkaran seluas 153.93804002589985
persegi seluas 25.0Metode 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.
// 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.
// tidak-dijalankan
static String nama(Bentuk b) {
return switch (b) {
case Lingkaran l -> "lingkaran";
};
}
// error: the switch expression does not cover all possible input valuesPola 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 turunan | Akibatnya | Cocok untuk |
|---|---|---|
final | cabang tertutup, tidak ada turunan lagi | daun hierarki yang perilakunya sudah lengkap |
sealed | penyegelan berlanjut dengan daftar permits baru | tingkat menengah yang masih mengatur perluasannya |
non-sealed | cabang dibuka kembali untuk siapa pun | bagian yang memang disengaja terbuka |
| tanpa penanda | ditolak kompiler | tidak 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.
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
permitsyang ditulis polos ditolak dengan pesansealed, 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 memangextendsinduknya, dan daftar yang memuat kelas tak bertalian ditolak dengan pesaninvalid permits clause. Samakan isipermitsdengan deklarasiextendsyang nyata. - Mengira satu cabang
non-sealedtidak memengaruhi switch. Begitu satu turunan dibuka, kompiler kehilangan bukti kelengkapan dan switch tanpadefaultditolak. Tutup kembali cabangnya, atau sediakandefaultyang menjawab kasus tak dikenal. - Menyegel hierarki tanpa alasan domain. Daftar
permitsadalah 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#
sealedmembatasi turunan langsung pada daftarpermits, dan setiap kelas pada daftar wajibfinal,sealed, ataunon-sealed.- Hierarki yang tertutup memberi kompiler bukti kelengkapan, sehingga switch expression diterima tanpa
defaultdan kasus yang hilang berhenti di kompilasi. non-sealedmembuka 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#
- JEP 409: Sealed Classes — OpenJDK
— Proposal fitur yang menetapkan
sealedbeserta aturanpermitssejak Java 17. - JLS SE 21 §8.1.1.2: sealed Classes — Oracle — Spesifikasi deklarasi kelas disegel beserta pilihan penanda turunannya.
- Sealed Classes and Interfaces — Oracle — Panduan resmi fitur pada dokumentasi pembaruan bahasa Java 21.