CodeGym /Kursy /JAVA 25 SELF /Sealed classes: składnia i zastosowanie

Sealed classes: składnia i zastosowanie

JAVA 25 SELF
Poziom 65, Lekcja 0
Dostępny

1. Składnia klas sealed: jak to wygląda

Zacznijmy od klasycznego problemu OOP w Javie: otwarta hierarchia dziedziczenia. W zwykłej Javie każdy może dziedziczyć po twojej klasie, o ile nie jest ona zadeklarowana jako final. To wygodne, ale czasem prowadzi do niespodzianek — na przykład nie możesz z góry wiedzieć, które dokładnie klasy będą potomkami twojej klasy, a więc nie możesz zagwarantować, że obsłużyłeś wszystkie warianty w switch lub if-else.

W rezultacie, gdy piszesz obsługę obiektów według typu (na przykład przez pattern matching w switch), jesteś zmuszony dodać gałąź default „na wszelki wypadek”: a nuż ktoś gdzieś stworzy nową podklasę?

Klasy sealed rozwiązują ten problem: pozwalają jawnie ograniczyć listę klas‑potomków. Dzięki temu hierarchia jest zamknięta i kontrolowana, a twój kod — bardziej przewidywalny i bezpieczny.

Podstawowa składnia

Klasy sealed pojawiły się w Javie 17. Deklaruje się je za pomocą modyfikatora sealed, a listę dozwolonych potomków wskazuje słowo kluczowe permits:

public sealed class Shape permits Circle, Rectangle, Square {
    // Wspólne zachowanie dla wszystkich figur
}

Tutaj zadeklarowaliśmy klasę Shape i tylko klasy Circle, Rectangle i Square mogą być jej bezpośrednimi potomkami. Nikt inny nie rozszerzy Shape — kompilator na to nie pozwoli.

Ważne: Wszystkie klasy wymienione w permits muszą być zadeklarowane w tym samym pliku lub być widoczne dla kompilatora (zwykle w tym samym pakiecie). Swoją drogą, jeśli wszyscy potomkowie są zadeklarowani w jednym pliku z klasą sealed, permits można w ogóle pominąć — kompilator sam wszystko rozpozna.

Przykład:

// Wszystko w jednym pliku — permits nie jest wymagane
public sealed class Shape {
}
final class Circle extends Shape {}
final class Rectangle extends Shape {}

Wymagania wobec potomków

Każdy z potomków musi jawnie określić swój status:

  • być final (zabrania dalszego dziedziczenia),
  • albo być sealed (i dalej ograniczać dziedziczenie),
  • albo być non-sealed (pozwala na dziedziczenie, znosi ograniczenia).

Przykład:

public sealed class Shape permits Circle, Rectangle, Square {}

public final class Circle extends Shape {}
public sealed class Rectangle extends Shape permits FilledRectangle, EmptyRectangle {}
public non-sealed class Square extends Shape {}
  • Circle — klasa finalna; nie można po niej dziedziczyć.
  • Rectangle — sam jest sealed; dopuszcza tylko dwie podklasy.
  • Square — non-sealed; każdy może ją rozszerzyć.

Minimalny przykład

public sealed class Animal permits Dog, Cat {}

public final class Dog extends Animal {}
public final class Cat extends Animal {}

Spróbuj zadeklarować nową klasę public class Wolf extends Animal {} — dostaniesz błąd kompilacji:

class Wolf is not allowed to extend sealed class Animal

2. Zastosowania klas sealed: gdzie i po co ich używać

Pattern matching i switch

Klasy sealed szczególnie dobrze łączą się z pattern matching w switch. Jeśli kompilator zna wszystkie możliwe podklasy, może upewnić się, że obsłużyłeś każdy wariant, i nawet nie wymaga gałęzi default.

public sealed interface Result permits Success, Error {}

public final class Success implements Result {
    public final String data;
    public Success(String data) { this.data = data; }
}

public final class Error implements Result {
    public final String message;
    public Error(String message) { this.message = message; }
}

public class Main {
    public static void main(String[] args) {
        Result result = new Success("Hurra!");
        switch (result) {
            case Success s -> System.out.println("Sukces: " + s.data);
            case Error e -> System.out.println("Błąd: " + e.message);
        }
    }
}

Kompilator wie, że innych wariantów Result być nie może, i nie wymaga default. Jeśli zapomnisz obsłużyć któryś wariant, kompilator od razu ci o tym przypomni.

Zwróć uwagę: począwszy od Java 21, jeśli w switch nie wszystkie warianty są obsłużone, dostaniesz błąd kompilacji. W starszych wersjach (17–20) może być potrzebne default, ale IDE i tak ostrzeże o niepełnym pokryciu.

Bezpieczeństwo i kontrola

Klasy sealed pozwalają programiście w pełni kontrolować hierarchię. Jest to szczególnie ważne dla modeli domenowych, w których zestaw wariantów powinien być stały (na przykład stan zamówienia: New, Paid, Cancelled).

Ułatwienie utrzymania i rozwoju kodu

Gdy znasz wszystkie warianty potomków, łatwiej dodawać nowe funkcje, utrzymywać kod i przeprowadzać refaktoryzację. IDE też będzie „świadome” wszystkich wariantów i pomoże w autouzupełnianiu oraz analizie.

3. Praktyczne przykłady użycia klas sealed

Przykład 1: Figury geometryczne

public sealed interface Shape permits Circle, Rectangle, Square {}

public final class Circle implements Shape {
    public final double radius;
    public Circle(double radius) { this.radius = radius; }
}

public final class Rectangle implements Shape {
    public final double width, height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
}

public final class Square implements Shape {
    public final double side;
    public Square(double side) { this.side = side; }
}

Teraz można bezpiecznie używać switch z pattern matching:

Shape shape = new Circle(5);
switch (shape) {
    case Circle c -> System.out.println("Okrąg o promieniu " + c.radius);
    case Rectangle r -> System.out.println("Prostokąt " + r.width + "x" + r.height);
    case Square s -> System.out.println("Kwadrat o boku " + s.side);
}

Przykład 2: Transakcje finansowe

public sealed interface Transaction permits Deposit, Withdraw, Transfer {}

public final class Deposit implements Transaction {
    public final double amount;
    public Deposit(double amount) { this.amount = amount; }
}

public final class Withdraw implements Transaction {
    public final double amount;
    public Withdraw(double amount) { this.amount = amount; }
}

public final class Transfer implements Transaction {
    public final double amount;
    public final String toAccount;
    public Transfer(double amount, String toAccount) {
        this.amount = amount;
        this.toAccount = toAccount;
    }
}

Teraz w obsłudze możesz mieć pewność, że nie pominąłeś żadnego typu transakcji:

Transaction tx = new Transfer(100, "ACC123");
switch (tx) {
    case Deposit d -> System.out.println("Wpłata: " + d.amount);
    case Withdraw w -> System.out.println("Wypłata: " + w.amount);
    case Transfer t -> System.out.println("Przelew: " + t.amount + " na " + t.toAccount);
}

Przykład 3: Ograniczona hierarchia z non-sealed

public sealed class Notification permits EmailNotification, SmsNotification, PushNotification {}

public final class EmailNotification extends Notification {}
public non-sealed class SmsNotification extends Notification {}
public final class PushNotification extends Notification {}

// Teraz każdy może dziedziczyć po SmsNotification
public class ViberNotification extends SmsNotification {}

4. Cechy, ograniczenia i niuanse klas sealed

Wymagania dotyczące modyfikatorów

  • Klasa sealed musi jawnie wskazać wszystkich potomków przez permits.
  • Wszyscy potomkowie muszą być albo final, albo sealed, albo non-sealed.
  • Potomkowie muszą być zadeklarowani w tym samym pliku albo być widoczni dla kompilatora.

Abstrakcyjne klasy sealed

Klasa sealed może być zarówno abstrakcyjna, jak i interface, jak i zwykła klasa. Na przykład:

public sealed abstract class Expr permits Const, Add, Mul {}

Zgodność z innymi modyfikatorami

  • Nie można zadeklarować klasy sealed jako final ani non-sealed.
  • interface również może być sealed (i to bardzo wygodne!).

Użycie z klasami record

Klasy record mogą być potomkami klasy sealed, jeśli są zadeklarowane jako final (domyślnie record jest zawsze final):

public sealed interface Expr permits Const, Add, Mul {}

public record Const(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}

5. Przydatne niuanse

Klasy sealed a pattern matching: jak to się łączy

Główna zaleta klas sealed — wyczerpujący pattern matching. Brzmi groźnie, działa prosto. Kompilator zna wszystkie możliwe warianty i możesz śmiało pisać switch bez gałęzi default:

Expr expr = ...;
switch (expr) {
    case Const c -> ...
    case Add a -> ...
    case Mul m -> ...
}

Jeśli przypadkiem nie obsłużysz któregoś wariantu, kompilator nie pozwoli zbudować projektu — to bardzo wygodne i bezpieczne.

Zastosowanie w realnych zadaniach

Gdzie klasy sealed są naprawdę przydatne? Wszędzie, gdzie masz stały zestaw wariantów:

  • Wynik operacji: Success, Error (jak w przykładzie wyżej).
  • Status zamówienia: New, Paid, Cancelled.
  • Abstrakcyjne drzewa składniowe (AST) dla parserów.
  • Odpowiedzi API: Ok, NotFound, Error.
  • Zdarzenia w systemie: UserLoggedIn, UserLoggedOut, UserRegistered.

6. Typowe błędy przy pracy z klasami sealed

Błąd nr 1: zapomniano wskazać wszystkich potomków w permits.
Jeśli nie wymienisz wszystkich potrzebnych potomków przez permits, kompilator od razu to zgłosi. Na przykład jeśli napisałeś permits Circle, Rectangle, ale zapomniałeś o Square, a taka klasa istnieje — otrzymasz błąd.

Błąd nr 2: potomek nie jest final, sealed ani non-sealed.
Jeśli potomek nie jest zadeklarowany z wymaganym modyfikatorem, kompilator zgłosi błąd: „Class must be either final, sealed or non-sealed”.

Błąd nr 3: potomek jest zadeklarowany w innym pliku i niewidoczny dla kompilatora.
Wszystkie klasy wymienione w permits muszą być dostępne dla kompilatora — albo w tym samym pliku, albo w tym samym pakiecie.

Błąd nr 4: nie obsłużono wszystkich wariantów w switch.
Jeśli używasz klasy sealed w switch z pattern matching i zapomnisz obsłużyć jakiś wariant, kompilator nie pozwoli zbudować projektu. To dobrze — nie pominiesz żadnego przypadku.

Błąd nr 5: próba dziedziczenia po klasie sealed spoza permits.
Jeśli spróbujesz stworzyć klasę‑potomka, nie wskazaną w permits, otrzymasz błąd: „is not allowed to extend sealed class”.

Błąd nr 6: próba użycia klas sealed na starszych wersjach JDK.
Klasy sealed pojawiły się dopiero w Java 17. Jeśli spróbujesz użyć ich w starszej wersji, dostaniesz błąd kompilacji albo w ogóle nie zbudujesz projektu.

1
Zadanie
JAVA 25 SELF, poziom 65, lekcja 0
Niedostępne
Naruszenie ograniczeń w systemie transportu 🚫
Naruszenie ograniczeń w systemie transportu 🚫
1
Zadanie
JAVA 25 SELF, poziom 65, lekcja 0
Niedostępne
Elastyczny system powiadomień 📱
Elastyczny system powiadomień 📱
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION