Java 15 - czym są sealed classes?

W wydaniu 15 Javy pojawiła się interesująca nowość - klasy sealed, czyli klasy zaplombowane czy może zamknięte. “Nareszcie!” - pomyślałam. To jeden z tych egzotycznych smaków, które od dawna posiadała Scala. Sealed classes są też w Ceylonie, Kotlinie, Haskellu. Do Javy trafią pewnie dopiero w kolejnej wersji LTS, czyli w siedemnastce planowanej na wrzesień tego roku. Zarówno w wydaniu 15. jak i 16. są w fazie preview
.
Czym dokładnie są klasy sealed? Jaki problem rozwiązują? Jakie są potencjalne przypadki ich użycia? Na te pytania postaram się odpowiedzieć w tym wpisie.
Czym są klasy sealed?
To klasy, które nie są final i których lista klas pochodnych jest statycznie zadeklarowana w kodzie. Nazwy klas pochodnych są albo wymienione w deklaracji klasy tuż po deklaracjach extends / implements, albo klasy pochodne są zadeklarowane bezpośrednio w tej samej jednostce kompilacji.
Na etapie kompilacji hierarchia klas jest już ustalona (zaplombowana) i żadna inna klasa nie może dziedziczyć po żadnej z klas w hierarchii.
Klasy sealed są więc sposobem na ograniczenie dziedziczenia, a ściślej na ograniczenie liczby klas dziedziczących jedynie do tych, które dopuszcza programista - twórca.
Potrzeba Ograniczania
Dlaczego w ogóle ktoś chciałby ograniczać możliwość dziedziczenia klas? Czy nie jest to jawny gwałt na samej istocie paradygmatu programowania obiektowego?
Ano, chciałby. I to z kilku powodów:
- Przede wszystkim dlatego, że wprost daje wyraz swojej intencji - projektuje hierarchię klas nie przeznaczoną do rozszerzania, lecz jedynie do używania.
- Po drugie dlatego, że wymaga tego dziedzina “biznesowa”, którą chce zareprezentować w kodzie.
- Po trzecie, chce pisać prosty kod, w którym dokładnie wie, jakich podtypów może się spodziewać pod referencją do typu bazowego i na tej podstawie chce podejmować decyzje
(Uważam zresztą, że należy ograniczać na maksa wszystko, co się da, i dopuszczać nie wiecej, niż trzeba. Wtedy po prostu jest bezpiecznie i zmniejsza się przestrzeń możliwości popełnienia błędów. Jestem pewna, że takie podejście ma nawet jakąś formalną nazwę - jak ją znajdę, to tu dopiszę.)
Jaki problem rozwiązują?
Programista może chcieć na przykład:
- uniemożliwić dodawanie nowych klas dziedziczących po pewnej klasie abstrakcyjnej ze względu na specyficzny charakter interakcji między klasą-przodkiem i klasami-potomkami (tight coupling)
- zamodelować dziedzinę, której kształt jest znany i nie będzie podlegał dalszym zmianom
- stworzyć kod, w którym hierarchia klas to szczegół implementacyjny pewnego ogólniejszego rozwiązania, a abstrakcyjna klasa bazowa nie powinna być w ogóle widoczna
- mieć taki enum, którego wartości nie są singletonami, ale które mają możliwość tworzenia instancji, są templejtami wartości (doskonale pasują rekordy)
- i mieć możliwość dopasowania referencji-wzorca (patrz: pattern matching w instanceof) do listy znanych podtypów w sposób wyczerpujący
Jak wyglądają klasy sealed?
Oto prosty przykład klasy abstrakcyjnej Shape
, która jest sealed i pozwala na dziedziczenie trzem innym klasom:
|
|
Popatrzmy bliżej:
- w deklaracji klas sealed pojawia się słówko
sealed
- po sekcji
extends
/implements
jest słówkopermits
z listą dopuszczonych “bezpośrednich” typów pochodnych - każdy z dopuszczonych typów pochodnych bezpośrednio rozszerza typ sealed…
- i musi zadeklarować sposób rozszerzenia klasy sealed.
Jak można zadeklarować sposób rozrzerzania?
- klasa pochodna może zabronić roszerzania jej przez zadeklarowanie siebe jako
final
- klasa pochodna może być
sealed
- klasa pochodna może być
non-sealed
, dając możliwość dziedziczenia (a tym samym otwierając hierarchię i niszcząc korzyści wynikające z jej zamknięcia)
Na przykład:
|
|
Typy pochodne muszą być zadeklarowane w tym samym module bądź w tym samym pakiecie (jeśli klasa nadrzędna jest w module nienazwanym)
Dopasowywanie wzorca
Jeśli hierarchia klas jest zamknięta, można bezpiecznie przeiterować po liście znanych typów:
|
|
a w niedalekiej (oby!) przyszłości będzie możliwy pełen pattern matching i dopasowanie do klasy:
|
|
Przypadki użycia
Jednym z przypadków użycia jest modelowanie hierarchii bytów, dla których Enum jest niewystarczający: nie mamy ustalonej z góry listy pojedynczych bytów, lecz listę rodzajów bytów. Każdy z bytów może nieść ze sobą inne dane. I tutaj doskonale sprawdzą się rekordy, które - jako że są final z definicji - utworzyć mogą spójny front “liści” w hierarchii klas sealed.
Planety
Zamiast
|
|
chcemy mieć coś więcej: możliwość wyrażenia różnych instancji typów Planet
, Star
czy Comet
:
|
|
Wyrażenia
Jeśli użyjemy rekordów, uzyskamy charakterystyczny kod reprezentujący typy wyrażenia:
|
|
Algebraiczne typy danych
Kombinacja rekordów oraz klas sealed pozwala stwierdzić, że w Javie występują algebraiczne typy danych:
Rekordy - to typy produktowe (od “product” czyli iloczyn wektorowy; każda instancja rekordu reprezentuje krotkę w iloczynie kartezjańskim typów wchodzących w skład rekodrdu)
Klasy sealed - to typu sumowe (pewien byt, np. Caelestial
, może być Planet
ą lub Comet
ą lub Star
em)
Zakończenie
Przyjemności są programistom Javy dozowane bardzo skromnie: najpierw pattern matching w instanceof, później rekordy, później sealed classes. Ciekawe, kiedy będzie możliwy pełem pattern matching po liście typów. W wydaniu 17? Kto wie. Warto trzymać rękę na klawiaturze.
Źródła
Ten wpis jest częścią serii java.
- 2021-09-12 - Java 18: co nowego? - przegląd JEP-ów
- 2021-21-09 - Java 17 - RandomGenerator i spółka
- 2021-15-09 - Java 17 - co nowego?
- 2021-04-03 - Java 15 - czym są sealed classes?
- 2021-26-02 - Java 13 i 14: Bloki tekstowe i rekordy
- 2021-24-02 - Java 12 - wyrażenie switch (preview feature)
- 2021-23-02 - Java 11 - HTTP Client i uruchamianie jednoplikowych programów
- 2021-18-02 - Java 9 - co to jest JShell i dlaczego warto używać REPL-a w Javie
- 2021-12-02 - Java 10 - var, nowe metody w Optional, kolekcje "unmodifiable"
- 2021-11-02 - Java 9 - nowości w bibliotece
- 2021-10-02 - Java 9: czy mogę stworzyć z mojej aplikacji binarkę?
- 2021-10-02 - Java 9: praktyczny przykład - trzy moduły
- 2021-08-02 - Java 8: praktyczny przykład - przewidywanie kolejnej daty w serii