Ricerca…


introduzione

Diverse attività linguistiche di programmazione Java potrebbero condurre un programma a generare risultati errati nonostante siano stati compilati correttamente. Lo scopo principale di questo argomento è elencare le insidie ​​più comuni con le loro cause e proporre il modo corretto per evitare di cadere in tali problemi.

Osservazioni

Questo argomento riguarda aspetti specifici della sintassi del linguaggio Java che sono soggetti a errori o che non dovrebbero essere utilizzati in determinati modi.

Pitfall - Ignorare la visibilità del metodo

Persino gli esperti sviluppatori Java tendono a pensare che Java abbia solo tre modificatori di protezione. La lingua ha in realtà quattro! Il livello di visibilità privato (ovvero predefinito) del pacchetto è spesso dimenticato.

Dovresti prestare attenzione a quali metodi rendi pubblici. I metodi pubblici in un'applicazione sono l'API visibile dell'applicazione. Questo dovrebbe essere il più piccolo e compatto possibile, specialmente se si sta scrivendo una libreria riutilizzabile (vedere anche il principio SOLID ). È importante considerare allo stesso modo la visibilità di tutti i metodi e utilizzare solo l'accesso privato protetto o pacchetto se appropriato.

Quando si dichiarano metodi che devono essere privati come pubblici, si espongono i dettagli di implementazione interna della classe.

Un corollario di questo è che tu collaudi solo i metodi pubblici della tua classe - in effetti puoi solo testare metodi pubblici. È una cattiva pratica aumentare la visibilità dei metodi privati ​​solo per poter eseguire test unitari contro questi metodi. Il test di metodi pubblici che chiamano i metodi con una visibilità più restrittiva dovrebbe essere sufficiente per testare un'intera API. Non devi mai espandere la tua API con altri metodi pubblici solo per consentire il test delle unità.

Pitfall - Manca una 'pausa' in un caso 'interruttore'

Questi problemi di Java possono essere molto imbarazzanti e talvolta rimangono sconosciuti fino a quando non vengono eseguiti in produzione. Il comportamento fallito nelle istruzioni switch è spesso utile; tuttavia, mancare una parola chiave "break" quando tale comportamento non è desiderato può portare a risultati disastrosi. Se hai dimenticato di mettere una "interruzione" in "caso 0" nell'esempio di codice qui sotto, il programma scriverà "Zero" seguito da "Uno", poiché il flusso di controllo qui dentro passerà attraverso l'intera istruzione "switch" fino a raggiunge una "pausa". Per esempio:

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

Nella maggior parte dei casi, la soluzione più pulita sarebbe utilizzare le interfacce e spostare il codice con un comportamento specifico in implementazioni separate ( composizione sull'ereditarietà )

Se una dichiarazione di commutazione è inevitabile, si consiglia di documentare le scoperte "previste" se si verificano. In questo modo mostri agli altri sviluppatori che sei consapevole della rottura mancante e che questo è un comportamento previsto.

switch(caseIndex) {
    [...]
    case 2:
        System.out.println("Two");
        // fallthrough
    default:
        System.out.println("Default");

Trappola - punto e virgola fuori luogo e parentesi graffe mancanti

Questo è un errore che crea vera confusione per i principianti di Java, almeno per la prima volta che lo fanno. Invece di scrivere questo:

if (feeling == HAPPY)
    System.out.println("Smile");
else
    System.out.println("Frown");

accidentalmente scrivono questo:

if (feeling == HAPPY);
    System.out.println("Smile");
else
    System.out.println("Frown");

e sono perplessi quando il compilatore Java dice loro che il else è fuori posto. Il compilatore Java interpreta quanto sopra come segue:

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 altri casi, non ci saranno errori di compilazione, ma il codice non farà ciò che il programmatore intende fare. Per esempio:

for (int i = 0; i < 5; i++);
    System.out.println("Hello");

stampa solo "Hello" una volta. Ancora una volta, il punto e virgola spuria significa che il corpo del ciclo for è un'istruzione vuota. Ciò significa che la chiamata println che segue è incondizionata.

Un'altra variante:

for (int i = 0; i < 5; i++);
    System.out.println("The number is " + i);

Questo darà un errore "Impossibile trovare il simbolo" per i . La presenza del punto e virgola spuria significa che la chiamata println sta tentando di utilizzare i al di fuori del suo ambito.

In questi esempi, c'è una soluzione semplice: basta eliminare il punto e virgola spuria. Tuttavia, ci sono alcune lezioni più profonde da trarre da questi esempi:

  1. Il punto e virgola in Java non è "rumore sintattico". La presenza o l'assenza di un punto e virgola può modificare il significato del tuo programma. Non aggiungerli solo alla fine di ogni riga.

  2. Non fidarti del rientro del codice. Nel linguaggio Java, gli spazi bianchi extra all'inizio di una riga vengono ignorati dal compilatore.

  3. Utilizzare un penetratore automatico. Tutti gli IDE e molti semplici editor di testo comprendono come indentare correttamente il codice Java.

  4. Questa è la lezione più importante. Segui le ultime linee guida in stile Java e metti le parentesi attorno alle istruzioni "then" e "else" e all'istruzione body di un loop. La parentesi aperta ( { ) non dovrebbe essere su una nuova riga.

Se il programmatore ha seguito le regole di stile, l'esempio if con un punto e virgola fuori posto sarebbe simile a questo:

if (feeling == HAPPY); {
    System.out.println("Smile");
} else {
    System.out.println("Frown");
}

Sembra strano per un occhio esperto. Se indentri automaticamente tale codice, probabilmente sarà simile a questo:

if (feeling == HAPPY); {
                           System.out.println("Smile");
                       } else {
                           System.out.println("Frown");
                       }

che dovrebbe distinguersi come sbagliato anche per un principiante.

Trappola - Lasciando fuori parentesi: i problemi "dangling if" e "dangling else"

L'ultima versione della guida di stile Oracle Java impone che le istruzioni "then" e "else" in un'istruzione if vengano sempre racchiuse tra "parentesi graffe" o "parentesi graffe". Regole simili si applicano ai corpi di varie dichiarazioni di loop.

if (a) {           // <- open brace
    doSomething();
    doSomeMore();
}                  // <- close brace

Questo non è in realtà richiesto dalla sintassi del linguaggio Java. In effetti, se la parte "allora" di un'istruzione if è una singola istruzione, è legale escludere le parentesi

if (a)
    doSomething();

o anche

if (a) doSomething();

Tuttavia ci sono pericoli nell'ignorare le regole di stile Java e tralasciare le parentesi. In particolare, si aumenta in modo significativo il rischio che il codice con rientri errati venga interpretato erroneamente.

Il problema "dangling if":

Considera il codice di esempio riportato sopra, riscritto senza parentesi.

if (a)
   doSomething();
   doSomeMore();

Questo codice sembra dire che le chiamate a doSomething e doSomeMore si verificheranno entrambe se e solo se a è true . In effetti, il codice è rientrato in modo errato. La specifica del linguaggio Java che la chiamata doSomeMore() è un'istruzione separata che segue l'istruzione if . La rientranza corretta è la seguente:

if (a)
   doSomething();
doSomeMore();

Il problema "dangling else"

Un secondo problema appare quando aggiungiamo else al mix. Considera il seguente esempio con parentesi mancanti.

if (a)
   if (b)
      doX();
   else if (c)
      doY(); 
else
   doZ();

Il codice sopra sembra dire che doZ sarà chiamato quando a è false . In realtà, il rientro è errato ancora una volta. Il rientro corretto per il codice è:

if (a)
   if (b)
      doX();
   else if (c)
      doY(); 
   else
      doZ();

Se il codice è stato scritto in base alle regole di stile Java, sarebbe in effetti simile a questo:

if (a) {
   if (b) {
      doX();
   } else if (c) {
      doY(); 
   } else {
      doZ();
   }
}

Per illustrare il motivo per cui è meglio, supponiamo di aver accidentalmente indentato il codice. Potresti finire con qualcosa del genere:

if (a) {                         if (a) {
   if (b) {                          if (b) {
      doX();                            doX();
   } else if (c) {                   } else if (c) {
      doY();                            doY();
} else {                         } else {
   doZ();                            doZ();
}                                    }
}                                }

Ma in entrambi i casi, il codice errato "sembra sbagliato" per l'occhio di un esperto programmatore Java.

Trappola - Sovraccarico invece di scavalcare

Considera il seguente esempio:

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

Questo codice non si comporterà come previsto. Il problema è che i metodi equals e hashcode per Person non sovrascrivono i metodi standard definiti da Object .

  • Il metodo equals ha la firma sbagliata. Dovrebbe essere dichiarato come equals(Object) non equals(String) .
  • Il metodo hashcode ha il nome sbagliato. Dovrebbe essere hashCode() (notare la maiuscola C ).

Questi errori significano che abbiamo dichiarato sovraccarichi accidentali, e questi non saranno usati se la Person viene usata in un contesto polimorfico.

Tuttavia, c'è un modo semplice per gestire questo (da Java 5 in poi). Usa l'annotazione @Override ogni volta che intendi che il tuo metodo sia un override:

Java SE 5
public final class Person {
    ...

    @Override
    public boolean equals(String other) {
        ....
    }

    @Override
    public hashcode() {
        ....
    }
}

Quando aggiungiamo un @Override un'annotazione a una dichiarazione di metodo, il compilatore verificherà che il metodo non sovrascrivere (o implementare) un metodo dichiarato in una superclasse o interfaccia. Quindi nell'esempio sopra, il compilatore ci darà due errori di compilazione, che dovrebbero essere sufficienti per avvisarci dell'errore.

Pitfall - Ottali letterali

Considera il seguente frammento di codice:

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

Un principiante Java potrebbe essere sorpreso di sapere che il programma di cui sopra stampa la risposta sbagliata. In realtà stampa la somma dei numeri da 1 a 8.

Il motivo è che un intero letterale che inizia con la cifra zero ('0') viene interpretato dal compilatore Java come un letterale ottale, non come un valore letterale decimale come ci si potrebbe aspettare. Quindi, 010 è il numero ottale 10, che è 8 in decimale.

Pitfall - Dichiarare classi con lo stesso nome delle classi standard

A volte, i programmatori che sono nuovi in ​​Java commettono l'errore di definire una classe con un nome uguale a una classe ampiamente utilizzata. Per esempio:

package com.example;

/**
 * My string utilities
 */
public class String {
    ....
}

Poi si chiedono perché ottengono errori imprevisti. Per esempio:

package com.example;

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

Se si compila e quindi si tenta di eseguire le classi precedenti, si otterrà un errore:

$ 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

Qualcuno che guarda il codice per la classe Test vedrebbe la dichiarazione di main e guarderà la sua firma e si chiederà di cosa si lamenta il comando java . Ma in realtà, il comando java sta dicendo la verità.

Quando dichiariamo una versione di String nello stesso pacchetto di Test , questa versione ha la precedenza sull'importazione automatica di java.lang.String . Pertanto, la firma del metodo Test.main è in realtà

void main(com.example.String[] args) 

invece di

void main(java.lang.String[] args)

e il java comando non riconoscerà come metodo entry point.

Lezione: non definire classi con lo stesso nome delle classi esistenti in java.lang o altre classi comunemente utilizzate nella libreria Java SE. Se lo fai, ti stai preparando per tutti i tipi di errori oscuri.

Pitfall - Usando '==' per testare un booleano

A volte un nuovo programmatore Java scriverà un codice come questo:

public void check(boolean ok) {
    if (ok == true) {           // Note 'ok == true'
        System.out.println("It is OK");
    }
}

Un programmatore esperto vedrebbe che è goffo e desidera riscriverlo come:

public void check(boolean ok) {
    if (ok) {
       System.out.println("It is OK");
    }
}

Tuttavia, c'è più sbagliato con ok == true rispetto alla semplice goffaggine. Considera questa variazione:

public void check(boolean ok) {
    if (ok = true) {           // Oooops!
        System.out.println("It is OK");
    }
}

Qui il programmatore ha scritto male == as = ... e ora il codice ha un bug sottile. L'espressione x = true assegna incondizionatamente true a x e quindi valuta true . In altre parole, il metodo di check ora stamperà "It is OK" indipendentemente dal parametro.

La lezione qui è di uscire dall'abitudine di usare == false e == true . Oltre ad essere prolissi, rendono la tua codifica più incline agli errori.


Nota: una possibile alternativa a ok == true che evita il trabocchetto consiste nell'utilizzare le condizioni Yoda ; vale a dire mettere il letterale sul lato sinistro dell'operatore relazionale, come in true == ok . Funziona, ma la maggior parte dei programmatori probabilmente concorderebbe sul fatto che le condizioni di Yoda sembrano strane. Certamente ok (o !ok ) è più conciso e più naturale.

Pitfall: le importazioni con caratteri jolly possono rendere fragile il tuo codice

Considera il seguente esempio parziale:

import com.example.somelib.*;
import com.acme.otherlib.*;

public class Test {
    private Context x = new Context();   // from com.example.somelib
    ...
}

Supponiamo che quando hai sviluppato per la prima volta il codice contro la versione 1.0 di somelib e la versione 1.0 di otherlib . Quindi, in un secondo momento, è necessario aggiornare le dipendenze a versioni successive e si decide di utilizzare otherlib versione 2.0. Supponiamo inoltre che una delle modifiche apportate ad otherlib tra 1.0 e 2.0 sia stata l'aggiunta di una classe Context .

Ora quando ricomponi Test , riceverai un errore di compilazione che ti dice che Context è un'importazione ambigua.

Se hai familiarità con il codebase, probabilmente questo è solo un piccolo inconveniente. In caso contrario, hai del lavoro da fare per affrontare questo problema, qui e potenzialmente altrove.

Il problema qui è l'importazione di caratteri jolly. Da un lato, l'uso di caratteri jolly può rendere le tue lezioni di qualche riga più corte. D'altro canto:

  • Modifiche verso l'alto compatibili con altre parti del codice base, con librerie standard Java o con librerie di terze parti possono portare a errori di compilazione.

  • La leggibilità soffre. A meno che non si stia utilizzando un IDE, è difficile determinare quale delle importazioni di caratteri jolly stia utilizzando una classe denominata.

La lezione è che è una cattiva idea utilizzare le importazioni di caratteri jolly nel codice che deve essere longevo. Le importazioni specifiche (non jolly) non sono molto difficili da mantenere se si utilizza un IDE e lo sforzo è utile.

Pitfall: utilizzo di 'assert' per argomento o validazione dell'input dell'utente

Una domanda che occasionalmente su StackOverflow è se sia appropriato utilizzare assert per convalidare gli argomenti forniti a un metodo o anche gli input forniti dall'utente.

La risposta semplice è che non è appropriato.

Migliori alternative includono:

  • Lanciare un IllegalArgumentException usando un codice personalizzato.
  • Utilizzo dei metodi Preconditions disponibili nella libreria Google Guava.
  • Utilizzo dei metodi Validate disponibili nella libreria di Apache Commons Lang3.

Questo è ciò che la specifica del linguaggio Java (JLS 14.10, per Java 8) consiglia su questo argomento:

In genere, il controllo delle asserzioni è abilitato durante lo sviluppo e il test del programma e disabilitato per la distribuzione, per migliorare le prestazioni.

Poiché le asserzioni possono essere disabilitate, i programmi non devono presumere che le espressioni contenute nelle asserzioni saranno valutate. Pertanto, queste espressioni booleane dovrebbero generalmente essere prive di effetti collaterali. La valutazione di tale espressione booleana non dovrebbe influire su nessuno stato visibile dopo aver completato la valutazione. Non è illegale che un'espressione booleana contenuta in un'asserzione abbia un effetto collaterale, ma in genere è inappropriata, in quanto potrebbe causare una variazione del comportamento del programma a seconda che le asserzioni fossero abilitate o disabilitate.

Alla luce di ciò, le asserzioni non dovrebbero essere usate per il controllo degli argomenti nei metodi pubblici. Il controllo degli argomenti è in genere parte del contratto di un metodo e questo contratto deve essere mantenuto se le asserzioni sono abilitate o disabilitate.

Un problema secondario con l'utilizzo delle asserzioni per il controllo degli argomenti è che gli argomenti errati dovrebbero comportare un'eccezione run-time appropriata (come IllegalArgumentException , ArrayIndexOutOfBoundsException o NullPointerException ). Un errore di asserzione non genererà un'eccezione appropriata. Ancora una volta, non è illegale usare asserzioni per il controllo degli argomenti sui metodi pubblici, ma è generalmente inappropriato. È inteso che AssertionError non venga mai rilevato, ma è possibile farlo, quindi le regole per le istruzioni try dovrebbero trattare le asserzioni che appaiono in un blocco try analogamente al trattamento corrente delle istruzioni throw.

Trappola di oggetti Null Auto-Unboxing in Primitive

public class Foobar {
    public static void main(String[] args) {

        // example: 
        Boolean ignore = null;
        if (ignore == false) {
            System.out.println("Do not ignore!");
        }
    }
}

Il trabocchetto qui è che null è paragonato a false . Dal momento che stiamo confrontando un boolean primitivo con un Boolean , Java tenta di unbox Object Boolean in un equivalente primitivo, pronto per il confronto. Tuttavia, poiché tale valore è null , viene generata una NullPointerException .

Java non è in grado di confrontare i tipi primitivi con valori null , il che causa una NullPointerException in fase di runtime. Considera il caso primitivo della condizione false == null ; questo genererebbe un errore incomparable types: int and <null> tempo di compilazione incomparable types: int and <null> .



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