Sealed Class — co to jest, zasada działania i zastosowanie

Autor: IT Sectr Opublikowano: 2026-05-26 Czas czytania: 8 min

Sealed Class — specjalny typ klasy w Kotlinie, który ogranicza hierarchię dziedziczenia do ustalonego zestawu podtypów. Wszyscy dziedziczący są deklarowani w tym samym pliku i są znane kompilatorowi, co pozwala na użycie wyczerpującego bloku when bez obowiązkowej gałęzi else. Według Kotlin Docs, 2026, zapieczętowane klasy — kluczowy mechanizm do reprezentacji ograniczonych hierarchii, takich jak stany, typy błędów i zdarzenia UI.

Najważniejsze

  • Sealed Class — klasa z ustalonym zestawem dziedziczących, zadeklarowanych w tym samym pliku.
  • Exhaustive when — kompilator sprawdza, czy wszystkie podtypy zostały obsłużone, eliminując zapomniane gałęzie else.
  • Sealed interface — Kotlin 1.5+ obsługuje zapieczętowane interfejsy do wielodziedziczenia.
  • Hierarchia błędów — sealed class to standardowy sposób typbezpiecznej obsługi błędów w Kotlinie.
  • Różnica od enum — każdy dziedziczący sealed class może zawierać unikalny stan i różną liczbę pól.

Co to jest Sealed Class?

Sealed Class (zapieczętowana klasa) — to klasa w Kotlinie oznaczona modyfikatorem sealed. Definiuje ograniczoną hierarchię typów: wszyscy możliwi dziedziczący są wymienieni w tym samym pliku, a kompilator wie o każdym z nich. To odróżnia sealed class od zwykłej otwartej klasy, której dziedziczący mogą być zadeklarowani w dowolnym miejscu.

Głównym celem sealed class jest typbezpieczna reprezentacja skończonego zestawu wariantów. Każdy dziedziczący może mieć własną strukturę danych, co czyni sealed class bardziej elastycznym niż enum. W czasie wykonania sealed class to zwykła klasa abstrakcyjna, kompilator nakłada ograniczenia tylko na etapie kompilacji.

Sealed class jest szczególnie przydatny w architekturze Android aplikacji: stany UI, wyniki zapytań sieciowych, zdarzenia nawigacyjne typu Intent i, oczywiście, hierarchie błędów — to typowe scenariusze zastosowania.

Podczas kompilacji sealed class jest optymalizowany do tablicy przejść dla wyrażeń when, co czyni go wydajniejszym niż łańcuchy if-else. W połączeniu z data class każdy dziedziczący może zawierać nie tylko stan, ale także metody, co pozwala budować samodokumentujące się modele domenowe bez kodu boilerplate.

Sealed class jest również skuteczny do reprezentacji automatów stanowych (state machine) w aplikacjach mobilnych. Każdy stan — osobny dziedziczący z unikalnymi parametrami, a przejścia między stanami są kontrolowane przez wyrażenie when. Kompilator gwarantuje, że wszystkie możliwe stany zostały obsłużone, co eliminuje błędy wykonawcze przy zmianie stanu UI lub logiki biznesowej.

Sealed Class a Enum: kluczowe różnice

Początkujący programiści Kotlina często mylą sealed class z enum, ponieważ oba ograniczają zestaw wartości. Jednak istnieje między nimi zasadnicza różnica: enum — to zestaw stałych jednego typu, sealed class — hierarchia różnych typów.

Kiedy wybrać enum

Enum jest optymalny, gdy wszystkie warianty są stałymi bez dodatkowej struktury. Na przykład dni tygodnia, statusy zamówienia czy typy akcji bez parametrów. Każda wartość enum to singleton o ustalonej nazwie.

Kiedy wybrać sealed class

Sealed class jest potrzebny, gdy każdy wariant ma własne dane. Na przykład błąd sieci zawiera kod odpowiedzi, błąd parsowania — szczegóły, a błąd autoryzacji — komunikat. Każdy dziedziczący sealed class to osobny typ z unikalnymi polami.

kotlin
// Enum — wszystkie warianty jednego typu
enum class Status { LOADING, SUCCESS, ERROR }

// Sealed class — każdy wariant z własnymi danymi
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

Sealed Interface a Sealed Class

Od Kotlin 1.5 istnieje możliwość deklarowania sealed interface. Rozszerza to koncepcję sealed na interfejsy: sealed interface również ma ustalony zestaw implementacji, ale obsługuje wielodziedziczenie.

Kiedy używać sealed interface

Sealed interface jest wygodny, gdy dziedziczący muszą implementować wiele kontraktów jednocześnie. Na przykład zdarzenie UI może być jednocześnie klikalne i śledzone. Przy sealed class trzeba by wybrać jedną klasę bazową, przy sealed interface dziedziczący implementuje oba.

Ograniczenia sealed class

Sealed class — to klasa, więc każdy dziedziczący może mieć tylko jednego rodzica. Sealed interface rozwiązuje ten problem, ale nie może zawierać stanu. Wybór między nimi zależy od zadania: potrzebna jest wspólna logika z polami — sealed class, potrzebna jest elastyczność kontraktów — sealed interface.

kotlin
sealed interface ScreenEvent {
    data class Refresh(val force: Boolean) : ScreenEvent
    data class Navigate(val route: String) : ScreenEvent
    data class ShowError(val toast: String) : ScreenEvent
}

sealed interface AnalyticsEvent {
    val name: String
    val params: Map<String, Any>
}

// Dziedziczący implementuje oba interfejsy
data class LoginClicked(
    override val name: String = "login_click",
    override val params: Map<String, Any> = emptyMap()
) : ScreenEvent, AnalyticsEvent

Sealed Class dla hierarchii błędów w tworzeniu aplikacji mobilnych

Jedno z głównych zastosowań sealed class w tworzeniu aplikacji mobilnych — typbezpieczna hierarchia błędów. Zamiast rzucać wyjątki różnych typów lub używać ogólnego Exception, sealed class zbiera wszystkie możliwe błędy dziedziny w jeden typ.

Jak zbudować hierarchię błędów

Utwórz sealed class DomainError i wypisz wszystkie rodzaje awarii jako dziedziczących. Każdy dziedziczący zawiera tylko te dane, które mają znaczenie dla danego typu błędu. Kompilator gwarantuje, że przy obsłudze błędu nie zapomnisz żadnego wariantu.

Przykład: obsługa błędów autoryzacji

Rozważmy aplikację z autoryzacją, gdzie możliwe są różne scenariusze awarii: nieprawidłowe hasło, blokada konta, problem z serwerem. Sealed class łączy je w jeden typ z wyczerpującą obsługą.

kotlin
sealed class AuthError {
    data class InvalidCredentials(
        val attempts: Int
    ) : AuthError()

    data class AccountBlocked(
        val until: Long
    ) : AuthError()

    data class NetworkFailure(
        val cause: Throwable
    ) : AuthError()

    object ServerError : AuthError()
}

fun handleError(error: AuthError): String = when (error) {
    is AuthError.InvalidCredentials ->
        "Pozostało prób: ${3 - error.attempts}"
    is AuthError.AccountBlocked ->
        "Dostęp zablokowany do ${Date(error.until)}"
    is AuthError.NetworkFailure ->
        "Sprawdź połączenie: ${error.cause.localizedMessage}"
    AuthError.ServerError ->
        "Serwer tymczasowo niedostępny"
}

Wzorce użycia Sealed Class w Androidzie

Sealed class stał się standardowym narzędziem w architekturze aplikacji Android. Rozważmy trzy kluczowe wzorce, w których sealed class jest niezastąpiony w tworzeniu aplikacji mobilnych.

  • UI State — reprezentacja ekranu jako automatu skończonego: Loading, Content, Error. Każdy stan zawiera swoje dane, a sealed class gwarantuje, że wszystkie przejścia są obsłużone.
  • Navigation Event — sealed class zamiast stałych nawigacyjnych: każdy ekran to osobny dziedziczący z parametrami trasy. Kompilator sprawdza typy argumentów.
  • Action/Intent — wzorzec Unidirectional Data Flow używa sealed class do reprezentacji wszystkich akcji, które użytkownik może wykonać na ekranie.

Osobno warto wspomnieć o zastosowaniu sealed class w Clean Architecture. Każda warstwa (data, domain, presentation) używa sealed class dla swoich typów błędów, a mappery przekształcają jeden sealed class w inny. Na przykład DataError z warstwy danych jest mapowany na DomainError dla logiki biznesowej, a następnie na UiState dla warstwy prezentacji. Zachowuje to typbezpieczeństwo na wszystkich poziomach aplikacji i gwarantuje, że żaden błąd nie pozostanie nieobsłużony.

Testowanie hierarchii sealed class

Testowanie sealed class wymaga szczególnego podejścia, ponieważ każdy dziedziczący to osobny typ z własnym stanem. Zaleca się pisanie testów parametryzowanych, które przechodzą przez wszystkich dziedziczących sealed class. Gwarantuje to, że wyrażenia when pokrywają wszystkie warianty, włącznie z nowymi dodanymi przy rozszerzaniu hierarchii.

Dla testów UI sealed class jako UiState pozwala sprawdzić wyświetlanie każdego stanu: Loading pokazuje spinner, Content — dane, Error — komunikat o błędzie. Ponieważ sealed class jest skończony, pokrycie testowe wszystkich stanów daje pełną pewność poprawności logiki UI.

Typowe błędy przy pracy z Sealed Class

Mimo prostoty koncepcji, programiści regularnie popełniają błędy przy projektowaniu hierarchii sealed class. Rozważmy główne problemy i sposoby ich unikania.

  • Dziedziczący w różnych plikach — kompilator nie pozwoli zadeklarować sealed class, jeśli dziedziczący znajdują się poza plikiem. To ograniczenie gwarantuje wyczerpujący when.
  • Mieszanie sealed i open — sealed class nie może być jednocześnie open. Jeśli potrzebna jest rozszerzalna hierarchia, użyj zwykłego abstract class, ale poświęcisz exhaustiveness.
  • Nadmierne zagnieżdżenie — sealed class w sealed class tworzy głęboką hierarchię, którą trudno utrzymać. W prostych scenariuszach wystarczą dwa poziomy.
  • Zapomniany else w when — jeśli sealed class z biblioteki nie ma exhaustiveness, kompilator nie ostrzeże o pominiętej gałęzi. Dodawaj else tylko świadomie.

Często zadawane pytania

Czy sealed class może mieć metody abstrakcyjne?

Tak, sealed class może zawierać metody abstrakcyjne, a każdy dziedziczący musi je zaimplementować. Jest to wygodne, gdy wszystkie warianty powinny udostępniać wspólny interfejs, ale z różną logiką wykonania.

Czy sealed class są dostępne w Javie?

W Java 17+ pojawiły się zapieczętowane klasy i interfejsy z modyfikatorem sealed. Android na razie obsługuje Java 17 częściowo, ale w projektach Kotlin sealed class jest dostępny od Kotlin 1.0 bez ograniczeń.

Czy sealed class może dziedziczyć po innym sealed class?

Tak, jeden sealed class może być dziedziczącym drugiego. Hierarchia sealed class pozostaje skończona: kompilator zna wszystkich dziedziczących na każdym poziomie. Pozwala to budować szczegółowe klasyfikacje błędów.

Czy sealed class wpływa na wydajność?

Sealed class nie tworzy narzutów w czasie wykonania. Kompilator optymalizuje wyrażenia when z sealed class w tablice przejść (tableswitch), co jest szybsze niż łańcuchy if-else. Wydajność jest identyczna z enum.

Jak testować hierarchie sealed class?

Każdy dziedziczący sealed class jest testowany osobno. Ponieważ sealed class jest skończony, można napisać test parametryzowany, który przechodzi przez wszystkie warianty. Daje to pełne pokrycie gałęzi bloków when.

Podsumowanie

  • Sealed class — klasa z ustalonym zestawem dziedziczących, zadeklarowanych w jednym pliku, co daje wyczerpującą analizę when na etapie kompilacji.
  • Każdy dziedziczący sealed class może mieć własną strukturę danych — to główna różnica od enum, gdzie wszystkie warianty są stałymi jednego typu.
  • Sealed interface (Kotlin 1.5+) obsługuje wielodziedziczenie, sealed class — tylko pojedyncze. Wybór zależy od potrzeby wspólnego stanu.
  • Sealed class — standardowy mechanizm dla typbezpiecznej hierarchii błędów w Kotlinie: każdy typ awarii — osobny dziedziczący z odpowiednimi polami.
  • Główne wzorce w Androidzie: UI State, Navigation Event i Action/Intent — opierają się na sealed class dla gwarancji kompletności obsługi.
  • Unikaj dziedziczących w różnych plikach, nadmiernego zagnieżdżenia i mieszania sealed z open — narusza to kontrakt skończonej hierarchii.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również