Schlüsselworte final und sealed
Schlüsselwort final
Das Schlüsselwort final
hat mehrere Bedeutungen
- Eine finale Klasse kann keine Subklassen haben
- Eine finale Methode kann nicht überschrieben werden
- Eine finale Variable kann nicht mehr verändert werden
Finale Methoden
public class A {
public void methode() {}
public final void finalMethode() {}
}
public class B extends A {
@Override
public void methode() {}
@Override
public void finalMethode() {} // FEHLER!!
}
Finale Klasse
public final class A {
}
public class B extends A { // FEHLER!!
}
Finale Variablen
- Finale Variablen können nicht mehr verändert werden
- Finale Variablen primitiver Typen sind echte Konstanten
- Finale Variablen unveränderlicher Typen verhalten sich ähnlich wie Konstanten
- Finale Variablen veränderlicher Typen schützen das Objekt nicht vor Änderungen (sollte vermieden werden)
- Die Zuweisung an eine final Variable muss nicht in der Deklaration erfolgen (blank final)
- Der Compiler stellt sicher, dass die Variable auf allen möglichen Pfaden vor der ersten Verwendung genau einmal gesetzt wird
Eine finale Variable kann nachträglich nicht mehr geändert werden, wohl aber das Objekt auf das sie zeigt. final
sorgt nur dafür, dass die Variable nicht mehr auf der linken Seite einer Zuweisung auftauchen kann.
Somit verhält sich final anders als das const
in C++, bei dem man tatsächlich durch konsequentes Durchziehen des const
-Modifiers durch alle Methoden, Parameter, Variablen und Klassen dafür sorgen kann, dass als const
deklarierte Objekte nicht mehr verändert werden können. In Java hat man von diesem Ansatz wegen seiner Komplexität Abstand genommen. Die Folge ist aber, dass es keinen wirklichen Schutz vor unbeabsichtigter Veränderung von Objekten gibt, den man bei der Verwendung noch hinzufügen kann. Entweder ein Objekt ist von einem unveränderlichen Typ oder nicht.
public class Konstanten {
// echte Konstante
public static final double PI = 3.14159265;
// echte Konstante
public static final double NA = 6.0221415E23;
// String ist immutable, verhält sich wie String-Konstante
public static final String XML_HEADER =
"<?xml version=\"1.0\"?>";
// nur die Variable, nicht aber das Array sind geschützt
public static final int[] PRIMZAHLEN = { 2, 3, 5, 7, 11, 13 };
}
In diesem Beispiel sind PI
und NA
echte Konstanten, weil sie von einem primitivem Typ sind. XML_HEADER
verhält sich wie eine Konstante, weil das String-Objekt unveränderlich ist.
Die Variable PRIMZAHLEN
ist zwar als final
deklariert, dies bezieht sich aber nur auf die Variable selbst, nicht aber auf das dahinterliegende Array. Insofern kann man die Daten noch verändern, wie das folgende Beispiel zeigt.
public class Konstanten {
public static final int[] PRIMZAHLEN =
{ 2, 3, 5, 7, 11, 13, 17, 19 };
}
public class Konstantenverwender {
public static void main(String[] args) {
System.out.println(Arrays.toString(Konstanten.PRIMZAHLEN));
Konstanten.PRIMZAHLEN[2] = 8;
System.out.println(Arrays.toString(Konstanten.PRIMZAHLEN));
}
}
[2, 3, 5, 7, 11, 13, 17, 19]
[2, 3, 8, 7, 11, 13, 17, 19]
Das Beispiel zeigt, dass es sinnlos ist, final
im Zusammenhang mit veränderlichen (mutable) Typen zu verwenden. Obwohl hier die Variable PRIMZAHLEN
als final
deklariert ist, kann man das Array auf das sie verweist problemlos modifizieren. In diesen Fällen sollte man das final
am besten ganz weglassen, um keine falsche Sicherheit zu suggerieren.
Wenn man Listen von Objekten benötigt, die unveränderlich sind, bietet das Collection-API entsprechende Möglichkeiten, die später noch beleuchtet werden. Arrays sind hier auf jeden Fall ungeeignet.
Beispiel: Blank final
public class Kunde {
private final int kundenNummer;
public Kunde(int kundenNummer) {
this.kundenNummer = kundenNummer;
}
}
Das Beispiel zeigt, dass man das Setzen einer finalen Variablen durchaus herauszögern kann. Wichtig ist hier, dass die Variable auf jeden Fall genau einmal zugewiesen wird und danach nicht mehr. Der Compiler stellt dies sicher, indem er alle Ausführungspfade untersucht. Da hier die Variable kundenNummer
im Konstruktor gesetzt wird, darf man sie als final
deklarieren. Nach der Initialisierung erfolgt dann keine Veränderung mehr.
Finale Parameter und lokale Variablen
public void methode1(final int k) {
k++; // FEHLER!!
}
public void methode2() {
final boolean b = true;
final String s;
if (b) {
s = "true";
}
else {
s = "false";
}
s.toUpperCase();
b = false; // FEHLER!!
}
Da es ohnehin als ausgesprochen schlechter Stil gilt, Methodenparameter innerhalb der Methode zu verändern, machen viele Programmierer:innen sie direkt bei der Deklaration final
. Im vorliegenden Beispiel ist der Parameter k
der Methode methode1
so deklariert. Der Versuch, die Variable innerhalb der Methode mit k++
zu ändern führt zu einem Compile-Fehler.
Der Grund für die Regel, dass man Parameter nicht verändern sollte, liegt darin, dass es das Debugging erschwert und den Quelltext schwerer lesbar macht. Wenn man eine Methode betrachtet, die mit einem bestimmten Parameterwert aufgerufen wurde, rechnet man nicht damit, dass dieser Parameter mitten in der Methode plötzlich einen anderen Wert hat.
In methode2
wird eine normale finale Variable (b
) und ein blank final s
verwendet. s
wird dann, in Abhängigkeit von b
, mit einem bestimmten Wert initialisiert. Auch hier überprüft der Compiler, ob bei allen Möglichen Pfaden durch das Programm die Variable genau einmal geschrieben wird.
Der Aufruf s.toUpperCase()
ist unkritisch, weil er den Inhalt des Strings nicht verändert, sondern einen neuen String zurückgibt. Der Versuch, b
noch einmal zuzuweisen schlägt dann aber mit einem Compiler-Fehler fehl.
Einkompilieren von Konstanten
Überraschende Optimierung
- Konstanten, d. h.
static
final
Variablen eines primitiven Typs, werden direkt in den Verwender einkompiliert, über Klassen und Paketgrenzen hinweg - Danach gibt es keine Referenz zwischen dem Anbieter der Konstanten und dem Verwender mehr
- Ändert man die Konstante, so muss man zwingend die Verwender neu kompilieren
Dead Code Elimination
public class NoDeadCodeElemination {
public static boolean SCHALTER = false;
public static void main(String[] args) {
if (SCHALTER) {
System.out.println("Schalter AN");
}
else {
System.out.println("Schalter AUS");
}
}
}
In diesem Beispiel ist die Variable SCHALTER
nicht als static final
deklariert und damit keine Konstante. Betrachtet man den vom Compiler erzeugten Bytecode (mithilfe von javap
), so sieht man in der folgenden Ausgabe, dass die Bedingung noch vorhanden ist (Zeile 3).
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=1, Args_size=1
0: getstatic #10; //Field schalter:Z
3: ifeq 17
6: getstatic #21; //Field java/lang/System.out
9: ldc #27; //String Schalter AN
11: invokevirtual #29; //Method java/io/PrintStream.println
14: goto 25
17: getstatic #21; //Field java/lang/System.out
20: ldc #35; //String Schalter AUS
22: invokevirtual #29; //Method java/io/PrintStream.println
25: return
Das folgende Beispiel zeigt den Fall, dass die Variable SCHALTER
eine echte Konstante ist.
public class DeadCodeElemination {
public static final boolean SCHALTER = false;
public static void main(String[] args) {
if (SCHALTER) {
System.out.println("Schalter AN");
}
else {
System.out.println("Schalter AUS");
}
}
}
In diesem Fall eliminiert der Compiler den unnötigen und unerreichbaren Code und generiert ein Ergebnis ohne Bedingung. Unerreichbare Code wird auch als dead code bezeichnet und der hier vorgestellte Mechanismus daher als dead code elimination.
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=1, Args_size=1
0: getstatic #20; //Field java/lang/System.out
3: ldc #26; //String Schalter AUS
5: invokevirtual #28; //Method java/io/PrintStream.println
8: return
Sealed Classes
Bisher bekannte Möglichkeiten und Beschränkungen von Klassen
Art | Instanzen | Subklassen | überschreiben |
---|---|---|---|
normale Klasse | beliebig | ja | ja |
Klasse mit privatem Konstruktor | fest | nein | nein |
abstrakte Klasse | beliebig | muss | ja |
Klasse mit finaler Methode | beliebig | ja | ja, bis auf finale Methode |
finale Klasse | beliebig | nein | nein |
Enumeration | fest | nein | ja |
Enumerationen werden erst später behandelt.
Man sieht an der Auflistung, dass die bisher bekannten Sprachmittel der Entwicklerin einer Klasse nur erlauben, Subklassen ganz zu verbieten oder eben jeder Klassen zu erlauben, Subklassen zu bilden. Dies ist für komplexe Softwareprojekte keine gute Lösung. Deswegen wurde mit Java 17 ein neuer Mechanismus eingeführt, mit dem eine Klasse selbst bestimmen kann, wer von ihr erben darf.
Java 17 führt Sealed Classes ein
- Syntax:
public sealed class <NAME> permits <CLASS>, <CLASS>, .. {
- Superklasse kann festlegen, welche Klassen von ihr erben
- Subklassen
- müssen direkt von der Sealed Class erben
- müssen im selben Modul liegen
- müssen zur Compile-Zeit sichtbar sein
- Interfaces können auch versiegelt werden
Sealed Classes wurden zwar bereits mit Java 15 testweise eingeführt (Preview Feature), sind aber erst seit Java 17 stabil und final in den Java-Standard aufgenommen.
Ziele
- Dem Autor einer Klasse oder Schnittstelle Kontrolle darüber zu geben, welcher Code für die Implementierung verantwortlich ist.
- Bereitstellung eines Verfahrens, die Verwendung einer Oberklasse einzuschränken.
- Unterstützung für zukünftige Erweiterungen im Pattern Matching.
Ein Sealed Class in Java beschränkt die Möglichkeiten, wie eine Klasse erweitert werden kann. Eine solche Klasse kann nur von einer festgelegten Liste an Klassen erweitert werden, was sicherstellt, dass nur erwartete Typen verwendet werden. Dies ist nützlich, wenn man sicherstellen möchte, dass nur eine bestimmte Anzahl an Klassen einer bestimmten Hierarchie angehören.
Eine Klasse wird als sealed deklariert und eine Liste mit erlaubten Unterklassen wird angegeben.
Diese Klasse deklariert sich als Sealed Class und gibt an, welche Klassen von ihr erben dürfen: Circle
, Square
und Rectangle
.
public sealed class Shape
permits Circle, Square, Rectangle {
}
Subklassen einer Sealed Class haben drei Möglichkeiten
- sind
final
: Ende der Vererbungshierarchie - sind
sealed
: Erlauben bestimmte Subklassen - sind
non-sealed
: Öffnen sich wieder für beliebige Subklassen
public final class Circle extends Shape {
public float radius;
}
public sealed class Rectangle extends Shape permits FilledRectangle {
public double length, width;
}
public non-sealed class Square extends Shape {
public double side;
}
Die Klassen Circle
, Square
und Rectangle
zeigen die drei Möglichkeiten für Subklassen einer Sealed Class:
Circle
istfinal
, sodass keine anderen Klassen vonCircle
erben können.Rectangle
istsealed
erlaubt ausschließlich der KlasseFilledRectangle
von ihr zu erben.Square
deklariert sich alsnon-sealed
, sodass beliebige weitere Klassen von ihr erben können.