Suche…


Einführung

In diesem Thema werden einige der häufigsten Fehler beschrieben, die von Anfängern in Java gemacht wurden.

Dies schließt etwaige häufige Fehler bei der Verwendung der Java-Sprache oder das Verständnis der Laufzeitumgebung ein.

Fehler, die mit bestimmten APIs verbunden sind, können in den für diese APIs spezifischen Themen beschrieben werden. Strings sind ein Sonderfall. Sie werden in der Java-Sprachspezifikation behandelt. Andere Details als häufig vorkommende Fehler können in diesem Thema unter Strings beschrieben werden .

Fallstricke: Verwenden Sie ==, um primitive Wrapper-Objekte wie Integer zu vergleichen

(Diese Fallstricke gilt für alle primitiven Wrapper-Typen, wir werden sie jedoch für Integer und int illustrieren.)

Wenn Sie mit Integer Objekten arbeiten, ist es verführerisch, == zum Vergleichen von Werten zu verwenden, da Sie dies mit int Werten tun würden. Und in manchen Fällen scheint das zu funktionieren:

Integer int1_1 = Integer.valueOf("1");
Integer int1_2 = Integer.valueOf(1);

System.out.println("int1_1 == int1_2: " + (int1_1 == int1_2));          // true
System.out.println("int1_1 equals int1_2: " + int1_1.equals(int1_2));   // true

Hier haben wir zwei Integer Objekte mit dem Wert 1 und vergleichen (in diesem Fall haben wir eines aus einem String und eines aus einem int Literal erstellt. Es gibt andere Alternativen). Wir beobachten auch, dass die beiden Vergleichsmethoden ( == und equals ) beide true .

Dieses Verhalten ändert sich, wenn wir andere Werte wählen:

Integer int2_1 = Integer.valueOf("1000");
Integer int2_2 = Integer.valueOf(1000);

System.out.println("int2_1 == int2_2: " + (int2_1 == int2_2));          // false
System.out.println("int2_1 equals int2_2: " + int2_1.equals(int2_2));   // true

In diesem Fall liefert nur der equals das korrekte Ergebnis.

Der Grund für diesen Verhaltensunterschied ist, dass die JVM einen Cache mit Integer Objekten für den Bereich von -128 bis 127 verwaltet. (Der obere Wert kann mit der Systemeigenschaft "java.lang.Integer.IntegerCache.high" oder ") überschrieben werden JVM-Argument "-XX: AutoBoxCacheMax = Größe"). Bei Werten in diesem Bereich gibt Integer.valueOf() den zwischengespeicherten Wert zurück, anstatt einen neuen zu erstellen.

Daher haben im ersten Beispiel die Integer.valueOf(1) und Integer.valueOf("1") die gleiche zwischengespeicherte Integer Instanz zurückgegeben. Im zweiten Beispiel dagegen haben Integer.valueOf(1000) und Integer.valueOf("1000") sowohl neue Integer Objekte erstellt als auch zurückgegeben.

Der Operator == für Referenztypen prüft auf Referenzgleichheit (dh dasselbe Objekt). Daher ist im ersten Beispiel int1_1 == int1_2 true da die Referenzen gleich sind. Im zweiten Beispiel ist int2_1 == int2_2 falsch, da die Referenzen unterschiedlich sind.

Fallstricke: Vergessen, Ressourcen freizusetzen

Jedes Mal, wenn ein Programm eine Ressource öffnet, z. B. eine Datei oder eine Netzwerkverbindung, ist es wichtig, die Ressource freizugeben, sobald Sie mit der Verwendung fertig sind. Ähnliche Vorsicht ist geboten, wenn während des Betriebs solcher Ressourcen eine Ausnahme ausgelöst wird. Man könnte argumentieren, dass FileInputStream über einen Finalizer verfügt , der die close() -Methode für ein Garbage Collection-Ereignis aufruft. Da wir jedoch nicht sicher sind, wann ein Speicherbereinigungszyklus beginnt, kann der Eingabestrom Computerressourcen auf unbestimmte Zeit beanspruchen. Die Ressource muss in einem finally Abschnitt eines try-catch-Blocks geschlossen werden:

Java SE 7
private static void printFileJava6() throws IOException {
    FileInputStream input;
    try {
        input = new FileInputStream("file.txt");
        int data = input.read();
        while (data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    } finally {
        if (input != null) {
            input.close();
        }
    }
}

Seit Java 7 gibt es eine wirklich nützliche und übersichtliche Anweisung, die speziell für diesen Fall in Java 7 eingeführt wird und Try-with-resources heißt:

Java SE 7
private static void printFileJava7() throws IOException {
    try (FileInputStream input = new FileInputStream("file.txt")) {
        int data = input.read();
        while (data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    }
}

Die try-with-resources- Anweisung kann mit jedem Objekt verwendet werden, das die Schnittstelle Closeable oder AutoCloseable implementiert. Dadurch wird sichergestellt, dass jede Ressource am Ende der Anweisung geschlossen wird. Der Unterschied zwischen den beiden Schnittstellen besteht darin, dass die close() Methode von Closeable eine IOException die auf irgendeine Weise behandelt werden muss.

In Fällen, in denen die Ressource bereits geöffnet wurde, aber nach der Verwendung sicher geschlossen werden soll, kann sie einer lokalen Variablen innerhalb der try-with-resources zugewiesen werden

Java SE 7
private static void printFileJava7(InputStream extResource) throws IOException {
    try (InputStream input = extResource) {
        ... //access resource
    }
}

Die lokale Ressourcenvariable, die im try-with-resources-Konstruktor erstellt wird, ist effektiv final.

Pitfall: Speicherlecks

Java verwaltet den Speicher automatisch. Sie müssen den Speicher nicht manuell freigeben. Der Speicher eines Objekts auf dem Heap kann von einem Speicherbereiniger freigegeben werden, wenn das Objekt von einem aktiven Thread nicht mehr erreichbar ist .

Sie können jedoch verhindern, dass Speicher freigegeben wird, indem Sie zulassen, dass nicht mehr benötigte Objekte erreichbar sind. Unabhängig davon, ob Sie dies als Speicherverlust oder als Speicherverpackung bezeichnen, das Ergebnis ist das gleiche - eine unnötige Erhöhung des zugewiesenen Speichers.

Speicherverluste in Java können auf verschiedene Arten auftreten. Der häufigste Grund sind jedoch permanente Objektverweise, da der Garbage Collector Objekte nicht aus dem Heap entfernen kann, solange noch Verweise darauf vorhanden sind.

Statische Felder

Sie können einen solchen Verweis erstellen, indem Sie eine Klasse mit einem static Feld definieren, das eine Sammlung von Objekten enthält, und vergessen, das static Feld auf null zu setzen, nachdem die Sammlung nicht mehr benötigt wird. static Felder werden als GC-Roots betrachtet und niemals gesammelt. Ein weiteres Problem sind Lecks im Nicht-Heap-Speicher, wenn JNI verwendet wird.

Classloader-Leck

Der heimtückischste Speicherleck ist jedoch das Classloader-Leck . Ein Classloader enthält einen Verweis auf jede geladene Klasse, und jede Klasse enthält einen Verweis auf ihren Classloader. Jedes Objekt enthält auch einen Verweis auf seine Klasse. Wenn also selbst ein einzelnes Objekt einer von einem Classloader geladenen Klasse kein Müll ist, kann keine einzige Klasse abgerufen werden, die dieser Classloader geladen hat. Da jede Klasse auch auf ihre statischen Felder verweist, können sie auch nicht erfasst werden.

Akkumulationsleck Das Beispiel für Akkumulationsleck könnte folgendermaßen aussehen:

final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    BigDecimal number = numbers.peekLast();
    if (number != null && number.remainder(divisor).byteValue() == 0) {
        System.out.println("Number: " + number);
        System.out.println("Deque size: " + numbers.size());
    }
}, 10, 10, TimeUnit.MILLISECONDS);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    numbers.add(new BigDecimal(System.currentTimeMillis()));
}, 10, 10, TimeUnit.MILLISECONDS);

try {
    scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
    e.printStackTrace();
}

In diesem Beispiel werden zwei geplante Aufgaben erstellt. Die erste Aufgabe übernimmt die letzte Nummer von einer Deque- numbers , und wenn die Nummer durch 51 teilbar ist, werden die Nummer und die Deque-Größe gedruckt. Die zweite Aufgabe setzt Zahlen in den Deque. Beide Tasks werden mit einer festen Rate geplant und alle 10 ms ausgeführt.

Wenn der Code ausgeführt wird, werden Sie feststellen, dass die Größe des Deque ständig zunimmt. Dies führt letztendlich dazu, dass der Deque mit Objekten gefüllt wird, die den gesamten verfügbaren Heapspeicher beanspruchen.

Um dies zu verhindern und gleichzeitig die Semantik dieses Programms zu erhalten, können wir eine andere Methode verwenden, um Zahlen aus dem Deque zu pollLast : pollLast . Im Gegensatz zur Methode peekLast gibt pollLast das Element zurück und entfernt es aus dem Deque, während peekLast nur das letzte Element zurückgibt.

Fallstricke: Verwenden von == zum Vergleichen von Zeichenketten

Ein häufiger Fehler für Java-Anfänger ist die Verwendung des Operators == , um zu testen, ob zwei Zeichenfolgen gleich sind. Zum Beispiel:

public class Hello {
    public static void main(String[] args) {
        if (args.length > 0) {
            if (args[0] == "hello") {
                System.out.println("Hello back to you");
            } else {
                System.out.println("Are you feeling grumpy today?");
            }
        }
    }
}

Das obige Programm soll das erste Befehlszeilenargument testen und verschiedene Meldungen drucken, wenn es nicht das Wort "Hallo" ist. Aber das Problem ist, dass es nicht funktioniert. Dieses Programm wird "Fühlen Sie sich heute mürrisch?" egal, was das erste Kommandozeilenargument ist.

In diesem Fall wird der String "Hallo" in den String-Pool eingefügt, während der String args [0] sich auf dem Heap befindet. Das bedeutet, dass es zwei Objekte gibt, die dasselbe Literal darstellen, und jedes mit seiner Referenz. Da == auf Referenzen prüft und nicht auf tatsächliche Gleichheit, führt der Vergleich meistens zu einem falschen Ergebnis. Dies bedeutet nicht, dass es immer so ist.

Wenn Sie == zum Testen von Strings verwenden, testen Sie tatsächlich, ob zwei String Objekte dasselbe Java-Objekt sind. Leider bedeutet dies nicht die Gleichheit von Zeichenfolgen in Java. In der Tat ist die korrekte Methode zum Testen von Zeichenfolgen die Verwendung der Methode equals(Object) . Für ein Paar von Strings möchten wir normalerweise testen, ob sie aus denselben Zeichen in derselben Reihenfolge bestehen.

public class Hello2 {
    public static void main(String[] args) {
        if (args.length > 0) {
            if (args[0].equals("hello")) {
                System.out.println("Hello back to you");
            } else {
                System.out.println("Are you feeling grumpy today?");
            }
        }
    }
}

Aber es wird tatsächlich schlimmer. Das Problem ist, dass == unter Umständen die erwartete Antwort gibt. Zum Beispiel

public class Test1 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        if (s1 == s2) {
            System.out.println("same");
        } else {
            System.out.println("different");
        }
    }
}

Interessanterweise wird dies "gleich" gedruckt, auch wenn wir die Zeichenketten falsch testen. Warum das? Da in der Java-Sprachspezifikation (Abschnitt 3.10.5: String-Literale) festgelegt ist, dass zwei aus denselben Zeichen bestehende Zeichenketten >> Literale << tatsächlich von demselben Java-Objekt dargestellt werden. Daher ist der Test == für gleiche Literale wahr. (Die String-Literale werden "interniert" und beim Laden Ihres Codes zu einem gemeinsam genutzten "String-Pool" hinzugefügt. Dies ist jedoch tatsächlich ein Implementierungsdetail.)

Die Java-Sprachspezifikation schreibt außerdem Folgendes vor: Wenn Sie einen Konstantenausdruck zur Kompilierungszeit haben, der zwei String-Literale verkettet, entspricht dies einem einzelnen Literal. Somit:

    public class Test1 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        String s3 = " mum";
        if (s1 == s2) {
            System.out.println("1. same");
        } else {
            System.out.println("1. different");
        }
        if (s1 + s3 == "hello mum") {
            System.out.println("2. same");
        } else {
            System.out.println("2. different");
        }
    }
}

Dies gibt "1. gleich" und "2. anders" aus. Im ersten Fall wird der Ausdruck + zur Kompilierzeit ausgewertet und wir vergleichen ein String Objekt mit sich selbst. Im zweiten Fall wird es zur Laufzeit ausgewertet und wir vergleichen zwei verschiedene String Objekte

Zusammenfassend ist die Verwendung von == zum Testen von Zeichenfolgen in Java fast immer falsch, es kann jedoch nicht garantiert werden, dass sie die falsche Antwort gibt.

Fallstricke: Testen Sie eine Datei, bevor Sie versuchen, sie zu öffnen.

Einige Personen empfehlen, dass Sie verschiedene Tests auf eine Datei anwenden, bevor Sie versuchen, sie zu öffnen, um eine bessere Diagnose zu bieten oder um Ausnahmen zu vermeiden. Diese Methode versucht beispielsweise zu prüfen, ob der path einer lesbaren Datei entspricht:

public static File getValidatedFile(String path) throws IOException {
    File f = new File(path);
    if (!f.exists()) throw new IOException("Error: not found: " + path);
    if (!f.isFile()) throw new IOException("Error: Is a directory: " + path);
    if (!f.canRead()) throw new IOException("Error: cannot read file: " + path);
    return f;
}

Sie können die obige Methode folgendermaßen verwenden:

File f = null;
try {
    f = getValidatedFile("somefile");
} catch (IOException ex) {
    System.err.println(ex.getMessage());
    return;
}
try (InputStream is = new FileInputStream(file)) {
    // Read data etc.
}

Das erste Problem liegt in der Signatur für FileInputStream(File) da der Compiler weiterhin darauf besteht, IOException hier zu fangen, oder weiter oben im Stack.

Das zweite Problem besteht darin, dass durch getValidatedFile durchgeführte getValidatedFile nicht garantieren, dass FileInputStream erfolgreich ist.

  • Racebedingungen: Ein anderer Thread oder ein separater Prozess könnte die Datei umbenennen, die Datei löschen oder den Lesezugriff entfernen, nachdem die getValidatedFile zurückgegeben wurde. IOException würde zu einer "einfachen" IOException ohne die benutzerdefinierte Nachricht führen.

  • Es gibt Randfälle, die von diesen Tests nicht abgedeckt werden. Auf einem System mit SELinux im Modus "Erzwingen" kann der Versuch, eine Datei zu lesen, zum Beispiel fehlschlagen, obwohl canRead() true canRead() .

Das dritte Problem ist, dass die Tests ineffizient sind. Zum Beispiel führen die Aufrufe exists , isFile und canRead jeweils einen Syscall aus , um die erforderliche Prüfung durchzuführen. Anschließend wird ein weiterer Systemaufruf ausgeführt, um die Datei zu öffnen. Dabei werden die gleichen Überprüfungen im Hintergrund wiederholt.

Kurz gesagt, Methoden wie getValidatedFile sind fehlgeleitet. Versuchen Sie einfach, die Datei zu öffnen und die Ausnahme zu behandeln:

try (InputStream is = new FileInputStream("somefile")) {
    // Read data etc.
} catch (IOException ex) {
    System.err.println("IO Error processing 'somefile': " + ex.getMessage());
    return;
}

Wenn Sie IO-Fehler beim Öffnen und Lesen unterscheiden möchten, können Sie ein verschachteltes Try / Catch verwenden. Wenn Sie eine bessere Diagnostik für offene Ausfälle produzieren wollte, könnte führen Sie die exists , isFile und canRead Kontrollen im Handler.

Fallstricke: Variablen als Objekte betrachten

Keine Java-Variable repräsentiert ein Objekt.

String foo;   // NOT AN OBJECT

Kein Java-Array enthält Objekte.

String bar[] = new String[100];  // No member is an object.

Wenn Sie Variablen fälschlicherweise als Objekte betrachten, wird Sie das tatsächliche Verhalten der Java-Sprache überraschen.

  • Bei Java-Variablen, die einen primitiven Typ haben (z. B. int oder float ), enthält die Variable eine Kopie des Werts. Alle Kopien eines primitiven Wertes sind nicht unterscheidbar. dh es gibt nur einen int Wert für die Nummer eins. Primitive Werte sind keine Objekte und sie verhalten sich nicht wie Objekte.

  • Bei Java-Variablen, die einen Referenztyp (entweder einen Klassen- oder einen Arraytyp) haben, enthält die Variable eine Referenz. Alle Exemplare einer Referenz sind nicht unterscheidbar. Verweise können auf Objekte zeigen oder sie können null was bedeutet, dass sie auf kein Objekt zeigen. Sie sind jedoch keine Objekte und verhalten sich nicht wie Objekte.

Variablen sind in beiden Fällen keine Objekte und enthalten in beiden Fällen keine Objekte. Sie können Verweise auf Objekte enthalten, sagen aber etwas anderes aus.

Beispielklasse

In den folgenden Beispielen wird diese Klasse verwendet, die einen Punkt im 2D-Raum darstellt.

public final class MutableLocation {
   public int x;
   public int y;

   public MutableLocation(int x, int y) {
       this.x = x;
       this.y = y;
   }

   public boolean equals(Object other) {
       if (!(other instanceof MutableLocation) {
           return false;
       }
       MutableLocation that = (MutableLocation) other;
       return this.x == that.x && this.y == that.y;
   }
}

Eine Instanz dieser Klasse ist ein Objekt, das zwei Felder x und y , die den Typ int .

Wir können viele Instanzen der MutableLocation Klasse haben. Einige stellen die gleichen Positionen im 2D-Raum dar; dh die jeweiligen Werte von x und y stimmen überein. Andere repräsentieren verschiedene Standorte.

Mehrere Variablen können auf dasselbe Objekt zeigen

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

In der oben haben wir drei Variablen erklärt here , there und elsewhere , die Verweise auf halten können MutableLocation Objekte.

Wenn Sie diese Variablen (falsch) als Objekte betrachten, werden Sie die Anweisungen wahrscheinlich falsch interpretieren:

  1. Kopieren Sie den Ort "[1, 2]" here
  2. Kopieren Sie den Ort "[1, 2]" there
  3. Kopieren Sie den Ort "[1, 2]" an eine elsewhere

Daraus lässt sich wahrscheinlich schließen, dass wir in den drei Variablen drei unabhängige Objekte haben. In der Tat gibt es nur zwei Objekte, die von den oben genannten erstellt wurden. Die Variablen here und there beziehen sich tatsächlich auf dasselbe Objekt.

Wir können das demonstrieren. Angenommen die Variablendeklarationen wie oben:

System.out.println("BEFORE: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);
here.x = 42;
System.out.println("AFTER: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);

Dadurch wird Folgendes ausgegeben:

BEFORE: here.x is 1, there.x is 1, elsewhere.x is 1
AFTER: here.x is 42, there.x is 42, elsewhere.x is 1

Wir haben here.x einen neuen Wert here.x und der Wert, den wir über dort sehen, wurde there.x . Sie beziehen sich auf dasselbe Objekt. Der Wert, den wir über elsewhere.x hat sich jedoch nicht geändert, weshalb elsewhere auf ein anderes Objekt verweisen muss.

Wenn eine Variable ein Objekt ist, dann ist die Zuordnung here.x = 42 würde mich nicht ändern there.x .

Der Gleichheitsoperator testet NICHT, ob zwei Objekte gleich sind

Wenn Sie den Gleichheitsoperator ( == ) auf Referenzwerte anwenden, wird geprüft, ob sich die Werte auf dasselbe Objekt beziehen. Es wird nicht getestet, ob zwei (verschiedene) Objekte im intuitiven Sinne "gleich" sind.

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

 if (here == there) {
     System.out.println("here is there");
 }
 if (here == elsewhere) {
     System.out.println("here is elsewhere");
 }

Dies wird gedruckt "Hier ist da", aber es wird nicht gedruckt "Hier ist woanders". (Die Referenzen here und elsewhere beziehen sich auf zwei verschiedene Objekte.)

Wenn wir dagegen die equals(Object) implementierte equals(Object) -Methode aufrufen, testen wir, ob zwei MutableLocation Instanzen MutableLocation Position haben.

 if (here.equals(there)) {
     System.out.println("here equals there");
 }
 if (here.equals(elsewhere)) {
     System.out.println("here equals elsewhere");
 }

Dadurch werden beide Meldungen gedruckt. Insbesondere gibt here.equals(elsewhere) true zurück true da die semantischen Kriterien, die wir für die Gleichheit zweier MutableLocation Objekte ausgewählt haben, erfüllt wurden.

Methodenaufrufe übergeben KEINE Objekte

Java-Methodenaufrufe verwenden Übergabe nach Wert 1 , um Argumente zu übergeben und ein Ergebnis zurückzugeben.

Wenn Sie einen Verweiswert an eine Methode übergeben, übergeben Sie tatsächlich einen Verweis auf ein Objekt anhand des Werts . Dies bedeutet, dass eine Kopie des Objektverweises erstellt wird.

Solange beide Objektverweise immer noch auf dasselbe Objekt zeigen, können Sie das Objekt von einem der beiden Verweise aus ändern. Dies führt bei einigen zu Verwirrung.

Allerdings sind vorbei Sie nicht ein Objekt als Verweis 2. Der Unterschied ist, dass, wenn die Objektreferenzkopie so geändert wird, dass sie auf ein anderes Objekt zeigt, die ursprüngliche Objektreferenz weiterhin auf das Originalobjekt zeigt.

void f(MutableLocation foo) {  
    foo = new MutableLocation(3, 4);   // Point local foo at a different object.
}

void g() {
    MutableLocation foo = MutableLocation(1, 2);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 1".
}

Sie übergeben auch keine Kopie des Objekts.

void f(MutableLocation foo) {  
    foo.x = 42;
}

void g() {
    MutableLocation foo = new MutableLocation(0, 0);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 42"
}

1 - In Sprachen wie Python und Ruby wird der Begriff "Weitergabe durch Freigeben" für "Wertübergabe" eines Objekts / einer Referenz bevorzugt.

2 - Der Begriff "Referenzübergabe" oder "Referenzabruf" hat in der Programmiersprache eine ganz bestimmte Bedeutung. In der Tat bedeutet dies, dass Sie die Adresse einer Variablen oder eines Array-Elements übergeben . Wenn die aufgerufene Methode dem Formalargument einen neuen Wert zuweist, ändert sie den Wert in der ursprünglichen Variablen. Java unterstützt dies nicht. Eine ausführlichere Beschreibung der verschiedenen Mechanismen für die Übergabe von Parametern finden Sie unter https://en.wikipedia.org/wiki/Evaluation_strategy .

Pitfall: Kombination von Zuordnung und Nebenwirkungen

Gelegentlich sehen wir StackOverflow Java-Fragen (und C- oder C ++ - Fragen), in denen Folgendes gefragt wird:

i += a[i++] + b[i--];

wird für einige bekannte Anfangszustände von i , a und b zu ... ausgewertet.

Allgemein gesagt:

  • Für Java ist die Antwort immer 1 angegeben, aber nicht naheliegend und oft schwer zu verstehen
  • Für C und C ++ ist die Antwort oft nicht spezifiziert.

Solche Beispiele werden häufig in Prüfungen oder Vorstellungsgesprächen verwendet, um zu sehen, ob der Student oder der Interviewpartner versteht, wie die Ausdrucksbewertung in der Java-Programmiersprache wirklich funktioniert. Dies ist wohl als "Test des Wissens" legitim, aber das bedeutet nicht, dass Sie dies in einem echten Programm tun sollten.

Zur Veranschaulichung ist das folgende, scheinbar einfache Beispiel in StackOverflow-Fragen (wie dieser ) einige Male erschienen. In manchen Fällen erscheint dies als echter Fehler in einem Code.

int a = 1;
a = a++;
System.out.println(a);    // What does this print.

Die meisten Programmierer (einschließlich Java-Experten), die diese Anweisungen schnell lesen, würden sagen, dass sie 2 ausgeben. Tatsächlich gibt es 1 . Für eine detaillierte Erklärung der Gründe lesen Sie bitte diese Antwort .

Der wahre Einstieg aus diesem und ähnlichen Beispielen ist jedoch, dass jede Java-Anweisung, die dieselbe Variable sowohl als auch Nebeneffekte zuordnet, im besten Fall schwer zu verstehen ist und im schlimmsten Fall irreführend ist. Sie sollten das Schreiben von Code vermeiden.


1 - mögliche Probleme mit dem Java-Speichermodell, falls die Variablen oder Objekte für andere Threads sichtbar sind.

Pitfall: Dass String nicht eine unveränderliche Klasse ist, ist nicht bekannt

Neue Java-Programmierer vergessen häufig, dass die Java- String Klasse nicht veränderbar ist, oder sie verstehen nicht ganz. Dies führt zu Problemen wie im folgenden Beispiel:

public class Shout {
    public static void main(String[] args) {
        for (String s : args) {
            s.toUpperCase();
            System.out.print(s);
            System.out.print(" ");
        }
        System.out.println();
    }
}

Der obige Code soll Befehlszeilenargumente in Großbuchstaben drucken. Leider funktioniert es nicht, der Fall der Argumente wird nicht geändert. Das Problem ist diese Aussage:

s.toUpperCase();

Möglicherweise denken Sie, dass der Aufruf von toUpperCase() s in eine Großbuchstabenzeichenfolge ändert. Tut es nicht Es kann nicht! String Objekte sind unveränderlich. Sie können nicht geändert werden.

In Wirklichkeit ist die toUpperCase() zurückgibt Methode ein String - Objekt , das eine Großversion der ist String , die Sie nennen es auf. Dies wird wahrscheinlich ein neues String Objekt sein, aber wenn s bereits alle Großbuchstaben war, könnte das Ergebnis die vorhandene Zeichenfolge sein.

Um diese Methode effektiv verwenden zu können, müssen Sie das vom Methodenaufruf zurückgegebene Objekt verwenden. zum Beispiel:

s = s.toUpperCase();

Tatsächlich gilt die Regel "Zeichenfolgen ändert sich nicht" für alle String Methoden. Wenn Sie sich daran erinnern, können Sie eine ganze Kategorie von Anfängerfehlern vermeiden.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow