Best Practices aus "Effective Java (3rd Edition)"
Exzerpt aus „Effective Java (3rd Edition) “ von Joshua Bloch
1. Leere Collections oder Arrays zurückgeben, nicht null
Wenn die Methode ein Set zurückgibt, kein return null; bei leerem Result, sondern return Set.of();
2. Statische Factory-Methoden statt Konstruktoren in Betracht ziehen
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
Nicht immer besser, können aber in manchen Anwendungsfällen zahlreiche Vorteile bieten:
- Ein Vorteil statischer Factory-Methoden ist, dass sie im Gegensatz zu Konstruktoren einen Namen haben.
- Sie müssen nicht bei jedem Aufruf ein neues Objekt erzeugen.
- Sie können ein Objekt eines beliebigen Subtyps ihres Rückgabetyps zurückgeben.
- Die Klasse des zurückgegebenen Objekts kann je nach Eingabeparametern von Aufruf zu Aufruf variieren.
- Die Klasse des zurückgegebenen Objekts muss noch nicht existieren, wenn die Klasse mit der Methode geschrieben wird.
Nachteile:
- Die größte Einschränkung, wenn man ausschließlich statische Factory-Methoden anbietet: Von Klassen ohne public oder protected Konstruktoren lassen sich keine Unterklassen bilden.
- Ein zweiter Nachteil statischer Factory-Methoden ist, dass Programmierer sie nur schwer finden.
3. Bei vielen Konstruktorparametern einen Builder in Betracht ziehen
Statische Factories und Konstruktoren haben eine gemeinsame Schwäche: Sie skalieren schlecht, wenn es viele optionale Parameter gibt. Genau das hat einige Projekte, an denen ich gearbeitet habe, sehr hart getroffen.
4. Die Singleton-Eigenschaft mit einem privaten Konstruktor oder einem Enum-Typ erzwingen
Alles, was nicht mehrere Instanzen braucht, soll nur einmal instanziiert werden. Sei es mit @ApplicationScoped oder einem anderen Singleton-Pattern.
5. Nicht-Instanziierbarkeit mit einem privaten Konstruktor erzwingen
Gelegentlich willst du eine Klasse schreiben, die nur eine Sammlung statischer Methoden und statischer Felder ist. Solche Klassen haben einen schlechten Ruf, weil manche sie missbrauchen, um nicht in Objekten denken zu müssen – sie haben aber durchaus ihre berechtigten Einsatzzwecke.
private MyStaticClass() {
// private constructor to prevent instantiation
}
6. Unnötige Objekterzeugung vermeiden
String s = new String("bikini"); // DON'T DO THIS!
Oder:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
Auflösung: Dieses Programm liefert das richtige Ergebnis, ist aber viel langsamer als nötig – wegen eines Tippfehlers von einem einzigen Zeichen. Die Variable sum ist als Long statt als long deklariert, wodurch das Programm etwa 2^31 unnötige Long-Instanzen erzeugt (ungefähr eine für jedes Mal, wenn das long i zur Long-Variable sum addiert wird).
7. Finalizer und Cleaner vermeiden
Finalizer sind unberechenbar, oft gefährlich und in der Regel unnötig. Ihr Einsatz kann zu sprunghaftem Verhalten, schlechter Performance und Portabilitätsproblemen führen.
8. try-with-resources gegenüber try-finally bevorzugen
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
9. Die Implementierung von Comparable in Betracht ziehen
Wenn du Comparable implementierst, kann deine Klasse mit all den vielen generischen Algorithmen und Collection-Implementierungen zusammenarbeiten, die auf diesem Interface aufbauen.
10. Die Zugreifbarkeit von Klassen und Membern minimieren
Der wichtigste Faktor, der eine gut entworfene Komponente von einer schlecht entworfenen unterscheidet, ist das Ausmaß, in dem die Komponente ihre internen Daten und andere Implementierungsdetails vor anderen Komponenten verbirgt. Eine gut entworfene Komponente verbirgt sämtliche Implementierungsdetails und trennt ihre API sauber von ihrer Implementierung. Komponenten kommunizieren dann nur über ihre APIs und wissen nichts vom Innenleben der jeweils anderen. Dieses Konzept, bekannt als Information Hiding oder Kapselung, ist ein Grundprinzip des Softwaredesigns.
11. In öffentlichen Klassen Zugriffsmethoden statt öffentlicher Felder verwenden
Gelegentlich bist du vielleicht versucht, degenerierte Klassen zu schreiben, die keinen anderen Zweck haben, als Instanzfelder zu gruppieren:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
Da auf die Datenfelder solcher Klassen direkt zugegriffen wird, bieten sie nicht die Vorteile der Kapselung. Du kannst die Repräsentation nicht ändern, ohne die API zu ändern, du kannst keine Invarianten erzwingen und du kannst keine zusätzlichen Aktionen ausführen, wenn auf ein Feld zugegriffen wird. Für kompromisslose objektorientierte Programmierer sind solche Klassen ein rotes Tuch: Sie sollten immer durch Klassen mit privaten Feldern und öffentlichen Zugriffsmethoden (Gettern) und – bei veränderlichen Klassen – Mutatoren (Settern) ersetzt werden:
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
Bei öffentlichen Klassen haben die Hardliner sicherlich recht: Wenn eine Klasse außerhalb ihres Packages zugänglich ist, stelle Zugriffsmethoden bereit, damit du dir die Flexibilität bewahrst, die interne Repräsentation der Klasse zu ändern. Legt eine öffentliche Klasse ihre Datenfelder offen, ist jede Hoffnung verloren, ihre Repräsentation je zu ändern, weil Client-Code weit verstreut sein kann.
Ist eine Klasse dagegen package-private oder eine private verschachtelte Klasse, spricht grundsätzlich nichts dagegen, ihre Datenfelder offenzulegen – vorausgesetzt, sie beschreiben die Abstraktion, die die Klasse bietet, angemessen.
12. Veränderlichkeit minimieren
Eine unveränderliche (immutable) Klasse ist einfach eine Klasse, deren Instanzen nicht verändert werden können. Alle Informationen einer Instanz stehen für die gesamte Lebensdauer des Objekts fest, sodass sich nie eine Änderung beobachten lässt. Die Bibliotheken der Java-Plattform enthalten viele unveränderliche Klassen, darunter String, die Wrapper-Klassen der primitiven Typen sowie BigInteger und BigDecimal. Dafür gibt es viele gute Gründe: Unveränderliche Klassen lassen sich leichter entwerfen, implementieren und verwenden als veränderliche. Sie sind weniger fehleranfällig und sicherer. Um eine Klasse unveränderlich zu machen, befolge diese fünf Regeln:
- Keine Methoden anbieten, die den Zustand des Objekts verändern (sogenannte Mutatoren).
-
Sicherstellen, dass die Klasse nicht erweitert werden kann. Das verhindert, dass unvorsichtige oder böswillige Unterklassen das unveränderliche Verhalten der Klasse
untergraben, indem sie sich so verhalten, als hätte sich der Zustand des Objekts geändert. Unterklassen verhindert man in der Regel, indem man die Klasse zum Beispiel
finalmacht. - Alle Felder final machen. Damit drückst du deine Absicht klar aus, und das System setzt sie durch. Außerdem ist es nötig, um korrektes Verhalten sicherzustellen, wenn eine Referenz auf eine neu erzeugte Instanz ohne Synchronisierung von einem Thread an einen anderen übergeben wird, wie im Speichermodell festgelegt.
- Alle Felder private machen. Das verhindert, dass Clients Zugriff auf veränderliche Objekte erhalten, auf die Felder verweisen, und diese Objekte direkt verändern. Zwar dürfen unveränderliche Klassen technisch gesehen public final Felder mit primitiven Werten oder Referenzen auf unveränderliche Objekte haben, empfohlen wird das aber nicht, weil es ausschließt, die interne Repräsentation in einem späteren Release zu ändern.
-
Exklusiven Zugriff auf alle veränderlichen Komponenten sicherstellen. Hat deine Klasse Felder, die auf veränderliche Objekte verweisen, stelle sicher, dass Clients der Klasse
keine Referenzen auf diese Objekte erhalten können. Initialisiere ein solches Feld nie mit einer vom Client übergebenen Objektreferenz und gib das Feld nie aus einer Zugriffsmethode zurück.
Erstelle defensive Kopien in Konstruktoren, Zugriffsmethoden und
readObject-Methoden.
13. Komposition der Vererbung vorziehen
Vererbung ist ein mächtiges Mittel zur Wiederverwendung von Code, aber nicht immer das beste Werkzeug für die Aufgabe. Unsachgemäß eingesetzt, führt sie zu fragiler Software. Innerhalb eines Packages ist Vererbung sicher, wenn die Implementierungen von Unter- und Oberklasse unter der Kontrolle derselben Programmierer stehen. Ebenso sicher ist Vererbung, wenn du Klassen erweiterst, die speziell für die Erweiterung entworfen und dokumentiert wurden. Über Package-Grenzen hinweg von gewöhnlichen konkreten Klassen zu erben, ist dagegen gefährlich.
Anders als ein Methodenaufruf verletzt Vererbung die Kapselung. Du denkst vielleicht, es sei sicher, eine Klasse zu erweitern, solange du nur neue Methoden hinzufügst und keine bestehenden überschreibst. Diese Art der Erweiterung ist zwar deutlich sicherer, aber nicht ohne Risiko. Bekommt die Oberklasse in einem späteren Release eine neue Methode und hast du das Pech, der Unterklasse bereits eine Methode mit derselben Signatur und einem anderen Rückgabetyp gegeben zu haben, kompiliert deine Unterklasse nicht mehr. Hast du der Unterklasse eine Methode mit derselben Signatur und demselben Rückgabetyp wie die neue Methode der Oberklasse gegeben, überschreibst du diese jetzt – und hast damit die zuvor beschriebenen Probleme. Außerdem ist zweifelhaft, ob deine Methode den Vertrag der neuen Oberklassenmethode erfüllt, denn dieser Vertrag war noch gar nicht geschrieben, als du die Methode der Unterklasse geschrieben hast.
Zum Glück gibt es einen Weg, all diese Probleme zu vermeiden. Statt eine bestehende Klasse zu erweitern, gibst du deiner neuen Klasse ein privates Feld, das eine Instanz der bestehenden Klasse referenziert. Dieses Design heißt Komposition, weil die bestehende Klasse zu einer Komponente der neuen wird. Jede Instanzmethode der neuen Klasse ruft die entsprechende Methode auf der enthaltenen Instanz der bestehenden Klasse auf und gibt deren Ergebnis zurück. Das nennt man Weiterleitung (Forwarding), und die Methoden der neuen Klasse heißen Weiterleitungsmethoden. Die resultierende Klasse ist grundsolide und hängt nicht von den Implementierungsdetails der bestehenden Klasse ab. Selbst neue Methoden in der bestehenden Klasse wirken sich nicht auf die neue Klasse aus.
14. Interfaces gegenüber abstrakten Klassen bevorzugen
Java bietet zwei Mechanismen, um einen Typ zu definieren, der mehrere Implementierungen zulässt: Interfaces und abstrakte Klassen. Seit Java 8 Default-Methoden für Interfaces eingeführt hat, kannst du mit beiden Mechanismen Implementierungen für einige Instanzmethoden bereitstellen.
Ein wesentlicher Unterschied: Um den durch eine abstrakte Klasse definierten Typ zu implementieren, muss eine Klasse eine Unterklasse dieser abstrakten Klasse sein. Da Java nur Einfachvererbung erlaubt, schränkt das den Einsatz abstrakter Klassen als Typdefinitionen stark ein. Ein Interface dagegen darf jede Klasse implementieren, die alle erforderlichen Methoden definiert und den allgemeinen Vertrag einhält – egal, wo sie in der Klassenhierarchie steht. Bestehende Klassen lassen sich leicht nachrüsten, sodass sie ein neues Interface implementieren.
- Interfaces eignen sich ideal für die Definition von Mixins.
- Interfaces ermöglichen nicht-hierarchische Typ-Frameworks.
- Interfaces ermöglichen sichere, leistungsfähige Funktionserweiterungen über das Wrapper-Klassen-Idiom. Wenn du Typen mit abstrakten Klassen definierst, bleibt Programmierern, die Funktionalität hinzufügen wollen, keine andere Wahl als Vererbung.
15. Generics: Keine Raw Types verwenden
Du entdeckst den Fehler erst zur Laufzeit, lange nachdem er passiert ist, und womöglich in Code, der weit von dem fehlerhaften Code entfernt liegt. Sobald du die ClassCastException siehst, musst du die Codebasis nach dem Methodenaufruf durchsuchen, der die Münze in die Briefmarkensammlung gelegt hat. Der Compiler kann dir dabei nicht helfen.
16. Generics: Unchecked-Warnungen beseitigen
Wenn du mit Generics programmierst, wirst du viele Compiler-Warnungen sehen:
- Unchecked-Cast-Warnungen
- Unchecked-Warnungen bei Methodenaufrufen
- Unchecked-Warnungen bei parametrisierten Vararg-Typen
- Unchecked-Konvertierungswarnungen.
Je mehr Erfahrung du mit Generics sammelst, desto weniger Warnungen bekommst du – erwarte aber nicht, dass neu geschriebener Code ohne Warnungen kompiliert. Viele Unchecked-Warnungen lassen sich leicht beseitigen. Angenommen, du schreibst versehentlich diese Deklaration:
Set<Lark> exaltation = new HashSet();
Ändere sie zu:
Set<Lark> exaltation = new HashSet<>();
Manche Warnungen sind viel schwieriger zu beseitigen. Wenn du Warnungen bekommst, die etwas Nachdenken erfordern, bleib dran! Beseitige jede Unchecked-Warnung, die du beseitigen kannst.
Wenn du alle Warnungen beseitigst, kannst du sicher sein, dass dein Code typsicher ist – und das ist eine sehr gute Sache. Es bedeutet, dass du zur Laufzeit keine ClassCastException bekommst, und es
stärkt dein Vertrauen, dass sich dein Programm so verhält, wie du es beabsichtigt hast. Wenn du eine Warnung nicht beseitigen kannst, aber beweisen kannst, dass der Code, der sie auslöst,
typsicher ist, dann (und nur dann) unterdrücke die Warnung mit einer @SuppressWarnings("unchecked")-Annotation. Wenn du Warnungen unterdrückst, ohne vorher zu beweisen,
dass der Code typsicher ist, wiegst du dich in falscher Sicherheit.
Der Code kompiliert dann vielleicht ohne eine einzige Warnung, kann zur Laufzeit aber trotzdem eine ClassCastException werfen.
Jedes Mal, wenn du eine @SuppressWarnings("unchecked")-Annotation verwendest, füge einen Kommentar hinzu, der erklärt, warum das sicher ist.
17. Generics: Wo sie sinnvoll sind
Autsch – eine Klasse, die dringend Generics bräuchte:
So etwas sollte spätestens beim Merge Request abgefangen werden!
18. Generics: Wie man es nicht macht
Sorry, dazu braucht es keine Worte. Bitte merge das NIEMALS:
public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
// ...
}
Eine bloße Klassendeklaration sollte man nie erst 5 Minuten lang entschlüsseln müssen.
19. Listen gegenüber Arrays bevorzugen
Arrays unterscheiden sich in zwei wichtigen Punkten von generischen Typen. Erstens sind Arrays kovariant. Dieses furchteinflößend klingende Wort bedeutet einfach: Wenn Sub ein Subtyp
von Super ist, dann ist der Array-Typ Sub[] ein Subtyp des Array-Typs Super[]. Generics sind dagegen invariant: Für zwei beliebige verschiedene Typen
Type1 und Type2 ist List<Type1> weder ein Subtyp noch ein Supertyp von List<Type2>.
20. Lambdas gegenüber anonymen Klassen bevorzugen
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
Mach es stattdessen so:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
21. Den Gültigkeitsbereich lokaler Variablen minimieren
Die wirkungsvollste Technik, um den Gültigkeitsbereich einer lokalen Variable zu minimieren, besteht darin, sie dort zu deklarieren, wo sie zum ersten Mal verwendet wird …
22. For-each-Schleifen gegenüber klassischen for-Schleifen bevorzugen
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
23. Die Bibliotheken kennen und nutzen
Angenommen, du willst zufällige Ganzzahlen zwischen null und einer Obergrenze erzeugen. Vor diese alltägliche Aufgabe gestellt, würden viele Programmierer eine kleine Methode schreiben, die etwa so aussieht:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
Diese Methode sieht vielleicht gut aus, hat aber drei Schwächen. Erstens: Ist n eine kleine Zweierpotenz, wiederholt sich die Folge der Zufallszahlen schon nach relativ kurzer Zeit. Zweitens: Ist n keine Zweierpotenz, werden manche Zahlen im Durchschnitt häufiger zurückgegeben als andere. Ist n groß, kann dieser Effekt recht deutlich ausfallen.
Die dritte Schwäche der Methode random ist, dass sie in seltenen Fällen katastrophal versagen und eine Zahl außerhalb des vorgegebenen Bereichs zurückgeben kann. Das liegt daran, dass die
Methode versucht, den von rnd.nextInt() gelieferten Wert durch einen Aufruf von Math.abs auf einen nicht-negativen int abzubilden. Gibt nextInt()
Integer.MIN_VALUE zurück, liefert auch Math.abs Integer.MIN_VALUE, und der Restoperator (%) gibt eine negative Zahl zurück – vorausgesetzt,
n ist keine Zweierpotenz. Das lässt dein Programm mit ziemlicher Sicherheit scheitern, und der Fehler ist womöglich schwer zu reproduzieren.
24. float und double vermeiden, wenn exakte Ergebnisse gefragt sind
BigDecimal für Geldbeträge verwenden. float und double erzeugen Rundungsfehler.
Angenommen, du hast 1,03 Dollar in der Tasche und gibst 42 Cent aus. Wie viel Geld bleibt dir? Hier ein naives Programmfragment, das diese Frage beantworten will:
System.out.println(1.03 - 0.42);
Leider gibt es 0.6100000000000001 aus. Das ist kein Einzelfall. Angenommen, du hast einen Dollar in der Tasche und kaufst neun Unterlegscheiben zu je zehn Cent. Wie viel Wechselgeld
bekommst du zurück?
System.out.println(1.00 - 9 * 0.10);
Laut diesem Programmfragment bekommst du $0.09999999999999998.
Du denkst vielleicht, das Problem ließe sich einfach lösen, indem man die Ergebnisse vor der Ausgabe rundet – leider funktioniert das nicht immer. Angenommen, du hast einen Dollar in der Tasche und siehst ein Regal mit einer Reihe köstlicher Bonbons zu 10 Cent, 20 Cent, 30 Cent und so weiter bis zu einem Dollar. Du kaufst von jeder Sorte eines, beginnend mit dem für 10 Cent, bis du dir das nächste Bonbon im Regal nicht mehr leisten kannst.
25. Primitive Typen gegenüber geboxten primitiven Typen bevorzugen
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
26. Objekte über ihre Interfaces referenzieren
Gutes Beispiel:
Set<Son> sonSet = new LinkedHashSet<>();
Schlechtes Beispiel:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
27. Interfaces gegenüber Reflection bevorzugen
Andernfalls:
- Du verlierst alle Vorteile der Typprüfung zur Compile-Zeit, einschließlich der Prüfung von Exceptions. Versucht ein Programm, per Reflection eine nicht existierende oder nicht zugängliche Methode aufzurufen, scheitert es zur Laufzeit, sofern du keine besonderen Vorkehrungen getroffen hast.
- Der Code für reflektiven Zugriff ist umständlich und langatmig. Er ist mühsam zu schreiben und schwer zu lesen.
- Die Performance leidet. Reflektive Methodenaufrufe sind deutlich langsamer als normale Methodenaufrufe.
28. Allgemein anerkannte Namenskonventionen einhalten
Die Java-Plattform hat einen etablierten Satz von Namenskonventionen, von denen viele in „The Java Language Specification“ [JLS, 6.1] festgehalten sind.
29. Exceptions nur für Ausnahmesituationen verwenden
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
Was macht dieser Code? Auf den ersten Blick ist das überhaupt nicht ersichtlich – und das allein ist Grund genug, ihn nicht zu verwenden. Wie sich herausstellt, ist es ein furchtbar missratenes
Idiom, um über die Elemente eines Arrays zu iterieren. Die Endlosschleife endet, indem beim Zugriff auf das erste Element außerhalb der Array-Grenzen eine ArrayIndexOutOfBoundsException
geworfen, gefangen und ignoriert wird. Das Ganze soll dem Standard-Idiom zum Durchlaufen eines Arrays entsprechen, das jeder Java-Programmierer sofort erkennt:
for (Mountain m : range) {
m.climb();
}
Exceptions sind, wie ihr Name schon sagt, ausschließlich für Ausnahmesituationen gedacht; für den normalen Kontrollfluss sollten sie nie verwendet werden.
30. Checked Exceptions für behebbare Situationen und Runtime Exceptions für Programmierfehler verwenden
Verwende Checked Exceptions für Situationen, bei denen man vernünftigerweise erwarten kann, dass der Aufrufer sie behebt. Indem der API-Designer den Benutzer mit einer Checked Exception konfrontiert, verpflichtet er ihn, die Situation zu behandeln. Der Benutzer kann diese Verpflichtung missachten, indem er die Exception fängt und ignoriert – das ist aber meist eine schlechte Idee.
Verwende Runtime Exceptions, um Programmierfehler anzuzeigen. Die große Mehrheit der Runtime Exceptions zeigt verletzte Vorbedingungen an. Eine verletzte Vorbedingung bedeutet
einfach, dass sich der Client einer API nicht an den Vertrag hält, den die API-Spezifikation festlegt. Der Vertrag für Array-Zugriffe legt zum Beispiel fest, dass der Array-Index zwischen null und
der Array-Länge minus eins liegen muss, jeweils einschließlich. Eine ArrayIndexOutOfBoundsException zeigt an, dass diese Vorbedingung verletzt wurde.
31. Checked Exceptions nicht unnötig einsetzen
Zusammengefasst: Sparsam eingesetzt, können Checked Exceptions die Zuverlässigkeit von Programmen erhöhen; übermäßig eingesetzt, machen sie APIs mühsam in der Verwendung.
Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
LearningCardAssignment learningCardAssignment,
@NotNull Collection<AnswerOption> answers)
throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
ConsecutiveCorrectCountExceededException,
PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
// this is a mistake seen a lot with junior devs
// exceptions are exceptionally slow and should be avoided for mere validation
// the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions
32. Standard-Exceptions bevorzugen
Was Experten von weniger erfahrenen Programmierern unterscheidet: Experten streben ein hohes Maß an Code-Wiederverwendung an und erreichen es in der Regel auch.
Exceptions sind keine Ausnahme von der Regel, dass Code-Wiederverwendung eine gute Sache ist. Die Java-Bibliotheken bieten eine Reihe von Exceptions, die den Großteil dessen abdeckt, was die meisten APIs an Exceptions werfen müssen. Standard-Exceptions wiederzuverwenden hat mehrere Vorteile. Der wichtigste: Deine API wird leichter zu erlernen und zu verwenden, weil sie etablierten Konventionen folgt, die Programmierer bereits kennen. Knapp dahinter: Programme, die deine API verwenden, sind leichter zu lesen, weil sie nicht mit unbekannten Exceptions übersät sind. Zuletzt (und am wenigsten wichtig): Weniger Exception-Klassen bedeuten weniger Speicherbedarf und weniger Zeit für das Laden von Klassen.
Der am häufigsten wiederverwendete Exception-Typ ist IllegalArgumentException. Diese Exception wirfst du in der Regel, wenn der Aufrufer ein Argument mit einem unpassenden Wert übergibt –
etwa eine negative Zahl für einen Parameter, der angibt, wie oft eine Aktion wiederholt werden soll. Eine weitere häufig wiederverwendete Exception ist IllegalStateException. Sie wirfst
du in der Regel, wenn der Aufruf aufgrund des Zustands des empfangenden Objekts unzulässig ist – etwa wenn der Aufrufer ein Objekt verwenden will, bevor es ordnungsgemäß initialisiert wurde.
Man kann argumentieren, dass sich jeder fehlerhafte Methodenaufruf auf ein unzulässiges Argument oder einen unzulässigen Zustand zurückführen lässt, doch für bestimmte Arten unzulässiger Argumente
und Zustände werden standardmäßig andere Exceptions verwendet. Übergibt ein Aufrufer null in einem Parameter, für den null-Werte verboten sind, sollte laut Konvention eine
NullPointerException statt einer IllegalArgumentException geworfen werden.
Ebenso sollte eine IndexOutOfBoundsException statt einer IllegalArgumentException geworfen werden, wenn ein Aufrufer in einem Parameter, der einen Index in einer Sequenz
darstellt, einen Wert außerhalb des gültigen Bereichs übergibt.
Verwende Exception, RuntimeException, Throwable oder Error nicht direkt. Behandle diese Klassen so, als wären sie abstrakt. RuntimeException darfst du allerdings
verwenden, wenn du eine Checked Exception in eine Runtime Exception umwandeln willst – und wirklich weißt, was du tust.
33. Exceptions nicht ignorieren
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
Ein leerer catch-Block untergräbt den Zweck von Exceptions, nämlich dich zu zwingen, Ausnahmesituationen zu behandeln. Eine Exception zu ignorieren ist, als würdest du einen Feueralarm ignorieren – und ihn obendrein abschalten, damit niemand sonst nachsehen kann, ob es wirklich brennt.