JavaRush /Java-Blog /Random-DE /FindBugs hilft Ihnen, Java besser zu lernen
articles
Level 15

FindBugs hilft Ihnen, Java besser zu lernen

Veröffentlicht in der Gruppe Random-DE
Statische Code-Analysatoren sind beliebt, weil sie dabei helfen, Fehler zu finden, die durch Unachtsamkeit entstanden sind. Viel interessanter ist jedoch, dass sie dabei helfen, Fehler aus Unwissenheit zu korrigieren. Auch wenn alles in der offiziellen Dokumentation der Sprache steht, ist es keine Tatsache, dass alle Programmierer sie sorgfältig gelesen haben. Und Programmierer können verstehen: Sie werden es leid sein, die ganze Dokumentation zu lesen. In dieser Hinsicht ist ein statischer Analysator wie ein erfahrener Freund, der neben Ihnen sitzt und Ihnen beim Schreiben von Code zusieht. Er sagt Ihnen nicht nur: „Hier haben Sie beim Kopieren und Einfügen einen Fehler gemacht“, sondern sagt auch: „Nein, so können Sie nicht schreiben, schauen Sie sich die Dokumentation selbst an.“ Ein solcher Freund ist nützlicher als die Dokumentation selbst, weil er nur die Dinge vorschlägt, die Ihnen bei Ihrer Arbeit tatsächlich begegnen, und über die Dinge schweigt, die Ihnen niemals nützlich sein werden. In diesem Beitrag werde ich über einige der Java-Feinheiten sprechen, die ich durch die Verwendung des statischen Analysetools FindBugs gelernt habe. Vielleicht kommt auch für Sie einiges unerwartet. Wichtig ist, dass alle Beispiele nicht spekulativ sind, sondern auf echtem Code basieren.

Ternärer Operator ?:

Es scheint, dass es nichts Einfacheres als den ternären Operator gibt, aber er hat seine Tücken. Ich glaubte, dass es keinen grundlegenden Unterschied zwischen den Entwürfen gab Type var = condition ? valTrue : valFalse; , und Type var; if(condition) var = valTrue; else var = valFalse; es stellte sich heraus, dass es hier eine Feinheit gab. Da der ternäre Operator Teil eines komplexen Ausdrucks sein kann, muss sein Ergebnis ein konkreter Typ sein, der zur Kompilierungszeit bestimmt wird. Daher führt der Compiler beispielsweise bei einer wahren Bedingung in der if-Form valTrue direkt zum Typ Type und in Form eines ternären Operators zunächst zu den gemeinsamen Typen valTrue und valFalse (obwohl dies bei valFalse nicht der Fall ist). ausgewertet), und das Ergebnis führt dann zum Typ Type. Die Casting-Regeln sind nicht ganz trivial, wenn der Ausdruck primitive Typen und Wrapper darüber beinhaltet (Integer, Double usw.). Alle Regeln werden ausführlich in JLS 15.25 beschrieben. Schauen wir uns einige Beispiele an. Number n = flag ? new Integer(1) : new Double(2.0); Was passiert mit n, wenn das Flag gesetzt ist? Ein Double-Objekt mit einem Wert von 1,0. Der Compiler findet unsere ungeschickten Versuche, ein Objekt zu erstellen, lustig. Da das zweite und dritte Argument Wrapper für verschiedene primitive Typen sind, entpackt der Compiler sie und führt zu einem präziseren Typ (in diesem Fall double). Und nach der Ausführung des ternären Operators für die Zuweisung wird das Boxen erneut durchgeführt. Im Wesentlichen entspricht der Code diesem: Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); Aus Sicht des Compilers enthält der Code keine Probleme und lässt sich perfekt kompilieren. Aber FindBugs gibt eine Warnung aus:
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: Der primitive Wert wird entpackt und für den ternären Operator in TestTernary.main(String[]) erzwungen. Ein umschlossener primitiver Wert wird entpackt und im Rahmen der Auswertung eines bedingten ternären Operators (des b? e1: e2-Operators) in einen anderen primitiven Typ konvertiert ). Die Semantik von Java schreibt vor, dass, wenn e1 und e2 umschlossene numerische Werte sind, die Werte entpackt und in ihren gemeinsamen Typ konvertiert/erzwungen werden (z. B. wenn e1 vom Typ Integer und e2 vom Typ Float ist, dann wird e1 entpackt). in einen Gleitkommawert konvertiert und verpackt. Siehe JLS-Abschnitt 15.25. Natürlich warnt FindBugs auch, dass Integer.valueOf(1) effizienter ist als new Integer(1), aber das weiß bereits jeder.
Oder dieses Beispiel: Integer n = flag ? 1 : null; Der Autor möchte null in n einfügen, wenn das Flag nicht gesetzt ist. Glaubst du, dass es funktionieren wird? Ja. Aber machen wir es komplizierter: Integer n = flag1 ? 1 : flag2 ? 2 : null; Es scheint, dass es keinen großen Unterschied gibt. Wenn nun jedoch beide Flags gelöscht sind, löst diese Zeile eine NullPointerException aus. Die Optionen für den rechten ternären Operator sind int und null, daher ist der Ergebnistyp Integer. Die Optionen für die linke Seite sind int und Integer, sodass das Ergebnis gemäß den Java-Regeln int ist. Dazu müssen Sie das Unboxing durchführen, indem Sie intValue aufrufen, wodurch eine Ausnahme ausgelöst wird. Der Code entspricht diesem: Integer n; if( flag1 ) n = Integer.valueOf(1); else { if( flag2 ) n = Integer.valueOf(Integer.valueOf(2).intValue()); else n = Integer.valueOf(((Integer)null).intValue()); } Hier erzeugt FindBugs zwei Meldungen, die ausreichen, um einen Fehler zu vermuten:
BX_UNBOXING_IMMEDIATELY_REBOXED: Der geboxte Wert wird entpackt und dann sofort in TestTernary.main(String[]) erneut verpackt. NP_NULL_ON_SOME_PATH: Mögliche Nullzeiger-Dereferenzierung von null in TestTernary.main(String[]). Es gibt einen Zweig der Anweisung, der bei Ausführung garantiert, dass a Der Nullwert wird dereferenziert, was bei der Ausführung des Codes eine NullPointerException erzeugen würde.
Nun, ein letztes Beispiel zu diesem Thema: double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } Es ist nicht verwunderlich, dass dieser Code nicht funktioniert: Wie kann eine Funktion, die einen primitiven Typ zurückgibt, null zurückgeben? Überraschenderweise lässt es sich ohne Probleme kompilieren. Nun, Sie verstehen bereits, warum es kompiliert wird.

Datumsformat

Um Datums- und Uhrzeitangaben in Java zu formatieren, empfiehlt es sich, Klassen zu verwenden, die die DateFormat-Schnittstelle implementieren. Das sieht zum Beispiel so aus: public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } Oft verwendet eine Klasse immer wieder dasselbe Format. Viele Leute werden auf die Idee der Optimierung kommen: Warum jedes Mal ein Formatobjekt erstellen, wenn eine gemeinsame Instanz verwendet werden kann? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } Es ist so schön und cool, aber leider funktioniert es nicht. Genauer gesagt funktioniert es, aber gelegentlich geht es kaputt. Tatsache ist, dass in der Dokumentation für DateFormat Folgendes steht:
Datumsformate werden nicht synchronisiert. Es wird empfohlen, für jeden Thread separate Formatinstanzen zu erstellen. Greifen mehrere Threads gleichzeitig auf ein Format zu, muss dieses extern synchronisiert werden.
Und das trifft zu, wenn man sich die interne Implementierung von SimpleDateFormat ansieht. Während der Ausführung der format()-Methode schreibt das Objekt in die Klassenfelder, sodass die gleichzeitige Verwendung von SimpleDateFormat aus zwei Threads mit einiger Wahrscheinlichkeit zu einem falschen Ergebnis führt. Folgendes schreibt FindBugs dazu:
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: Aufruf der Methode des statischen java.text.DateFormat in TestDate.getDate() Wie im JavaDoc angegeben, sind DateFormats von Natur aus unsicher für die Multithread-Verwendung. Der Detektor hat einen Aufruf einer DateFormat-Instanz gefunden, der über ein statisches Feld abgerufen wurde. Das sieht verdächtig aus. Weitere Informationen hierzu finden Sie unter Sun Bug #6231579 und Sun Bug #6178997.

Fallstricke von BigDecimal

Nachdem Sie erfahren haben, dass Sie mit der BigDecimal-Klasse Bruchzahlen beliebiger Genauigkeit speichern können, und sehen, dass sie über einen Konstruktor für double verfügt, werden einige zu dem Schluss kommen, dass alles klar ist und Sie es so machen können: System.out.println(new BigDecimal( 1.1)); Niemand verbietet dies wirklich, aber das Ergebnis mag unerwartet erscheinen: 1.100000000000000088817841970012523233890533447265625. Dies liegt daran, dass das primitive Double im IEEE754-Format gespeichert ist, in dem es unmöglich ist, 1,1 vollkommen genau darzustellen (im binären Zahlensystem erhält man einen unendlichen periodischen Bruch). Daher wird dort der Wert gespeichert, der 1,1 am nächsten kommt. Im Gegenteil, der BigDecimal(double)-Konstruktor funktioniert genau: Er wandelt eine gegebene Zahl in IEEE754 perfekt in die Dezimalform um (ein endgültiger binärer Bruch ist immer als endgültige Dezimalzahl darstellbar). Wenn Sie genau 1,1 als BigDecimal darstellen möchten, können Sie entweder new BigDecimal("1.1") oder BigDecimal.valueOf(1.1) schreiben. Wenn Sie die Nummer nicht sofort anzeigen, sondern einige Operationen damit durchführen, verstehen Sie möglicherweise nicht, woher der Fehler kommt. FindBugs gibt eine Warnung DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE aus, die den gleichen Hinweis gibt. Hier ist noch etwas: BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); Tatsächlich stellen d1 und d2 dieselbe Zahl dar, aber equal gibt false zurück, da nicht nur der Wert der Zahlen, sondern auch die aktuelle Reihenfolge (die Anzahl der Dezimalstellen) verglichen wird. Dies steht in der Dokumentation, aber nur wenige Leute werden die Dokumentation für eine so bekannte Methode wie Equals lesen. Ein solches Problem tritt möglicherweise nicht sofort auf. FindBugs selbst warnt leider nicht davor, aber es gibt eine beliebte Erweiterung dafür – fb-contrib, die diesen Fehler berücksichtigt:
MDM_BIGDECIMAL_EQUALS equal() wird aufgerufen, um zwei java.math.BigDecimal-Zahlen zu vergleichen. Dies ist normalerweise ein Fehler, da zwei BigDecimal-Objekte nur dann gleich sind, wenn sie sowohl im Wert als auch in der Skalierung gleich sind, sodass 2,0 nicht gleich 2,00 ist. Um BigDecimal-Objekte auf mathematische Gleichheit zu vergleichen, verwenden Sie stattdessen „compareTo()“.

Zeilenumbrüche und printf

Oft sind Programmierer, die nach C auf Java umsteigen, froh, PrintStream.printf (sowie PrintWriter.printf usw.) zu entdecken . Großartig, ich weiß, dass man, genau wie in C, nichts Neues lernen muss. Es gibt tatsächlich Unterschiede. Eine davon sind Zeilenübersetzungen. Die C-Sprache ist in Text- und Binärströme unterteilt. Die Ausgabe des Zeichens „\n“ in einen Textstream wird automatisch in einen systemabhängigen Zeilenumbruch („\r\n“ unter Windows) umgewandelt. In Java gibt es keine solche Trennung: Es muss die richtige Zeichenfolge an den Ausgabestream übergeben werden. Dies geschieht automatisch, beispielsweise durch Methoden der PrintStream.println-Familie. Bei Verwendung von printf ist die Übergabe von „\n“ in der Formatzeichenfolge jedoch nur „\n“ und kein systemabhängiger Zeilenumbruch. Schreiben wir zum Beispiel den folgenden Code: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); Nachdem wir das Ergebnis in eine Datei umgeleitet haben, werden wir sehen: FindBugs hilft Ihnen, Java besser zu lernen - 1 So kann es zu einer seltsamen Kombination von Zeilenumbrüchen in einem Thread kommen, die schlampig aussieht und manchen Parser umhauen kann. Der Fehler kann lange Zeit unbemerkt bleiben, insbesondere wenn Sie hauptsächlich auf Unix-Systemen arbeiten. Um mit printf einen gültigen Zeilenumbruch einzufügen, wird ein spezielles Formatierungszeichen „%n“ verwendet. Folgendes schreibt FindBugs dazu:
VA_FORMAT_STRING_USES_NEWLINE: Die Formatzeichenfolge sollte %n statt \n in TestNewline.main(String[]) verwenden. Diese Formatzeichenfolge enthält ein Zeilenumbruchzeichen (\n). In Formatzeichenfolgen ist es im Allgemeinen besser, %n zu verwenden, das das plattformspezifische Zeilentrennzeichen erzeugt.
Vielleicht war einigen Lesern das alles schon lange bekannt. Aber ich bin mir fast sicher, dass es für sie eine interessante Warnung des statischen Analysators geben wird, die ihnen neue Funktionen der verwendeten Programmiersprache verrät.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION