Tampilan
Kenapa penting
Di backend, banyak operasi punya beberapa kemungkinan hasil dengan data yang berbeda-beda: pembayaran bisa berhasil (membawa id transaksi), gagal (membawa alasan), atau tertunda; pencarian produk bisa menemukan data, tidak menemukan, atau menolak input. Cara lama — satu class dengan banyak field yang sebagian null plus kode status "ajaib" — rawan lupa menangani satu kasus. Sealed type membuat daftar kemungkinan itu tertutup dan diketahui compiler, sedangkan pattern matching membuat setiap kasus bisa dibongkar dan ditangani dengan ringkas. Hasilnya: kalau ada kasus yang terlewat, build gagal — bukan pelanggan yang menemukannya.
Konsep inti
sealed dan permits
Interface atau class sealed hanya boleh diturunkan oleh tipe yang disebut di klausa permits. Kombinasi yang paling sering dipakai: sealed interface dengan beberapa record.
java
sealed interface Notification permits EmailNotification, SmsNotification, PushNotification {}
record EmailNotification(String to, String subject) implements Notification {}
record SmsNotification(String phone, String text) implements Notification {}
record PushNotification(String deviceId, String title) implements Notification {}
public class Notifications {
static String send(Notification notification) {
return switch (notification) { // type pattern: cocokkan tipe + ikat variabel
case EmailNotification email -> "EMAIL ke " + email.to() + ": " + email.subject();
case SmsNotification sms -> "SMS ke " + sms.phone() + ": " + sms.text();
case PushNotification push -> "PUSH ke " + push.deviceId() + ": " + push.title();
}; // tanpa default: compiler tahu daftarnya lengkap
}
public static void main(String[] args) {
System.out.println(send(new EmailNotification("budi@contoh.com", "Pesanan dikirim")));
System.out.println(send(new SmsNotification("081234567890", "Kode OTP 482913")));
System.out.println(send(new PushNotification("dev-77", "Promo 12.12")));
}
}Output:
text
EMAIL ke budi@contoh.com: Pesanan dikirim
SMS ke 081234567890: Kode OTP 482913
PUSH ke dev-77: Promo 12.12Tipe yang tidak terdaftar ditolak. Misalnya record WhatsAppNotification(String phone) implements Notification {} menghasilkan error class is not allowed to extend sealed class: Notification (as it is not listed in its 'permits' clause). Bila semua tipe berada di file yang sama, klausa permits boleh dihilangkan — compiler mengambil daftarnya dari file itu.
final, sealed, atau non-sealed
Setiap turunan langsung dari tipe sealed wajib menyatakan sikapnya:
| Modifier | Arti |
|---|---|
final | tidak bisa diturunkan lagi (record & enum otomatis final) |
sealed | boleh diturunkan, tapi hanya oleh daftar permits-nya |
non-sealed | dibuka kembali — siapa pun boleh menurunkannya |
java
sealed abstract class Account permits SavingsAccount, CheckingAccount, LoanAccount {
abstract String type();
}
final class SavingsAccount extends Account { // final: tidak bisa diturunkan lagi
String type() { return "Tabungan"; }
}
non-sealed class CheckingAccount extends Account { // non-sealed: boleh diturunkan bebas
String type() { return "Giro"; }
}
class BusinessChecking extends CheckingAccount { // sah karena CheckingAccount non-sealed
@Override
String type() { return "Giro Bisnis"; }
}
sealed class LoanAccount extends Account permits MortgageAccount { // sealed lagi, dengan daftar sendiri
String type() { return "Pinjaman"; }
}
final class MortgageAccount extends LoanAccount {
@Override
String type() { return "KPR"; }
}
public class Accounts {
public static void main(String[] args) {
Account[] accounts = {new SavingsAccount(), new BusinessChecking(), new MortgageAccount()};
for (Account account : accounts) {
System.out.println(account.type());
}
}
}Output: Tabungan, Giro Bisnis, KPR. Pakai non-sealed dengan hemat: begitu sebuah cabang dibuka, compiler tidak lagi bisa menjamin daftar lengkap tipe di bawahnya.
Type pattern di switch dan instanceof
Di bab Polymorphism kamu sudah memakai if (x instanceof Foo f). Bentuk yang sama — disebut type pattern — bisa dipakai sebagai label case, seperti di send() di atas: case EmailNotification email -> mencocokkan tipe sekaligus memberi variabel bertipe tepat. Tanpa cast manual.
Yang membuat switch atas sealed type istimewa adalah exhaustiveness. Karena compiler tahu semua turunannya, switch tanpa default diperiksa kelengkapannya. Bila case PushNotification dihapus, kompilasi gagal dengan the switch expression does not cover all possible input values. Ini perilaku yang kamu inginkan: saat tim menambah tipe baru ke permits, setiap switch yang belum menanganinya langsung ketahuan.
Record pattern, guard when, _, dan case null
Record pattern membongkar (dekonstruksi) record langsung di label case, termasuk record yang bersarang. Guard when menambah syarat, pola _ menandai bagian yang tidak dipakai, dan case null menangani null secara eksplisit.
java
sealed interface Shipment permits Regular, Express, Pickup {}
record Address(String city, String postalCode) {}
record Regular(Address address, int weightKg) implements Shipment {}
record Express(Address address, int weightKg, boolean sameDay) implements Shipment {}
record Pickup(String storeCode) implements Shipment {}
public class Patterns {
static long shippingCost(Shipment shipment) {
return switch (shipment) {
case null -> 0; // tangani null secara eksplisit
case Pickup _ -> 0; // _ : tidak butuh isinya
case Regular(Address(String city, _), int kg) when city.equals("Jakarta") -> 9_000L * kg;
case Regular(_, int kg) -> 12_000L * kg;
case Express(_, int kg, boolean sameDay) when sameDay -> 30_000L * kg;
case Express(_, int kg, _) -> 20_000L * kg;
};
}
public static void main(String[] args) {
Address jakarta = new Address("Jakarta", "10110");
Address medan = new Address("Medan", "20111");
System.out.println(shippingCost(new Regular(jakarta, 2)));
System.out.println(shippingCost(new Regular(medan, 2)));
System.out.println(shippingCost(new Express(medan, 2, true)));
System.out.println(shippingCost(new Express(medan, 2, false)));
System.out.println(shippingCost(new Pickup("JKT-01")));
System.out.println(shippingCost(null));
}
}Output: 18000, 24000, 60000, 40000, 0, 0. Beberapa aturan:
- Case diperiksa dari atas ke bawah. Pola yang lebih spesifik (dengan
when) harus di atas pola umum. Kalau terbalik, compiler menolak denganthis case label is dominated by a preceding case label. - Tanpa
case null, switch atas nilainullmenghentikan program denganNullPointerException. - Record pattern juga bisa dipakai di
instanceof, misalnyaif (value instanceof Regular(Address(String city, _), int kg) && kg > 10). - Semua fitur ini sudah final (record pattern & pattern matching switch sejak Java 21,
_sejak Java 22) — tidak perlu flag preview.
Contoh di dunia backend
Endpoint "lihat produk" punya tiga hasil: produk ditemukan, tidak ditemukan, atau input tidak valid. Alih-alih satu class dengan field product yang kadang null dan errorCode berupa angka, hasilnya dimodelkan sebagai sealed interface. Lapisan web lalu memetakan setiap kasus ke response HTTP.
java
record Product(String sku, String name, long price) {}
// Hasil pencarian produk: tiga kemungkinan, masing-masing membawa data yang berbeda
sealed interface LookupResult permits Found, NotFound, Invalid {}
record Found(Product product) implements LookupResult {}
record NotFound(String sku) implements LookupResult {}
record Invalid(String field, String message) implements LookupResult {}
// Response HTTP sederhana: kode status + body JSON
record HttpResponse(int status, String body) {}
public class ProductApi {
static LookupResult findProduct(String sku) {
if (sku.isBlank()) {
return new Invalid("sku", "wajib diisi");
}
if (sku.equals("KOPI-250G")) {
return new Found(new Product(sku, "Kopi Gayo 250 g", 85_000));
}
return new NotFound(sku);
}
static HttpResponse toResponse(LookupResult result) {
return switch (result) {
case Found(Product(String sku, String name, long price)) ->
new HttpResponse(200, """
{"sku": "%s", "name": "%s", "price": %d}""".formatted(sku, name, price));
case NotFound(String sku) ->
new HttpResponse(404, """
{"error": "Produk %s tidak ditemukan"}""".formatted(sku));
case Invalid(String field, String message) ->
new HttpResponse(400, """
{"error": "%s %s"}""".formatted(field, message));
};
}
public static void main(String[] args) {
String[] requests = {"KOPI-250G", "TEH-100G", " "};
for (String sku : requests) {
HttpResponse response = toResponse(findProduct(sku));
System.out.println(response.status() + " " + response.body());
}
}
}Output:
text
200 {"sku": "KOPI-250G", "name": "Kopi Gayo 250 g", "price": 85000}
404 {"error": "Produk TEH-100G tidak ditemukan"}
400 {"error": "sku wajib diisi"}Setiap kasus hanya membawa data yang relevan, tidak ada field null yang harus ditebak, dan bila besok ditambah kasus Discontinued, compiler langsung menunjuk toResponse yang perlu diperbarui. Pola ini sering disebut algebraic data type dan banyak dipakai untuk hasil validasi, event domain, dan respons layanan eksternal. Di latihan bab ini kamu menerapkannya pada hasil payment gateway.
Pertanyaan interview
1. Apa itu sealed class/interface dan masalah apa yang diselesaikannya?
Sealed type membatasi siapa saja yang boleh menjadi turunannya lewat permits. Hierarki menjadi tertutup dan diketahui compiler, sehingga switch tanpa default bisa diperiksa kelengkapannya. Cocok untuk memodelkan himpunan kasus yang tetap, seperti hasil operasi atau jenis event.
2. Apa beda enum dan sealed interface berisi record?
Konstanta enum adalah object tunggal tanpa data per pemakaian — cocok untuk status atau kategori. Sealed interface dengan record membiarkan setiap kasus membawa data yang berbeda dan dibuat berkali-kali, misalnya Success(id, amount) dan Failed(reason). Keduanya sama-sama exhaustive di switch.
3. Kenapa sebaiknya tidak menambahkan default pada switch atas sealed type?
default menelan semua kasus yang belum ditangani, termasuk tipe yang ditambahkan nanti, sehingga bug lolos diam-diam. Tanpa default, compiler menolak switch yang belum lengkap dan memaksa developer memutuskan perilaku untuk kasus baru.
Jebakan umum
- Menambahkan
defaultke switch atas sealed type. Kamu kehilangan pemeriksaan kelengkapan dari compiler. - Urutan case terbalik. Pola umum di atas pola spesifik membuat compiler menolak dengan pesan dominated. Taruh pola dengan
whenlebih dulu. - Lupa
null. Switch atas nilainullmelemparNullPointerExceptionkecuali adacase null. non-sealedtanpa alasan. Cabang yang dibuka kembali tidak lagi diketahui lengkap oleh compiler.- Kembali ke
instanceofberantai (if … else if … instanceof) untuk hierarki sealed. Switch dengan pattern lebih ringkas dan diperiksa kelengkapannya. - Memakai fitur yang masih preview. Pola tipe primitif di pattern (JEP 507) masih preview di Java 25 — jangan dipakai di kode production.
Ringkasan
sealed+permitsmembatasi turunan sebuah tipe; turunan langsung wajibfinal,sealed, ataunon-sealed.- Type pattern (
case Email e ->) mencocokkan tipe dan memberi variabel bertipe tepat tanpa cast. - Record pattern membongkar record, termasuk yang bersarang;
whenmenambah syarat,_mengabaikan bagian. - Switch atas sealed type tanpa
defaultdiperiksa kelengkapannya — tipe baru membuat build gagal di tempat yang perlu diperbarui. - Gunakan sealed interface + record untuk hasil operasi dengan beberapa kemungkinan, alih-alih field
nulldan kode status ajaib.