Top 10 der nützlichen, aber paranoiden Java-Programmiertechniken

(c) Shutterstock.com / HomeArt
Unter dem Stichwort „defensives Programmieren“ hat Lukas Eder von jOOQ seine persönlichen Angewohnheiten nach 20 Jahren Programmierarbeit zusammengefasst. Bestimmt ist er nicht der Einzige, der hin und wieder paranoid wird, oder?
Nachdem man eine Weile gecodet hat (oh, in meinen Fall sind das schon fast 20 Jahre; wenn man Spaß hat, vergeht die Zeit wie im Flug), fängt man an, gewisse Angewohnheiten wertzuschätzen. Weil, Sie wissen schon…
Aus diesem Grund betreiben die Leute so gerne „defensives Programmieren“, haben also paranoide Angewohnheiten, die manchmal Sinn machen, manchmal aber eher obskur und/oder raffiniert sind und die, wenn man an die Person denkt, die den Code geschrieben hat, vielleicht ein bisschen unheimlich wirken. Hier ist meine persönliche Top 10 der nützlichen, aber paranoiden Java-Programmiertechniken. Los gehts:
1. Den String literal nach vorne stellen
Es ist nie eine schlechte Idee, der gelegentlichen NullPointerException
zuvorzukommen, indem man die String
literal auf die linke Seite eines equals()
-Vergleichs stellt:
// Bad if (variable.equals("literal")) { ... } // Good if ("literal".equals(variable)) { ... }
Darüber muss man nicht groß nachdenken. Durch Umformulierung des Ausdrucks von der weniger guten zur besseren Version entsteht keinerlei Nachteil. Hätten wir nur richtige Options, stimmt’s? Aber das ist eine andere Diskussion…
2. Nicht den frühen JDK APIs vertrauen
In den Anfangstagen von Java muss Programmieren äußerst schmerzvoll gewesen sein. Die APIs waren noch sehr unausgereift und man ist möglicherweise einem solchen Code-Fragment begegnet:
String[] files = file.list(); // Watch out if (files != null) { for (int i = 0; i < files.length; i++) { ... } }
Wirkt paranoid? Vielleicht, aber lesen Sie das Javadoc:
If this abstract pathname does not denote a directory, then this method returns null. Otherwise an array of strings is returned, one for each file or directory in the directory.
Ja, sicher. Besser noch einmal einen Check einbauen, nur um sicher zu gehen:
if (file.isDirectory()) { String[] files = file.list(); // Watch out if (files != null) { for (int i = 0; i < files.length; i++) { ... } } }
Schade! Verstoß gegen Regel #5 und #6 unserer Liste der zehn raffiniertesten Best Practices beim Java-Coding. Seien Sie also vorbereitet und fügen den null
-Check hinzu!
3. Vertrauen Sie der „-1“ nicht
Das ist paranoid, ich weiß. Das Javadoc zu String.indexOf()
sagt ganz klar, dass…
the index of the first occurrence of the character in the character sequence represented by this object [is returned], or -1 if the character does not occur.
Also kann man sich auf -1
verlassen, richtig? Ich sage nein. Man bedenke Folgendes:
// Bad if (string.indexOf(character) != -1) { ... } // Good if (string.indexOf(character) >= 0) { ... }
Wer weiß, vielleicht wird irgendwann eine WEITERE Codierung gebraucht, um zu sagen, dass der otherString
enthalten gewesen wäre, wenn auf Groß- und Kleinschreibung geachtet worden wäre. Vielleicht ist das ein guter Fall, um -2
zurückzugeben? Wer weiß.
Immerhin hatten wir tausende Diskussionen über den 1-Millionen-Dollar-Fehler namens NULL
. Warum sollten wir nicht anfangen über -1
zu diskutieren, das auf eine Weise ja eine alternatives null
für den primitive type int
ist?
4. Eine versehentliche Zuweisung verhindern
Jap, das passiert selbst den Besten (obwohl, nicht mir – siehe #7).
(Nehmen Sie an, es handelt sich im folgenden Code Snippet um JavaScript, aber lassen Sie uns auch paranoid bezüglich der Sprache sein)
// Ooops if (variable = 5) { ... } // Better (because causes an error) if (5 = variable) { ... } // Intent (remember. Paranoid JavaScript: ===) if (5 === variable) { ... }
Nochmal: wenn Sie ein Literal in Ihrem Ausdruck haben, stellen Sie es auf die linke Seite. Man kann dann nicht aus Versehen einen Fehler produzieren, wenn man eigentlich ein weiteres Gleichzeichen hinzufügen wollte.
5. Überprüfen Sie null UND length
Wenn immer Sie eine Collection, ein Array, etc. haben, stellen Sie sicher, dass es vorhanden ist UND auch nicht leer ist.
// Bad if (array.length > 0) { ... } // Good if (array != null && array.length > 0) { ... }
Man weiß nie, wo diese Arrays herkommen. Vielleicht von einem frühen JDK API?
6. Alle Methoden sind final
Sie können mir viel über Ihre open/closed-Prinzipien erzählen, das ist alles Quatsch. Ich vertraue weder Ihnen (meine Klassen korrekt zu erweitern), noch vertraue ich mir selbst (nicht versehentlich meine Klassen zu erweitern). Darum ist alles, was nicht explizit fürs subtyping bestimmt ist (d.h. nur Interfaces), strikt final
. Vergleichen Sie hier Punkt 9 in unserer Liste der zehn raffiniertesten Best Practices beim Java-Coding.
// Bad public void boom() { ... } // Good. Don't touch. public final void dontTouch() { ... }
Ja, das ist final. Wenn das nicht für Sie funktioniert, patchen Sie es, instrumentalisieren Sie es oder schreiben Sie den Bytecode neu. Oder senden Sie einen feature request. Ich bin sicher, dass Ihr Vorhaben, das Obenstehende zu überschreiben, sowieso keine gute Idee ist.
7. Alle Variablen und Parameter sind final
Wie schon gesagt: ich vertraue mir nicht (nicht aus Versehen meine Werte zu überschreiben). Überhaupt vertraue ich mir nicht, weil…
Darum werden alle Variablen und Parameter ebenfalls auf final
gesetzt.
// Bad void input(String importantMessage) { String answer = "..."; answer = importantMessage = "LOL accident"; } // Good final void input(final String importantMessage) { final String answer = "..."; }
Ok, ich geb’s zu. Diese Regel halte ich nicht besonders oft ein, obwohl ich es sollte. Wäre Java nur etwas mehr wie Scala, wo man einfach überall val
hinschreiben kann, ohne sich über mutability Gedanken machen zu müssen – außer, wenn man es ausdrücklich benötigt (was selten ist), dann via var
.
8. Bei Overloading nicht auf generics vertrauen
Ja, das kann passieren. Gerade glauben Sie noch, dieses coole API geschrieben zu haben, das total genial und super intuitiv ist, und schon kommt irgendein User vorbei, der alles zu Object
castet, bis der lästige Kompilierer aufhört zu meckern. Und auf einmal verlinken sie die falsche Methode und denken, es sei Ihre Schuld (es ist immer Ihre Schuld).
Schauen Sie mal hier:
// Bad <T> void bad(T value) { bad(Collections.singletonList(value)); } <T> void bad(List<T> values) { ... } // Good final <T> void good(final T value) { if (value instanceof List) good((List<?>) value); else good(Collections.singletonList(value)); } final <T> void good(final List<T> values) { ... }
Denn, Sie wissen schon… Ihre User sind so:
// This library sucks @SuppressWarnings("all") Object t = (Object) (List) Arrays.asList("abc"); bad(t);
Vertrauen Sie mir, ich hab schon alles gesehen. Inklusive solcher Geschichten:
Es ist gut, paranoid zu sein.
9. Werfen Sie immer switch default an
Switch… eines dieser lustigen Statements, bei denen ich nicht weiß, ob ich in Ehrfurcht erstarren oder einfach heulen soll. Wie auch immer, wir sitzen mit switch
fest, also können wir es auch gleich richtig machen. So z.B.:
// Bad switch (value) { case 1: foo(); break; case 2: bar(); break; } // Good switch (value) { case 1: foo(); break; case 2: bar(); break; default: throw new ThreadDeath("That'll teach them"); }
Spätestens, wenn value==3
in die Software eingeführt wird, kommt es sowieso! Und kommen Sie jetzt nicht mit enum
, denn mit enum
passiert das Gleiche.
10. Switch mit geschweifter Klammer
Tatsächlich ist switch
das wahnwitzigste Statement, das jemals in einer Programmiersprache zugelassen wurde. Irgendjemand muss wohl betrunken gewesen sein oder eine Wette verloren haben. Man sehe sich nur das folgende Beispiel an:
// Bad, doesn't compile switch (value) { case 1: int j = 1; break; case 2: int j = 2; break; } // Good switch (value) { case 1: { final int j = 1; break; } case 2: { final int j = 2; break; } // Remember: default: throw new ThreadDeath("That'll teach them"); }
Innerhalb des switch
-Statements ist lediglich ein Scope unter allen case
-Statements definiert. Eigentlich sind diese case
-Statements gar keine wirklichen Statements, sondern verhalten sich wie Labels und der switch ist ein goto Aufruf. Man könnte case
-Statements sogar mit dem beeindruckenden FORTRAN 77 ENTRY-Statement vergleichen, einem Instrument, dessen Mysterium nur von seiner Macht übertroffen wird.
Das bedeutet, dass die Varialbe final int j
für alle verschiedenen cases definiert ist, ganz gleich ob wir ein break
ausgeben oder nicht. Nicht besonders intuitiv. Darum ist es immer eine gute Idee, über einen einfachen Block einen neuen, geschachtelten Scope per case
-Statement zu erzeugen (aber vergessen Sie nicht den break
innerhalb des Blocks)
Fazit
Paranoides Programmieren mag manchmal etwas merkwürdig erscheinen, da der Code oft etwas ausladender ausfällt als wirklich notwendig. Man denkt vielleicht, „ach, das passiert sowieso nicht“. Aber wie gesagt: Nach 20 Jahren Programmieren will man nicht mehr diese dummen kleinen Bugs reparieren, die nur existieren, weil die Sprache so alt und mangelhaft ist. Denn Sie wissen ja…
Was ist Ihre paranoideste Programmier-Marotte?
Aufmacherbild: Bearded funny man in a cap of aluminum foil sends signals von Shutterstock
Urheberrecht: HomeArt
+1 bis auf die final methods. Da wirds nämlich bei allen frameworks die proxying verwenden fürchterbar krachen…
LieGrue,
strub
@struberg: Ein Proxy auf eine konkrete Klasse? Was ist der Use-case davon?