- Ein Überblick über die Bedeutung von final in Java und wie man damit die Code-Qualität verbessern kann.
Das final
Keyword hat eine Reihe von Bedeutungen in Java und fristet leider zu oft ein „Außenseiterdasein“ bei vielen ProgrammiererInnen. Dabei ist es sehr nützlich, um Fehler zur Compile-Zeit auszuschließen, die sonst erst zur Laufzeit auffallen würden (also erst bei Tests oder schlimmer – beim User). Man kann sowohl viele NullPointerExceptions
als auch eine ganze Reihe von logischen Fehlern, die durch nicht berücksichtigte Programmpfade entstehen frühzeitig entdecken.
Außerdem forciert die umfassende Verwendung von final
einen sauberen Programmierstil, da mutable state, also sich über die Zeit verändernde Zustände von Variablen und Objekten minimiert wird. Dadurch wird es schwerer „Spaghetticode“ zu schreiben.
final als Modifier für Klassen
Diese Verwendung soll hier nur der Vollständigkeit halber aufgeführt werden. Deklariert man eine Klasse final
, kann von ihr nicht mehr abgeleitet werden. In 14 Jahren Java Entwicklung habe ich hierfür noch keinen wirklich nützlichen Anwendungsfall erlebt. Nützlich nur für aussagelose Einstellungs-Quizfragen.
public final class ICannotBeExtended {
//...
}
public class UnwantedChild extends ICannotBeExtended {
// won't compile
}
final als Modifier für Member
Nun aber zu den wirklich nützlichen Anwendungsfällen für final
. Deklariere ich einen Member als final
, kann sich sein Wert nach der Zuweisung nicht mehr ändern. Meistens findet man dies aber leider nur, um Konstanten zu definieren:
public static final int MAGIC_NUMBER = 42;
Man kann aber auch nicht-statische member als final deklarieren. Auch muss man nicht bei der Deklaration sofort einen Wert angeben:
private final int importantId;
Was hat man dadurch gewonnen? Nun, der Compiler passt von nun an für uns auf, dass in jedem Constructor wirklich alle final
Member gesetzt werden. So kann es nicht passieren, dass jemand (meistens man selbst) zu einem späteren Zeitpunkt einen Konstruktor hinzufügt, der vergisst, einen wichtigen Wert zu setzen:
public class MyImportantClass {
private final int importantId;
private final String foo;
private final String bar;
/** Proper Constructor that leaves the constructed object
* in a valid state.
*/
public MyImportantClass (final int importantId, final String foo, final String bar) {
this.importantId = importantId;
this.foo = foo;
this.bar = bar;
}
/**
* I read somewhere that every Java class needs an
* empty constructor because Beans are a pattern. DERP! */
public MyImportantClass() {
//won't compile, the day is saved!
}
}
Man kann also bei der Deklaration der member sicher stellen, dass ein für das korrekte Funktionieren des Objekts notwendiger Wert einfach unter den Tisch gekehrt wird.
final als Modifier für Funktionsparameter und lokale Variablen
Benutzt man final
als Modifier für Funktionsparameter und lokale Variablen, ist der Effekt ähnlich wie bei Membern: Der Compiler achtet nun für uns darauf, dass eine Variable nicht benutzt wird, ohne dass sie initialisiert wurde.
final String foo;
if (magicBool) {
foo = getVitalString();
}
if (foo.length > 5) {
// won't compile, foo might not be initialized
}
Bei so einem trivialen Beispiel mag das noch nicht nützlich sein, wird der Workflow aber komplizierter, kann final
einem viele Stunden nerviger Fehlersuche ersparen. Zwar helfen einem die NullPointer-Warnings moderner IDEs heutzutage auch oft gegen diesen Fall. Nur müssen die auch aktiviert sein und gelesen werden. Nicht jede(r) EntwicklerIn sorgt dafür, dass der Code warning-free ist. Oft geht die wichtige NPE-Warnung in der Kakophonie der Warnings still und leise unter. Das folgende ist leider ein häufiges Idiom:
List foo;
// or worse: List foo = null;
if (someValue > 10) {
foo = new ArrayList<>();
} else if (someValue < 10) {
foo = new LinkedList<>();
}
// will happily compile & crash your App if someValue == 10
foo.size();
Setzt man nun foo auf final
, ist man gezwungen, foo für alle Wege durch den Code zu initialisieren.
final List foo;
if (someValue > 10) {
foo = new ArrayList<>();
} else if (someValue < 10) {
foo = new LinkedList<>();
}
// will not compile, you need an else clause
foo.size();
Zwar kann man nun foo auch im else
– Zweig auf null
initialisieren, aber dann grenzt es schon an mutwillige Zerstörung.
Vermeiden von NullPointerExceptions mit final
Mit zwei Änderungen des Programmierstils kann man nun einen sehr großen Teil der möglichen NullPointerException
s in seinem Code vermeiden:
- gewohnheitsmäßig erstmal alles
final
deklarieren, nur weglassen wenn es wirklich notwendig ist - Wenn irgend möglich nie
null
zuweisen oder zurückgeben
Der Compiler checkt für uns, dass ein Wert auch wirklich zugewiesen wurde, bevor wir ihn benutzen. Dadurch kann ein Wert nicht einfach null
sein, weil er einfach vergessen wurde. Z.B.:
public class FooWithList {
private List importantList;
//oops, null by default
private int magic;
//sloppy Constructor
public FooWithList() {
magic = 13;
//forgot list...
}
public void importantInitializer() {
importantList = new ArrayList();
}
//goes boom if initializer is not called.
//we have now leaked implementation details, as this
//object will only work if certain methods are called
//in the correct order - we are at the mercy of the caller
public String foobar() {
return magic + importantList.length;
}
}
Es „schleichen“ sich also keine null
-Werte in den Code, über die man später stolpert. Wenn wir uns nun noch angewöhnen, nicht einfach überall null
zurück zu geben, wenn wir nichts besseres haben, hat man NPEs schon dadurch in großem Maße eingeschränkt, dass es einfach (fast) keine null
s mehr gibt, über die wir stolpern können:
public List readResultsBad() {
//sadly, this kind of code is very common:
if (resultCount > 0) {
return null;
}
//let's initialize inefficently without resultCount for good measure:
List results = new ArrayList();
for (int n = 0; n < resultCount; n++) {
results.add(nextResult());
}
return results;
}
public List readResultsGood() {
final List results = new ArrayList(resultCount);
for (int n = 0; n < resultCount; n++) {
results.add(nextResult());
}
return result;
}
Statt null
sollte man also wenn möglich einen Wert zurückgeben, der ein sinnvolles default-Verhalten erlaubt. Also leere Liste statt null
, leerer String statt null
etc. Bei komplexeren Objekten kann man z.B. ein „Null-Objekt“ als Klassen-Member hinterlegen, der dafür sorgt, dass Eingabemasken automagisch leer vorausgefüllt werden o.Ä.
Zum Beispiel:
public class User {
//user without first or last name and id -1
public static final NULL_USER = new User(-1, "", "");
}
In vielen Fällen kann man sich in seinem Code durch Null-Objekte viele Sicherheitsabfragen sparen, da der Code auch mit dem Null-Objekt noch sinnvoll funktioniert. Muss man wissen, ob man einen „richtigen“ Wert hat, kann man immer noch bequem testen:
if (results.isEmpty()) {
displayWarning("No results found.");
}
// we don't need equals, as there is only one NULL_USER instance
//(beware serialization, though)
if (user == User.NULL_USER) {
throw NoSuchUserException();
}
if ("".equals(resultString)) {
//etc. pp.
}
final in Eclipse automagisch hinzufügen
Mit diesem nützlichen Eclipse Feature kann man sich selber zu mehr „final-Disziplin“ zwingen: Die folgende Checkbox aktivieren: Window → Preferences → Java → Editor → Save Actions → Additional Actions Über Configure… die folgenden Aktionen hinzufügen:
- Add final modifier to private fields
- Add final modifier to method parameters
- Add final modifier to local variables
Nun wird bei jedem Speichern alles final
gemacht, was geht. Anfangs wird man es etwas störend finden, aber langfristig sorgt es dafür, das viele unsaubere Programmier-Idiome reduziert werden.
Wann final keinen Sinn macht
In einigen wenigen Fällen kommt man um non-final (also veränderliche Werte) leider doch nicht herum: Wenn man etwas zählen will:
while(...) {
loopCount++;
}
Initialisierung von Objekten in einem try/catch-Block:
final Foo foo;
try {
foo = new Foo(id);
} catch (final Throwable t){
throw new NoSuchFooException();
} //won't compile, Compiler is not smart enough to see
//that foo is always initialized here
foo.doBar();
Leider ist der Compiler im obigen Beispiel nicht in der Lage zu erkennen, das foo an der Stelle, an der es verwendet wurde, in jedem Fall initialisiert wurde.
Fazit
Man sollte final
defaultmäßig für alle Member, Parameter und lokale Variablen benutzen, da hierdurch viele Programmierfehler im Vorfeld vermieden werden und man dadurch angehalten wird, Klassen immutable zu implementieren, was wiederum ebenfalls wieder gut für die Qualität ist. Es ist eigentlich schade, dass das final – Verhalten in Java nicht das Standardverhalten ist und es stattdessen ein Keyword für „nicht-final“ gibt – z.B. mutable
.