Sind Sie paranoid? Wahrscheinlich schon...

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

Lukas Eder

(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…

Alles, was schiefgehen kann, wird auch schiefgehen.

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:

Eder_1

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

Geschrieben von
Lukas Eder
Lukas Eder
Lukas Eder ist leidenschaftlicher Java- und SQL-Entwickler. Er ist Gründer und Leiter der Forschungs- und Entwicklungsabteilung der Data Geekery GmbH, dem Unternehmen hinter jOOQ - die einfachste Art, um SQL in Java zu schreiben.
Kommentare

Hinterlasse einen Kommentar

2 Kommentare auf "Top 10 der nützlichen, aber paranoiden Java-Programmiertechniken"

avatar
400
  Subscribe  
Benachrichtige mich zu:
struberg
Gast

+1 bis auf die final methods. Da wirds nämlich bei allen frameworks die proxying verwenden fürchterbar krachen…

LieGrue,
strub

Lukas Eder
Gast

@struberg: Ein Proxy auf eine konkrete Klasse? Was ist der Use-case davon?