Java Language
Java-Fallstricke - Sprachsyntax
Suche…
Einführung
Bei einem Missbrauch von Java-Programmiersprachen kann es vorkommen, dass ein Programm fehlerhafte Ergebnisse generiert, obwohl es korrekt kompiliert wurde. Das Hauptziel dieses Themas ist es, häufige Fallstricke mit ihren Ursachen aufzulisten und den richtigen Weg vorzuschlagen, um zu vermeiden, dass solche Probleme auftreten.
Bemerkungen
In diesem Thema werden bestimmte Aspekte der Java-Sprachsyntax behandelt, die entweder fehleranfällig sind oder auf bestimmte Weise nicht verwendet werden sollten.
Pitfall - Ignoriert die Sichtbarkeit der Methode
Selbst erfahrene Java-Entwickler neigen zu der Annahme, dass Java nur drei Schutzmodifikatoren hat. Die Sprache hat eigentlich vier! Das Paket private (auch bekannt als Standard) Sichtbarkeitsebene wird häufig vergessen.
Sie sollten darauf achten, welche Methoden Sie veröffentlichen. Die öffentlichen Methoden in einer Anwendung sind die sichtbare API der Anwendung. Dies sollte so klein und kompakt wie möglich sein, insbesondere wenn Sie eine wiederverwendbare Bibliothek schreiben (siehe auch das SOLID- Prinzip). Es ist wichtig, die Sichtbarkeit aller Methoden in ähnlicher Weise zu berücksichtigen und den geschützten oder geschützten privaten Zugriff nur bei Bedarf zu verwenden.
Wenn Sie Methoden deklarieren, die privat als öffentlich gelten sollen, legen Sie die internen Implementierungsdetails der Klasse offen.
Eine Folge davon ist, dass Sie nur die öffentlichen Methoden Ihrer Klasse als Unit-Test testen. Tatsächlich können Sie nur öffentliche Methoden testen. Es ist nicht ratsam, die Sichtbarkeit privater Methoden zu erhöhen, nur um Unit-Tests mit diesen Methoden durchführen zu können. Das Testen öffentlicher Methoden, die die Methoden mit einer restriktiveren Sichtbarkeit aufrufen, sollte ausreichen, um eine gesamte API zu testen. Sie sollten Ihre API niemals mit mehr öffentlichen Methoden erweitern, um nur Komponententests zu ermöglichen.
Pitfall - Fehlende "Pause" in einem "Switch" -Fall
Diese Java-Probleme können sehr peinlich sein und bleiben bis zum Start in der Produktion unentdeckt. Durchbruchsverhalten in switch-Anweisungen ist häufig nützlich. Wenn jedoch ein Schlüsselwort „break“ fehlt, wenn ein solches Verhalten nicht erwünscht ist, kann dies katastrophale Folgen haben. Wenn Sie vergessen haben, im folgenden Codebeispiel eine "Pause" in "Fall 0" einzugeben, schreibt das Programm "Null" gefolgt von "Eins", da der Steuerfluss hier die gesamte "switch" -Anweisung durchläuft es erreicht eine "Pause". Zum Beispiel:
public static void switchCasePrimer() {
int caseIndex = 0;
switch (caseIndex) {
case 0:
System.out.println("Zero");
case 1:
System.out.println("One");
break;
case 2:
System.out.println("Two");
break;
default:
System.out.println("Default");
}
}
In den meisten Fällen besteht die einfachere Lösung darin, Schnittstellen zu verwenden und Code mit spezifischem Verhalten in separate Implementierungen zu verschieben ( Komposition über Vererbung ).
Wenn eine switch-Anweisung unvermeidbar ist, wird empfohlen, "erwartete" Fallthroughs zu dokumentieren, wenn sie auftreten. Auf diese Weise zeigen Sie anderen Entwicklern, dass Sie über die fehlende Unterbrechung Bescheid wissen und dass dies ein erwartetes Verhalten ist.
switch(caseIndex) {
[...]
case 2:
System.out.println("Two");
// fallthrough
default:
System.out.println("Default");
Pitfall - falsch platzierte Semikolons und fehlende Klammern
Dies ist ein Fehler, der bei Java-Anfängern echte Verwirrung stiftet, zumindest beim ersten Mal. Anstatt dies zu schreiben:
if (feeling == HAPPY)
System.out.println("Smile");
else
System.out.println("Frown");
Sie schreiben aus Versehen:
if (feeling == HAPPY);
System.out.println("Smile");
else
System.out.println("Frown");
und sind verwirrt, wenn der Java-Compiler ihnen mitteilt, dass das else
falsch ist. Der Java-Compiler interpretiert das oben wie folgt:
if (feeling == HAPPY)
/*empty statement*/ ;
System.out.println("Smile"); // This is unconditional
else // This is misplaced. A statement cannot
// start with 'else'
System.out.println("Frown");
In anderen Fällen treten keine Kompilierungsfehler auf, der Code entspricht jedoch nicht dem, was der Programmierer beabsichtigt. Zum Beispiel:
for (int i = 0; i < 5; i++);
System.out.println("Hello");
druckt nur einmal "Hallo". Wiederum bedeutet das falsche Semikolon, dass der Rumpf der for
Schleife eine leere Anweisung ist. Das bedeutet, dass der folgende println
Aufruf unbedingt ist.
Eine andere Variante:
for (int i = 0; i < 5; i++);
System.out.println("The number is " + i);
Dies führt zu einem Fehler "Ich konnte kein Symbol finden" für i
. Das Vorhandensein des falschen Semikolons bedeutet, dass der Aufruf von println
versucht, i
außerhalb seines Gültigkeitsbereichs zu verwenden.
In diesen Beispielen gibt es eine einfache Lösung: Löschen Sie einfach das falsche Semikolon. Es gibt jedoch einige tiefere Lehren aus diesen Beispielen:
Das Semikolon in Java ist kein "syntaktisches Rauschen". Das Vorhandensein oder Fehlen eines Semikolons kann die Bedeutung Ihres Programms ändern. Fügen Sie sie nicht einfach am Ende jeder Zeile hinzu.
Vertrauen Sie dem Einzug Ihres Codes nicht. In der Java-Sprache werden zusätzliche Leerzeichen am Anfang einer Zeile vom Compiler ignoriert.
Verwenden Sie einen automatischen Indenter. Alle IDEs und viele einfache Texteditoren wissen, wie Java-Code korrekt eingerückt wird.
Dies ist die wichtigste Lektion. Folgen Sie den neuesten Java-Stilrichtlinien und setzen Sie die Anweisungen "then" und "else" sowie die body-Anweisung einer Schleife in Klammern. Die offene Klammer (
{
) sollte sich nicht in einer neuen Zeile befinden.
Wenn der Programmierer die Stilregeln befolgt, würde das if
Beispiel mit einem falsch platzierten Semikolon folgendermaßen aussehen:
if (feeling == HAPPY); {
System.out.println("Smile");
} else {
System.out.println("Frown");
}
Das sieht für ein erfahrenes Auge merkwürdig aus. Wenn Sie diesen Code automatisch eingerückt haben, würde er wahrscheinlich so aussehen:
if (feeling == HAPPY); {
System.out.println("Smile");
} else {
System.out.println("Frown");
}
das sollte sogar für einen Anfänger als falsch auffallen.
Fallstricke - Klammern weglassen: Probleme mit dem "baumelnden wenn" und "hängenden anderen"
Die neueste Version des Oracle Java-Style-Guide verlangt, dass die Anweisungen "then" und "else" in einer if
Anweisung immer in "geschweifte Klammern" oder "geschweifte Klammern" eingeschlossen werden. Ähnliche Regeln gelten für die Körper verschiedener Schleifenanweisungen.
if (a) { // <- open brace
doSomething();
doSomeMore();
} // <- close brace
Dies ist für die Java-Sprachsyntax eigentlich nicht erforderlich. Wenn der "dann" -Teil einer if
Anweisung eine einzige Anweisung ist, ist es legal, die Klammern wegzulassen
if (a)
doSomething();
oder auch
if (a) doSomething();
Es gibt jedoch Gefahren, wenn Java-Regeln ignoriert und die Klammern weggelassen werden. Insbesondere erhöhen Sie das Risiko, dass Code mit fehlerhafter Einrückung falsch gelesen wird.
Das "baumelnde if" Problem:
Betrachten Sie den Beispielcode von oben, ohne Klammern umgeschrieben.
if (a)
doSomething();
doSomeMore();
Dieser Code scheint zu sagen, dass die Aufrufe von doSomething
und doSomeMore
sowohl dann als auch nur dann auftreten, wenn a
true
. Tatsächlich ist der Code falsch eingerückt. Die Java-Sprachspezifikation, die der Aufruf von doSomeMore()
nach der if
Anweisung darstellt. Die richtige Einrückung lautet wie folgt:
if (a)
doSomething();
doSomeMore();
Das Problem des "baumelnden anderen"
Ein zweites Problem tritt auf, wenn wir der Mischung else
hinzufügen. Betrachten Sie das folgende Beispiel mit fehlenden geschweiften Klammern.
if (a)
if (b)
doX();
else if (c)
doY();
else
doZ();
Der obige Code scheint zu sagen, dass doZ
wird, wenn a
false
. In der Tat ist der Einzug erneut falsch. Die richtige Einrückung für den Code lautet:
if (a)
if (b)
doX();
else if (c)
doY();
else
doZ();
Wenn der Code gemäß den Regeln des Java-Stils geschrieben wurde, würde er tatsächlich so aussehen:
if (a) {
if (b) {
doX();
} else if (c) {
doY();
} else {
doZ();
}
}
Um zu veranschaulichen, warum das besser ist, nehmen Sie an, Sie hätten den Code versehentlich falsch eingerückt. Sie könnten mit so etwas enden:
if (a) { if (a) {
if (b) { if (b) {
doX(); doX();
} else if (c) { } else if (c) {
doY(); doY();
} else { } else {
doZ(); doZ();
} }
} }
In beiden Fällen sieht der falsch eingerückte Code jedoch für einen erfahrenen Java-Programmierer falsch aus.
Pitfall - Überladen statt überschreiben
Betrachten Sie das folgende Beispiel:
public final class Person {
private final String firstName;
private final String lastName;
public Person(String firstName, String lastName) {
this.firstName = (firstName == null) ? "" : firstName;
this.lastName = (lastName == null) ? "" : lastName;
}
public boolean equals(String other) {
if (!(other instanceof Person)) {
return false;
}
Person p = (Person) other;
return firstName.equals(p.firstName) &&
lastName.equals(p.lastName);
}
public int hashcode() {
return firstName.hashCode() + 31 * lastName.hashCode();
}
}
Dieser Code verhält sich nicht wie erwartet. Das Problem ist , dass die equals
und hashcode
Methoden zur Person
nicht über die Standardmethoden durch definierte außer Kraft setzen Object
.
- Die Methode
equals
hat die falsche Signatur. Es sollte alsequals(Object)
nichtequals(String)
deklariert werden. - Die
hashcode
Methode hat den falschen Namen. Es solltehashCode()
(beachten Sie die Hauptstadt C ).
Diese Fehler bedeuten, dass wir versehentliche Überladungen deklariert haben. Diese werden nicht verwendet, wenn Person
in einem polymorphen Kontext verwendet wird.
Es gibt jedoch einen einfachen Weg, um damit umzugehen (ab Java 5). Verwenden Sie die Annotation @Override
wenn Sie beabsichtigen, dass Ihre Methode eine Überschreibung ist:
public final class Person {
...
@Override
public boolean equals(String other) {
....
}
@Override
public hashcode() {
....
}
}
Wenn wir eine hinzufügen @Override
Anmerkung zu einer Methodendeklaration, prüft der Compiler , dass die Methode überschreibt , nicht (oder implementieren) eine Methode in einer übergeordneten Klasse oder Schnittstelle deklarierte. Im obigen Beispiel gibt der Compiler zwei Kompilierungsfehler aus, die ausreichen sollten, um auf den Fehler aufmerksam zu machen.
Pitfall - Oktal Literale
Betrachten Sie den folgenden Codeausschnitt:
// Print the sum of the numbers 1 to 10
int count = 0;
for (int i = 1; i < 010; i++) { // Mistake here ....
count = count + i;
}
System.out.println("The sum of 1 to 10 is " + count);
Ein Java-Anfänger könnte überrascht sein, dass das obige Programm die falsche Antwort ausgibt. Tatsächlich wird die Summe der Zahlen 1 bis 8 ausgegeben.
Der Grund ist, dass ein Integer-Literal, das mit der Ziffer Null ('0') beginnt, vom Java-Compiler als Oktal-Literal und nicht als Dezimal-Literal interpretiert wird. Somit ist 010
die Oktalzahl 10, die in Dezimalzahl 8 ist.
Pitfall - Deklarieren von Klassen mit denselben Namen wie Standardklassen
Manchmal machen Programmierer, die mit Java noch nicht vertraut sind, den Fehler, eine Klasse mit einem Namen zu definieren, der mit einer weit verbreiteten Klasse identisch ist. Zum Beispiel:
package com.example;
/**
* My string utilities
*/
public class String {
....
}
Dann fragen sie sich, warum sie unerwartete Fehler bekommen. Zum Beispiel:
package com.example;
public class Test {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
Wenn Sie die obigen Klassen kompilieren und dann versuchen, sie auszuführen, erhalten Sie eine Fehlermeldung:
$ javac com/example/*.java
$ java com.example.Test
Error: Main method not found in class test.Test, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application
Jemand, der sich den Code für die Test
Klasse ansieht, würde die Deklaration von main
sehen, sich die Signatur ansehen und sich fragen, worüber sich der java
Befehl beschwert. Tatsächlich sagt der java
Befehl jedoch die Wahrheit.
Wenn wir eine Version von String
im selben Paket wie Test
deklarieren, hat diese Version Vorrang vor dem automatischen Import von java.lang.String
. Die Signatur der Test.main
Methode ist also tatsächlich
void main(com.example.String[] args)
anstatt
void main(java.lang.String[] args)
und der java
Befehl erkennt das nicht als eine Trypoint-Methode.
Lektion: Definieren Sie keine Klassen, die denselben Namen wie vorhandene Klassen in java.lang
oder andere häufig verwendete Klassen in der Java SE-Bibliothek haben. Wenn Sie das tun, öffnen Sie sich für alle Arten von dunklen Fehlern.
Pitfall - Verwenden Sie '==', um einen Boolean zu testen
Manchmal schreibt ein neuer Java-Programmierer Code wie folgt:
public void check(boolean ok) {
if (ok == true) { // Note 'ok == true'
System.out.println("It is OK");
}
}
Ein erfahrener Programmierer würde das als unbeholfen wahrnehmen und möchte es wie folgt umschreiben:
public void check(boolean ok) {
if (ok) {
System.out.println("It is OK");
}
}
Es ist jedoch mehr falsch mit ok == true
als einfache Ungeschicklichkeit. Betrachten Sie diese Variante:
public void check(boolean ok) {
if (ok = true) { // Oooops!
System.out.println("It is OK");
}
}
Hier hat der Programmierer ==
as =
... falsch geschrieben, und jetzt hat der Code einen subtilen Fehler. Der Ausdruck x = true
weist bedingungslos true
zu x
und wertet dann zu true
. Mit anderen Worten, die check
gibt nun "Es ist OK" aus, unabhängig von dem Parameter.
Die Lektion hier ist, sich der Gewohnheit zu == false
, == false
und == true
. Sie sind nicht nur ausführlich, sondern machen sie auch fehleranfälliger.
Hinweis: Eine mögliche Alternative zu ok == true
, die die Gefahr vermeidet, besteht in der Verwendung von Yoda-Bedingungen . dh setzen Sie das Literal auf die linke Seite des relationalen Operators, wie in true == ok
. Dies funktioniert, aber die meisten Programmierer würden wahrscheinlich zustimmen, dass die Bedingungen von Yoda ungerade aussehen. Sicher ist ok
(oder !ok
) prägnanter und natürlicher.
Fallstricke - Wildcard-Importe können Ihren Code brüchig machen
Betrachten Sie das folgende Teilbeispiel:
import com.example.somelib.*;
import com.acme.otherlib.*;
public class Test {
private Context x = new Context(); // from com.example.somelib
...
}
Angenommen, Sie haben den Code bei der ersten Entwicklung gegen Version 1.0 von somelib
und Version 1.0 von otherlib
. Zu einem späteren Zeitpunkt müssen Sie Ihre Abhängigkeiten auf eine otherlib
Version aktualisieren, und Sie entscheiden sich für otherlib
Version 2.0. Nehmen Sie außerdem an, dass eine der Änderungen, die sie zwischen 1.0 und 2.0 an otherlib
, das Hinzufügen einer Context
Klasse war.
Wenn Sie jetzt den Test
kompilieren, wird ein Kompilierungsfehler angezeigt, der Sie darüber Context
dass Context
ein mehrdeutiger Import ist.
Wenn Sie mit der Codebase vertraut sind, ist dies wahrscheinlich nur eine geringfügige Unannehmlichkeit. Wenn nicht, dann haben Sie einige Arbeit zu tun, um dieses Problem hier und möglicherweise anderswo anzugehen.
Das Problem hier sind die Wildcard-Importe. Einerseits kann die Verwendung von Platzhaltern die Anzahl der Klassen um ein paar Zeilen verkürzen. Auf der anderen Seite:
Aufwärtskompatible Änderungen an anderen Teilen Ihrer Codebase, an Java-Standardbibliotheken oder an Drittanbieter-Bibliotheken können zu Kompilierungsfehlern führen.
Lesbarkeit leidet. Wenn Sie keine IDE verwenden, kann es schwierig sein, herauszufinden, welcher der Importe von Platzhaltern eine benannte Klasse zieht.
Die Lektion ist, dass es keine gute Idee ist, Wildcard-Importe in Code zu verwenden, der langlebig sein muss. Bestimmte (Nicht-Platzhalter) -Importe sind bei der Verwendung einer IDE nur schwer zu pflegen, und der Aufwand lohnt sich.
Pitfall: Verwenden von 'Assert' für die Validierung von Argumenten oder Benutzereingaben
Eine Frage , die gelegentlich auf Stackoverflow ist , ob es angemessen ist , zu verwenden assert
Argumente durch den Benutzer auf ein Verfahren oder auch Eingänge vorgesehen geliefert zu validieren.
Die einfache Antwort ist, dass es nicht angemessen ist.
Bessere Alternativen sind:
- Eine IllegalArgumentException mit benutzerdefiniertem Code auslösen.
- Verwenden der in Google Guava-Bibliothek verfügbaren
Preconditions
Methoden. - Verwenden der
Validate
Methoden, die in der Apache Commons Lang3-Bibliothek verfügbar sind.
Dies empfiehlt die Java-Sprachspezifikation (JLS 14.10 für Java 8) in dieser Angelegenheit:
Normalerweise ist die Assertionsprüfung während der Programmentwicklung und beim Testen aktiviert und für die Bereitstellung deaktiviert, um die Leistung zu verbessern.
Da Assertions möglicherweise deaktiviert sind, dürfen Programme nicht davon ausgehen, dass die in Assertions enthaltenen Ausdrücke ausgewertet werden. Daher sollten diese booleschen Ausdrücke im Allgemeinen frei von Nebenwirkungen sein. Die Auswertung eines solchen booleschen Ausdrucks sollte keinen Status beeinflussen, der nach Abschluss der Auswertung sichtbar ist. Es ist nicht illegal, dass ein boolescher Ausdruck, der in einer Assertion enthalten ist, Nebeneffekte hat, aber im Allgemeinen ist dies ungeeignet, da das Programmverhalten davon abhängen kann, ob Assertions aktiviert oder deaktiviert wurden.
Vor diesem Hintergrund sollten Assertions nicht zur Argumentprüfung in öffentlichen Methoden verwendet werden. Die Überprüfung von Argumenten ist normalerweise Teil des Vertrages einer Methode. Dieser Vertrag muss bestätigt werden, unabhängig davon, ob Zusicherungen aktiviert oder deaktiviert sind.
Ein sekundäres Problem bei der Verwendung von Assertions für die Argumentprüfung ist, dass fehlerhafte Argumente zu einer entsprechenden Laufzeitausnahme führen sollten (z. B.
IllegalArgumentException
,ArrayIndexOutOfBoundsException
oderNullPointerException
). Bei einem Assertionsfehler wird keine entsprechende Ausnahme ausgelöst. Es ist wiederum nicht illegal, Zusicherungen für die Argumentprüfung öffentlicher Methoden zu verwenden, ist aber generell unangemessen. Es ist beabsichtigt, dassAssertionError
niemals abgefangen wird, aber es ist möglich, dies zu tun. Daher sollten die Regeln für try-Anweisungen Assertions behandeln, die in einem try-Block erscheinen, ähnlich wie die aktuelle Behandlung von throw-Anweisungen.
Fallstricke beim automatischen Auspacken von Nullobjekten in Grundkörper
public class Foobar {
public static void main(String[] args) {
// example:
Boolean ignore = null;
if (ignore == false) {
System.out.println("Do not ignore!");
}
}
}
Die Falle ist hier, dass null
mit false
verglichen wird. Da wir ein primitives boolean
mit einem Boolean
, versucht Java, das Boolean
Object
in ein primitives Äquivalent zu entpacken, das zum Vergleich bereit ist. Da dieser Wert jedoch null
, wird eine NullPointerException
ausgelöst.
Java kann NullPointerException
primitiven Typen mit null
, was zur Laufzeit eine NullPointerException
verursacht. Betrachten Sie den primitiven Fall der Bedingung false == null
; Dies würde einen Kompilierzeitfehler erzeugen , der nicht vergleichbar ist incomparable types: int and <null>
.