Java 17: Sealed Classes

Aus MimiPedia
Version vom 21. Oktober 2023, 15:52 Uhr von Ullrich (Diskussion | Beiträge) (Die Seite wurde neu angelegt: „Category:Java __TOC__ Um die Ableitbarkeit von Klassen zu steuern bietet Java bis Versuion 16 nur eine einzige Möglichkeit, den Modifier {{java|final}}. Ist eine Klasse als {{java|final}} markiert, ist es unmöglich eine weitere Klasse von ihr abzuleiten. Ist die nicht {{java|final}}, kann jeder der Zugriff auf die Klasse hat von ihr beliebige Klassen ableiten. Das folgt dem Gedanken, daß die Klassen-Hierarchie nichts anderes ist als ein Erweiterun…“)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

Um die Ableitbarkeit von Klassen zu steuern bietet Java bis Versuion 16 nur eine einzige Möglichkeit, den Modifier final. Ist eine Klasse als final markiert, ist es unmöglich eine weitere Klasse von ihr abzuleiten. Ist die nicht final, kann jeder der Zugriff auf die Klasse hat von ihr beliebige Klassen ableiten.

Das folgt dem Gedanken, daß die Klassen-Hierarchie nichts anderes ist als ein Erweiterungs-Konzept. Möchte man Klassen so modellieren, daß dabei eine abgegrenzten DSL entsteht, eine Abstraktion über die die Elemente einer bestimmten Domäne der Verarbeitung zugänglich gemacht wird, benötigt man eine feinere Steuerung. Denn dann möchte man die Ableitung innerhalb der Anwendung einschränken und ggf. nach außen hin ganz unterbinden.

Das Konzept der sealed classes läßt uns Teile der Klassen-Hierarchie unserer Anwendung vollständig gegen Vererbung abschotten, wärend Teile der Hierarchie der Vererbung offen bleiben.

sealed classes

Betrachten wir die Klasse Shape von der nur zwei Klassen abgeleitet werden können sollen:

public sealed class Shape permits Quadrangle, Ellipse { ... }

Das context keyword sealed verschließt die Klasse gegen direkte, nicht vorgesehene Vererbung, wärend hinter dem context keyword permits alle Klassen aufgezählt werden die von der Klasse ableiten dürfen.

Was verhält es sich nun mit den beiden abgeleiteten Klassen? Java verlangt, daß für jede Klasse die von einer sealed Klasse abgeleitet wird festgelegt wird, wie von ihr abgeleitet werden kann:

final von der Klasse ist überhaupt keine Ableitung mehr möglich
non-sealed von der Klasse können wieder beliebige weitere Klassen abgeleitet werden
sealed wie bei der Super-Klasse werden alle ableitenden Klassen angegeben.

sealed interfaces

Analog zu Klassen können auch Interfaces abgeschottet werden. Hier bieten sich nun zwei Möglichkeiten der Erweiterung: Zum Einen können von einem Interface neue Interfaces abgeleitet werden, zum Anderen können Klassen das Interface implementieren. Es entstehen also zwei Varianten der Versiegelung.

Bei der Ableitung verhalten sich die Dinge genauso wie oben für Klassen beschrieben: Ist das Interface sealed, müssen bei der Definition alle abgeleiteten Interfaces angegeben werden:

public sealed interface Length permits Convex, NonConvex { ... }

Alle abgeleiteten Interfaces verhalten sich wieder wie Klassen, sie müssen entweder sealed oder non-sealed sein. Interfaces können -- weil das absurd wäre -- niemals final sein.

Implementiert eine Klasse ein sealed Interface, kommt es in den gleichen Zustand als wäre es von einer sealed Klasse abgeleitet. Es muß entweder als sealed, non-sealed oder final markiert sein.

enums

Da das klassische enum implizit -- zumindest fast immer -- final ist, kann es nicht sealed sein. Da es aber interfaces implementieren kann, kann es auch sealed interfaces implementieren. Da das enum implizit final ist, braucht es aber nicht weiter markiert zu werden.

In a nutshell: Ein enum kann ein sealed interface implementieren, als wäre es ein normales Interface.

Was sollte im vorangehenden Absatz aber der Einschub "fast immer"?

Betrachten wir dieses kleine -- zugegebenermaßen etwas unsinnige -- enum:

public enum Foo {
     BLA,
     FASEL {
            public int wert = 5;
            }
 }

Es soll nur eines verdeutlichen:

Das enum Foo ist implizit sealed und die einzige Klasse die davon ableitet ist die namenlose Klasse die den Typ des Objekts FASEL darstellt; diese namenlose Klasse ist imlizit final.

Vereinfacht ausgedrückt:

enums die sealed Interfaces implementieren sind implizit final, Objekte des enums können aber finale Sub-Klassen des enums bilden.

Mocking

Da sealed classes keine Ableitung erlauben, stellt sich die Frage ob man sie mocken kann. Mit Mockito ist das möglich ab Version 5.0.0, Version 4 kompiliert zwar, wirft bei der Ausführung aber eine Exception. Gleiches gilt für den Spy, der ebenfalls ab Version 5.0.0 erzeugt werden kann.

An dieser Stelle sei der Vollständigkeit halber darauf hingewiesen, daß Mockito auch final Klassen mocken und spyen kann -- aber auch erst mit Version 5.0.0

Fazit

Sealed Klasses und Interfaces sind für Java ein Konzept, das das Paradigma nicht gerade ändert, aber doch in eine ungewohnte Richtung biegt. Formal werden dadurch -- eigentlich unerwünschte -- zyklische Abhängigkeit zwischen Super- und Sub-Klasse geschaffen, weil plötzlich die Superklasse ihre Subklasses kennt. Inhaltlich aber macht die Superklasse ja davon gar keine Verwendung, es ist in Wirklichkeit eine Compiler-Anweisung.

Man muß die (teil)ge-sealte Hierarchie als ein Ganzes sehen, das einen Teil einer fachlichen Domäne abbildet. Weil die Domäne als geschlossen betrachtet wird, läßt sie auch keine unkontrollierte Erweiterung zu. Verwendet man diese Hierarchie nur, hat man keine Wahl und muß sie nehmen wie sie ist. Arbeitet man an der Hierarchie, weist einen die Versiegelung darauf hin, daß eine Erweiterung vom Konzept an dieser Stelle nicht vorgesehen ist. Eine Veränderung bedarf zunächst der fachlichen Klärung und die Versiegelung schützt uns davor, voreilige Änderungen durchzuführen.

Wenn eine Programmiersprache Möglichkeiten der Einschränkung schafft, ist das keine Behinderung des Coders. Es ist die Möglichkeit, Consraints der Domäne explizit zu machen -- und das ist ein wertvolles Hilfsmittel.