Ricerca…


introduzione

Questo argomento delinea alcuni degli errori più comuni fatti dai principianti in Java.

Ciò include qualsiasi errore comune nell'uso del linguaggio Java o comprensione dell'ambiente di runtime.

Gli errori associati a specifiche API possono essere descritti in argomenti specifici di tali API. Le stringhe sono un caso speciale; sono coperti nella specifica del linguaggio Java. Dettagli diversi dagli errori comuni possono essere descritti in questo argomento su Stringhe .

Pitfall: usare == per confrontare oggetti di wrapper primitivi come Integer

(Questo errore si applica ugualmente a tutti i tipi di wrapper primitivi, ma lo illustreremo per Integer e int .)

Quando si lavora con oggetti Integer , si è tentati di usare == per confrontare i valori, perché è ciò che si farebbe con i valori int . E in alcuni casi questo sembrerà funzionare:

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

Qui abbiamo creato due oggetti Integer con il valore 1 e li abbiamo confrontati (in questo caso ne abbiamo creati uno da una String e uno da un letterale int . Ci sono altre alternative). Inoltre, osserviamo che i due metodi di confronto ( == e equals ) rendono entrambi true .

Questo comportamento cambia quando scegliamo valori diversi:

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 questo caso, solo il confronto tra equals produce il risultato corretto.

La ragione di questa differenza di comportamento è che la JVM conserva una cache di oggetti Integer per l'intervallo da -128 a 127. (Il valore superiore può essere sostituito con la proprietà di sistema "java.lang.Integer.IntegerCache.high" o Argomento JVM "-XX: AutoBoxCacheMax = size"). Per i valori in questo intervallo, Integer.valueOf() restituirà il valore memorizzato nella cache anziché crearne uno nuovo.

Pertanto, nel primo esempio le chiamate Integer.valueOf(1) e Integer.valueOf("1") restituito la stessa istanza di Integer memorizzata nella cache. Al contrario, nel secondo esempio Integer.valueOf(1000) e Integer.valueOf("1000") entrambi creato e restituito nuovi oggetti Integer .

L'operatore == per i tipi di riferimento verifica l'uguaglianza di riferimento (ovvero lo stesso oggetto). Pertanto, nel primo esempio int1_1 == int1_2 è true perché i riferimenti sono gli stessi. Nel secondo esempio int2_1 == int2_2 è falso perché i riferimenti sono diversi.

Trappola: dimenticando di liberare risorse

Ogni volta che un programma apre una risorsa, come un file o una connessione di rete, è importante liberare la risorsa una volta che hai finito di usarla. Un'analoga cautela dovrebbe essere presa se dovessero essere lanciate delle eccezioni durante le operazioni su tali risorse. Si potrebbe obiettare che FileInputStream ha un finalizzatore che richiama il metodo close() su un evento di garbage collection; tuttavia, poiché non possiamo essere sicuri quando verrà avviato un ciclo di garbage collection, lo stream di input può consumare risorse del computer per un periodo di tempo indefinito. La risorsa deve essere chiuso in un finally sezione di un blocco try-catch:

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();
        }
    }
}

A partire da Java 7 c'è una dichiarazione veramente utile e chiara introdotta in Java 7, in particolare per questo caso, chiamata try-with-resources:

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();
        }
    }
}

L'istruzione try-with-resources può essere utilizzata con qualsiasi oggetto che implementa l'interfaccia Closeable o AutoCloseable . Assicura che ogni risorsa sia chiusa entro la fine dell'istruzione. La differenza tra le due interfacce è che il metodo close() di Closeable genera una IOException che deve essere gestita in qualche modo.

Nei casi in cui la risorsa è già stata aperta ma dovrebbe essere chiusa in sicurezza dopo l'uso, è possibile assegnarla a una variabile locale all'interno delle risorse try-with

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

La variabile di risorsa locale creata nel costruttore try-with-resources è effettivamente definitiva.

Trappola: perdite di memoria

Java gestisce automaticamente la memoria. Non è necessario liberare memoria manualmente. La memoria di un oggetto sull'heap può essere liberata da un garbage collector quando l'oggetto non è più raggiungibile da un thread attivo.

Tuttavia, è possibile impedire la liberazione della memoria, consentendo agli oggetti di essere raggiungibili e non più necessari. Indipendentemente dal fatto che si tratti di una perdita di memoria o di un packratting della memoria, il risultato è lo stesso: un aumento non necessario della memoria allocata.

Le perdite di memoria in Java possono accadere in vari modi, ma la ragione più comune sono i riferimenti agli oggetti eterni, perché il garbage collector non può rimuovere oggetti dall'heap mentre ci sono ancora riferimenti a essi.

Campi statici

È possibile creare tale riferimento definendo la classe con un campo static contenente una certa raccolta di oggetti e dimenticando di impostare il campo static su null dopo che la raccolta non è più necessaria. static campi static sono considerati root GC e non vengono mai raccolti. Un altro problema sono le perdite nella memoria non heap quando viene utilizzato JNI .

Perdita di Classloader

Di gran lunga, tuttavia, il tipo più insidioso di perdita di memoria è la perdita di classloader . Un classloader contiene un riferimento a ogni classe che ha caricato e ogni classe contiene un riferimento al suo programma di caricamento classi. Ogni oggetto contiene anche un riferimento alla sua classe. Pertanto, se anche un singolo oggetto di una classe caricato da un classloader non è garbage, non è possibile raccogliere una singola classe caricata da quel classloader. Poiché ogni classe fa riferimento anche ai suoi campi statici, non possono essere raccolti neanche.

Perdita di accumulo L'esempio di perdita di accumulo potrebbe essere simile al seguente:

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();
}

Questo esempio crea due attività pianificate. La prima attività prende l'ultimo numero da un deque chiamato numbers e, se il numero è divisibile per 51, stampa il numero e la dimensione della deque. Il secondo compito mette i numeri nella coda. Entrambe le attività sono pianificate a una velocità fissa e vengono eseguite ogni 10 ms.

Se il codice viene eseguito, vedrai che la dimensione della deque aumenta in modo permanente. Questo alla fine farà riempire la deque di oggetti che consumano tutta la memoria heap disponibile.

Per evitare ciò preservando la semantica di questo programma, possiamo usare un metodo diverso per prendere i numeri dal deque: pollLast . Contrariamente al metodo peekLast , pollLast restituisce l'elemento e lo rimuove dalla deque mentre peekLast restituisce solo l'ultimo elemento.

Pitfall: usare == per confrontare le stringhe

Un errore comune per i principianti Java è quello di utilizzare l'operatore == per verificare se due stringhe sono uguali. Per esempio:

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?");
            }
        }
    }
}

Il programma di cui sopra dovrebbe testare il primo argomento della riga di comando e stampare diversi messaggi quando non è la parola "ciao". Ma il problema è che non funzionerà. Il programma produrrà "Ti senti irritato oggi?" non importa quale sia il primo argomento della riga di comando.

In questo caso particolare, la String "ciao" viene inserita nel pool di stringhe mentre gli argomenti di String [0] risiedono nell'heap. Ciò significa che ci sono due oggetti che rappresentano lo stesso letterale, ciascuno con il suo riferimento. Poiché == verifica i riferimenti, non l'uguaglianza effettiva, il confronto produrrà un falso il più delle volte. Questo non significa che lo farà sempre.

Quando si usa == per testare le stringhe, ciò che si sta effettivamente testando è se due oggetti String sono lo stesso oggetto Java. Sfortunatamente, non è quello che significa l'uguaglianza delle stringhe in Java. In effetti, il modo corretto per testare le stringhe è utilizzare il metodo equals(Object) . Per un paio di stringhe, di solito vogliamo testare se consistono degli stessi caratteri nello stesso ordine.

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?");
            }
        }
    }
}

Ma in realtà peggiora. Il problema è che == darà la risposta attesa in alcune circostanze. Per esempio

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");
        }
    }
}

È interessante notare che questo verrà stampato "uguale", anche se stiamo testando le stringhe nel modo sbagliato. Perché? Poiché la specifica del linguaggio Java (Sezione 3.10.5: String Literals) stabilisce che qualsiasi stringa di due >> valori letterali << costituiti dagli stessi caratteri sarà effettivamente rappresentata dallo stesso oggetto Java. Quindi, il test == darà true per letterali uguali. (I valori letterali delle stringhe sono "internati" e aggiunti a un "pool di stringhe" condiviso quando viene caricato il codice, ma questo è in realtà un dettaglio di implementazione.)

Per aggiungere confusione, la specifica del linguaggio Java stabilisce anche che quando si dispone di un'espressione costante in fase di compilazione che concatena due valori letterali stringa, è equivalente a un singolo valore letterale. Così:

    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");
        }
    }
}

Questo produrrà "1. stesso" e "2. diverso". Nel primo caso, l'espressione + viene valutata al momento della compilazione e confrontiamo un oggetto String con se stesso. Nel secondo caso, viene valutato in fase di esecuzione e confrontiamo due diversi oggetti String

In sintesi, usare == per testare le stringhe in Java è quasi sempre errato, ma non è garantito dare la risposta sbagliata.

Pitfall: testare un file prima di tentare di aprirlo.

Alcune persone consigliano di applicare vari test a un file prima di tentare di aprirlo per fornire una migliore diagnostica o evitare di gestire eccezioni. Ad esempio, questo metodo tenta di verificare se il path corrisponde a un file leggibile:

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;
}

Potresti usare il metodo sopra come questo:

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.
}

Il primo problema è nella firma per FileInputStream(File) perché il compilatore continuerà a insistere sul fatto che intercettiamo IOException qui, o più in alto nello stack.

Il secondo problema è che i controlli eseguiti da getValidatedFile non garantiscono il successo di FileInputStream .

  • Condizioni di gara: un altro thread o un processo separato potrebbe rinominare il file, eliminare il file o rimuovere l'accesso in lettura dopo il ritorno di getValidatedFile . Ciò porterebbe a una IOException "semplice" senza il messaggio personalizzato.

  • Ci sono casi limite non coperti da questi test. Ad esempio, su un sistema con SELinux in modalità "enforcing", un tentativo di leggere un file può fallire malgrado canRead() restituisce true .

Il terzo problema è che i test sono inefficienti. Ad esempio, il exists , isFile e canRead chiamate saranno ogni effettuare una chiamata di sistema per eseguire il controllo desiderato. Viene quindi eseguito un altro syscall per aprire il file, che ripete gli stessi controlli dietro le quinte.

In breve, metodi come getValidatedFile sono fuorviati. È meglio semplicemente provare ad aprire il file e gestire l'eccezione:

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

Se si desidera distinguere gli errori IO generati durante l'apertura e la lettura, è possibile utilizzare un try / catch annidato. Se si voleva produrre diagnostiche migliori per i fallimenti aperti, è possibile eseguire le exists , isFile e canRead controlli nel gestore.

Trappola: pensare alle variabili come oggetti

Nessuna variabile Java rappresenta un oggetto.

String foo;   // NOT AN OBJECT

Nemmeno una matrice Java contiene oggetti.

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

Se pensi erroneamente alle variabili come oggetti, il comportamento effettivo del linguaggio Java ti sorprenderà.

  • Per le variabili Java che hanno un tipo primitivo (come int o float ) la variabile contiene una copia del valore. Tutte le copie di un valore primitivo sono indistinguibili; cioè c'è solo un valore int per il numero uno. I valori primitivi non sono oggetti e non si comportano come oggetti.

  • Per le variabili Java che hanno un tipo di riferimento (una classe o un tipo di matrice) la variabile contiene un riferimento. Tutte le copie di un riferimento sono indistinguibili. I riferimenti possono indicare oggetti, oppure possono essere null che significa che non puntano a nessun oggetto. Tuttavia, non sono oggetti e non si comportano come oggetti.

Le variabili non sono oggetti in entrambi i casi e non contengono oggetti in entrambi i casi. Possono contenere riferimenti a oggetti , ma ciò significa qualcosa di diverso.

Classe di esempio

Gli esempi che seguono utilizzano questa classe, che rappresenta un punto nello spazio 2D.

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;
   }
}

Un esempio di questa classe è un oggetto che ha due campi x ed y che hanno il tipo int .

Possiamo avere molte istanze della classe MutableLocation . Alcuni rappresenteranno le stesse posizioni nello spazio 2D; ossia i rispettivi valori di x ed y corrisponderanno. Altri rappresenteranno luoghi diversi.

Più variabili possono puntare allo stesso oggetto

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

In quanto sopra, abbiamo dichiarato here tre variabili, there e elsewhere che possono contenere riferimenti a oggetti MutableLocation .

Se pensate (erroneamente) a queste variabili come ad oggetti, probabilmente avrete erroneamente interpretato le affermazioni come dicendo:

  1. Copia la posizione "[1, 2]" here
  2. Copia la posizione "[1, 2]" a there
  3. Copia la posizione "[1, 2]" in elsewhere

Da ciò, è probabile dedurre che abbiamo tre oggetti indipendenti nelle tre variabili. In effetti ci sono solo due oggetti creati da quanto sopra. Le variabili here e there riferiscono effettivamente allo stesso oggetto.

Possiamo dimostrarlo. Assumendo le dichiarazioni variabili come sopra:

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);

Ciò produrrà il seguente:

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

Abbiamo assegnato un nuovo valore a here.x e ha cambiato il valore che vediamo tramite there.x . Si riferiscono allo stesso oggetto. Ma il valore che vediamo tramite elsewhere.x non è cambiato, quindi elsewhere deve fare riferimento a un oggetto diverso.

Se una variabile era un oggetto, allora l'assegnazione here.x = 42 non cambierebbe there.x

L'operatore di uguaglianza NON verifica che due oggetti siano uguali

L'applicazione dell'operatore di uguaglianza ( == ) ai valori di riferimento verifica se i valori si riferiscono allo stesso oggetto. Esso non verifica se due (differenti) oggetti sono "uguali" nel senso intuitivo.

 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");
 }

Questo stamperà "qui è lì", ma non stamperà "qui è altrove". (I riferimenti here e elsewhere sono per due oggetti distinti).

Al contrario, se chiamiamo il metodo equals(Object) che abbiamo implementato in precedenza, MutableLocation se due istanze di MutableLocation hanno una posizione uguale.

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

Questo stamperà entrambi i messaggi. In particolare, here.equals(elsewhere) restituisce true perché i criteri semantici che abbiamo scelto per l'uguaglianza di due oggetti MutableLocation sono stati soddisfatti.

Le chiamate di metodo NON passano affatto gli oggetti

Le chiamate al metodo Java utilizzano il valore 1 per passare gli argomenti e restituiscono un risultato.

Quando si passa un valore di riferimento a un metodo, si passa effettivamente un riferimento a un oggetto per valore , il che significa che sta creando una copia del riferimento all'oggetto.

Finché entrambi i riferimenti agli oggetti continuano a puntare allo stesso oggetto, puoi modificare quell'oggetto da entrambi i riferimenti, e questo è ciò che causa confusione per alcuni.

Tuttavia, non sta passando un oggetto per riferimento 2. La distinzione è che se la copia di riferimento dell'oggetto viene modificata per puntare a un altro oggetto, il riferimento all'oggetto originale continuerà a puntare all'oggetto originale.

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".
}

Né stai passando una copia dell'oggetto.

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 linguaggi come Python e Ruby, il termine "passa per condivisione" è preferito per "passare per valore" di un oggetto / riferimento.

2 - Il termine "passaggio per riferimento" o "chiamata per riferimento" ha un significato molto specifico nella programmazione della terminologia del linguaggio. In effetti, significa che si passa l'indirizzo di una variabile o di un elemento dell'array , in modo che quando il metodo chiamato assegna un nuovo valore all'argomento formale, esso cambia il valore nella variabile originale. Java non supporta questo. Per una descrizione più completa dei diversi meccanismi per il passaggio dei parametri, consultare https://en.wikipedia.org/wiki/Evaluation_strategy .

Trappola: combinazione di incarichi ed effetti collaterali

Occasionalmente vediamo domande Java StackOverflow (e domande C o C ++) che chiedono che cosa tipo questo:

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

restituisce ... per alcuni stati iniziali note di i , a e b .

Parlando in generale:

  • per Java la risposta è sempre specificata 1 , ma non ovvia e spesso difficile da capire
  • per C e C ++ la risposta è spesso non specificata.

Tali esempi vengono spesso utilizzati negli esami o nelle interviste di lavoro come tentativo di vedere se lo studente o l'intervistato comprende come la valutazione delle espressioni funzioni realmente nel linguaggio di programmazione Java. Questo è probabilmente legittimo come un "test di conoscenza", ma ciò non significa che dovresti mai farlo in un programma reale.

Per illustrare, il seguente esempio apparentemente semplice è apparso un paio di volte nelle domande StackOverflow (come questo ). In alcuni casi, appare come un vero errore nel codice di qualcuno.

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

La maggior parte dei programmatori (inclusi esperti Java) che leggono rapidamente queste affermazioni direbbero che emette 2 . In effetti, emette 1 . Per una spiegazione dettagliata del motivo, leggere questa risposta .

Tuttavia il vero takeaway da questo e gli esempi simili è che qualsiasi dichiarazione Java che sia assegna da e per gli effetti collaterali della stessa variabile sta per essere nella migliore delle ipotesi difficile da capire, e nel peggiore dei casi addirittura fuorviante. Dovresti evitare di scrivere codice come questo.


1 - Modulo potenziali problemi con il modello di memoria Java se le variabili o gli oggetti sono visibili ad altri thread.

Trappola: non capendo che String è una classe immutabile

I nuovi programmatori Java spesso dimenticano, o non riescono a comprendere appieno, che la classe Java String è immutabile. Questo porta a problemi come quello nell'esempio seguente:

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();
    }
}

Il codice precedente dovrebbe stampare gli argomenti della riga di comando in maiuscolo. Sfortunatamente, non funziona, il caso degli argomenti non è cambiato. Il problema è questa affermazione:

s.toUpperCase();

Si potrebbe pensare che la chiamata toUpperCase() cambierà s in una stringa maiuscola. Non è così. Non può! String oggetti String sono immutabili. Non possono essere cambiati.

In realtà, il metodo toUpperCase() restituisce un oggetto String che è una versione in maiuscolo della String cui viene chiamata. Questo sarà probabilmente un nuovo oggetto String , ma se s era già tutto in maiuscolo, il risultato potrebbe essere la stringa esistente.

Quindi, per utilizzare efficacemente questo metodo, è necessario utilizzare l'oggetto restituito dalla chiamata al metodo; per esempio:

s = s.toUpperCase();

In effetti, la regola "le stringhe non cambiano mai" si applica a tutti i metodi String . Se lo ricordi, puoi evitare un'intera categoria di errori del principiante.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow