FindBugs für Java

Ich will heute ein Java-Tool für statische Code-Analyse vorstellen.

Der Java-Compiler findet neben Syntaxfehlern auch teilweise offensichtliche Laufzeitfehler im Voraus. Beispiel:

String str;
str.isEmpty(); // Compile-Error

Klar, der String ist nicht initialisiert und würde zu einer NullPointerException führen.

Es gibt Code-Konstrukte, die der Compiler nicht anmeckert, weil der Code selbst legal und auf den ersten Blick auch zur Laufzeit keinen Fehler verursachen würde. Schauen wir uns mal ein konstruiertes Beispiel an:

public class MyClass {
 public boolean equals(Object e) {
    if(e.toString() == "")
      new IllegalArgumentException();
    return true;
 }
}

Solcher Code wird kompiliert. Aber wenn ein Fehler passiert, wird man ihn schlecht nachvollziehen können, weil er sich nur in bestimmten Programmzuständen auswirkt.

Deshalb ist statische Code-Analyse, eine Art maschineller Code-Review, eine Hilfe. Ich benutze aktuell das Java-Tool FindBugs. Wenn ich FindBugs über den obigen Code laufen lasse, liefert es folgende vier Fehler:

MyClass.java:4 new IllegalArgumentException() not thrown in MyClass.equals(Object)

In Zeile 4 will ich eigentlich eine Exception werfen, habe aber das throw vergessen.

MyClass.java:3 Comparison of String objects using == or != in MyClass.equals(Object)

In Zeile 3 mache ich einen String-Vergleich mit dem ==-Operator. Völlig legaler Code, aber ob es gewollt ist? Normalerweise benutzt man die equals-Methode.

MyClass.java:3 MyClass defines equals and uses Object.hashCode()

Stimmt, ich habe das Protokoll nicht eingehalten. Laut Javadoc soll man auch hashCode() implementieren, wenn man eine equals-Methode bereit stellt. Denn für HashMaps ist das wichtig. Wird einem aber so lange nicht zum Verhängnis, wie die Objekte nicht in HashMaps gepackt werden.

MyClass.java:3 MyClass.equals(Object) does not check for null argument

In Zeile 3 habe ich den Parameter einfach benutzt, ohne zu prüfen ob er Null ist. Sollte es Null sein, muss equals ein False zurück liefern. Es ist selten das der Parameter in equals Null ist, weil diese Methode selten Null-Referenzen bekommt. Durch Such- oder Sortier-Algorithmen in Collections sind Objekte meistens gültig. Aber das equals-Protokoll sagt, das bei Null ein False zurück muss.

FindBugs hat dann geholfen folgenden Code zu schreiben:

public class MyClass {
  public boolean equals(Object b) {
    if (b == null)
      return false;
    if("".equals(b.toString()))
      throw new IllegalArgumentException();
    return true;
  }

  public int hashCode() {
    assert false : "hashCode not designed";
    return 42; 
  }
}

Das sieht doch gleich viel sauberer aus. 🙂

Bug-Patterns und Bug-Levels

FindBugs benutzt zur Suche sogenannte Bug-Patterns. Diese werden je nach Schwere in Bug-Levels kategorisiert: Scariest, Scary, Troubling und Normal Confidence.
Dabei kann ein Bug-Pattern aber auch einen negativen Fehler melden. Man kann dem Tool somit nicht voll vertrauen, und sollte auch selbst mit denken.
Einige Bug-Patterns liefern auch gleich eine Lösung mit, wie z.B. die obige hashCode-Implementierung.
Es gibt für die Eclipse IDE und NetBeans IDE Plugins, damit man sich nicht mit der Commandozeile rum schlagen muss.

Fazit

Die statische Code-Analyse braucht man nicht bei jedem Compile-Vorgang ausführen. Denn der Compiler und Unittests decken schon viel auf. Ich orientiere mich danach, wie oft ich Code-Reviews von Menschen machen lassen würde… zu jedem Milestone. Wer zu Projektanfang sehr viel Code produziert, kann das auch vor jedem VCS-Check-in tun.

Ich kann FindBugs bzw. Statische Code-Analyse jedem empfehlen. Wer Erfahrung mit anderen Tools hat, die besser sind, kann gerne einen Kommentar hinterlassen.