Errori comuni ed eccezioni

Venerdì, 22 Ottobre 2004

OBIETTIVI DELLA LEZIONE

In questa lezione:

  1. Aumentare la comprensione di cosa è un oggetto Java
  2. Capire il meccanismo delle eccezioni

Errori comuni

Vediamo alcuni errori che è facile fare lavorando con le classi Java.

Copiare i riferimenti agli oggetti

Abbiamo visto che in Java c'è una differenza sostanziale tra "variabili oggetto" e oggetti veri e propri. Capiamolo meglio. Consideriamo questa classe:

class Punto3D {
  private double x, y, z;
  
  public Punto3D(double x0, double y0, double z0) {
    this.x = x0;
    this.y = y0;
    this.z = z0;
  }
  
  public double getX() {
    return this.x;
  }

  public void setX(double nuovoX) {
    this.x = nuovoX;
  }

  public double getY() {
    return this.y;
  }

  public double getZ() {
    return this.z;
  }
  
}

Analiziamo in dettaglio cosa significa istanziare un nuovo oggetto Punto3D

Punto3D questo = new Punto3D(1.0,2.0,3.0);

Ecco uno schema dei quattro passi necessari:






Vediamo invece cosa succede quando dichiariamo una variabile di tipo primitivo:

double distanza = 10.0;







Riassumendo:

Attenzione che quando copiate una variabile oggetto viene copiato solo il riferimento all'oggetto (cioè ovviamente il contenuto della variabile). Esempi:

    Punto3D questo = new Punto3D(1.0,2.0,3.0);
    Punto3D quello;
    
    quello = questo; // attenzione! quello e quello contengono lo stesso riferimento!
    quello.setX(8.0);
    
    System.out.println(questo.getX()); //Stampa 8.0 non 1.0!
    Punto3D questo = new Punto3D(1.0,2.0,3.0);
    Punto3D quello = new Punto3D(1.0,2.0,3.0);

    if(questo == quello) { // Attenzione stiamo confrontando i riferimenti NON gli oggetti!
      System.out.println("I due riferimenti coincidono");
    }      

    if((questo.getX()==quello.getX()) && 
      (questo.getY()==quello.getY()) && 
      (questo.getZ()==quello.getZ())) { // Giusto
      System.out.println("I due punti coincidono");
    }

Dimenticarsi di chiamare il costruttore

Da quello che abbiamo visto prima se ci dimentichiamo di invocare il costruttore i passi 2, 3 e 4 non vengono effettuati. La variabile oggetto contiene un valore qualunque e il compilatore si lamenta con l'errore variable ... might not have been initialized. Esempi:

    Punto3D questo;
    questo.setX(10.0); // Sbagliato questo non è stata inizializzata

    Punto3D questo = null;
    questo.setX(10.0); // Sbagliato questo non si riferisce a nessun oggetto

    Punto3D quello = new Punto3D(1.0,2.0,3.0);
    quello.setX(20.0); // Giusto

Invocare il costruttore come metodo istanza

    Punto3D quello = new Punto3D(1.0,2.0,3.0);
    
    // Sbagliato non possiamo ricreare un oggetto!
    quello.Punto3D(6.0,0.0,1.0); 
    
    // Giusto, creiamo un secondo oggetto e ne mettiamo il riferimento in quello.
    // Il vecchio oggetto viene eliminato prima o poi dal meccanismo automatico
    // di garbage collection.
    quello = new Punto3D(6.0,0.0,1.0);

Chiamare un metodo istanza senza specificare su quale oggetto deve operare

    Punto3D quello = new Punto3D(1.0,2.0,3.0);
    
    setX(10.0); // Sbagliato, su quale oggetto deve operare?

Passaggio parametri con tipi primitivi

In Java il passaggio dei parametri espliciti ad un metodo avviene sempre per "valore". Nel caso di parametri impliciti di tipo primitivo questo è immediato da capire ma porta ad eventuali errori. Vediamone uno tipico:

class Scambia {
  public static void main(String[] args) {
    int primoNumero = 10, secondoNumero = 20;
    
    scambiaNumeriInteri(primoNumero, secondoNumero);
    System.out.println("primoNumero = " + primoNumero + " e secondoNumero = " + secondoNumero);
  }
  
  static void scambiaNumeriInteri(int a, int b) {
    int temp;
    
    temp = a;
    a = b;
    b = temp;
    
    // a e b sono variabili temporanee che qui smettono di esistere.
  }
}

Quando il metodo di classe scambiaNumeriInteri è stato invocato, sono state create sullo stack due nuove variabili intere, a e b, in cui sono stati copiati primoNumero e secondoNumero. Al termine dell'esecuzione del metodo, le variabili vengono rimosse dallo stack e nessuna modifica si propaga nel metodo main. In Java NON esiste il passaggio parametro per indirizzo

Le variabili oggetto vengono anch'esse passate per valore, ma come abbiamo visto il loro valore è il riferimento all'oggetto, quindi non c'è problema.

Strutture di controllo del flusso 4/4

Il meccanismo delle eccezioni

Nonostante si faccia qualunque sforzo per rendere corretti ed efficaci i nostri programmi, succedono sempre cose che ci sfuggono (ad esempio eventualità che non avevamo preso in considerazione) e che li rendono poco robusti (un programma "robusto" è invece capace di reagire ad ogni eventualità che si presenta in maniera controllata, cioè seguendo la logica imposta dal programmatore). Vediamo un esempio banale

import javax.swing.*;

class Dividi {
  public static void main (String[] args) {
    String stringaInput;
    int dividendo, divisore, risultato;
    
    stringaInput = JOptionPane.showInputDialog("Inserisci il dividendo");
    dividendo = Integer.parseInt(stringaInput);
    
    stringaInput = JOptionPane.showInputDialog("Inserisci il divisore");
    divisore = Integer.parseInt(stringaInput);
    
    risultato = dividendo/divisore;
    
    JOptionPane.showMessageDialog(null, "Il risultato della divisione è " + risultato);

    System.exit(0);
  }
}

Il programma calcola il risultato della divisione intera tra due numeri immessi dall'utente. Funziona perfettamente. Immaginiamo però che un giorno per sbaglio l'utilizzatore scelga come secondo numero 0. Un risultato coerente sarebbe "infinito", ma i tipi primitivi interi non hanno una rappresentazione di questo valore. Quello che succede è che quando la JVM arriva alla riga risultato = dividendo/divisore; stampa il seguente messaggio sulla console:

>java Dividi
java.lang.ArithmeticException: / by zero
	at Dividi.main(Dividi.java:14)

Il flusso del programma si è interrotto, l'istruzione System.exit(0); non viene eseguita e la JVM non ritorna il controllo all'utente (dobbiamo uccidere il processo). Un bel problema per una semplice divisione per 0 in un programmino di poche righe! Immaginate quante situazioni di questo tipo si presentano in un programma più complesso.

Per risolvere alla radice il problema, i linguaggio di programmazione più recenti introducono il meccanismo delle eccezioni. Quando qualcosa non va, viene lanciata una eccezione opportuna indicante il tipo di problema: ad esempio nel nostro caso è stata lanciata l'eccezione ArithmeticException. Spetta poi al programmatore definire cosa vuole fare quando si presentano queste eccezioni: se il metodo che ha lanciato l'eccezione non prevede di gestirla, questa viene passata al metodo di livello superiore, fino ad arrivare al main() e alla JVM stessa.

Nell'esempio, l'eccezione ArithmeticException è stata rilanciata fino alla JVM, che non sapendo cosa fare ha informato l'utente.

E' come in una azienda: mentre un operaio utilizza una macchina utensile questa si rompe. Se l'operaio sa come risolvere il problema la aggiusta, altrimenti va dal capo reparto e gli chiede di intervenire. Questi se sa come intervenire lo fa, altrimenti si rivolge al direttore del reparto e così via.

try - catch

Ogni metodo in cui pensiamo possa essere lanciata una eccezione, può gestirla in due modi:

Una eccezione è anch'essa un oggetto che deriva dalla superclasse Throwable. Uno schema di ereditarietà elementare è il seguente:


Le eccezioni indicate in giallo devono essere trattate (cioè dobbiamo prevedere un blocco catch che le gestisca) altrimenti il compilatore non produce il bytecode della classe.

Il massimo e il minimo di un array

import javax.swing.*;

class MassimoArrayDiDouble {
  public static void main(String[] args) {
    String stringaInput;
    double[] movimenti = new double[10];

    for(int i=0; i<movimenti.length; i++) {
      stringaInput = JOptionPane.showInputDialog("Inserisci l'elemento numero " + i);
      movimenti[i] = Double.parseDouble(stringaInput);
    }
      
    double minimo, massimo;
    int posizioneMassimo, posizioneMinimo;
    
    minimo = movimenti[0];
    posizioneMinimo = 0;
    massimo = movimenti[0];
    posizioneMassimo = 0;
    
    for(int i=1; i<movimenti.length; i++) {
      if(movimenti[i] > massimo) {
        massimo = movimenti[i];
        posizioneMassimo = i;
      }
      if(movimenti[i] < minimo) {
        minimo = movimenti[i];
        posizioneMinimo = i;
      }
    }
      
    JOptionPane.showMessageDialog(null, "Il massimo è " + massimo + " in posizione " + posizioneMassimo + "\n" +
      "mentre il minimo è " + minimo + " in posizione " + posizioneMinimo);
    
    System.exit(0);
    
  }
}

Lab

Esercizio 1

Modifica il testo del programma che calcola il massimo ed il minimo di un array in modo che non si interrompa se l'utente inserisce valori non numerici. Il programma deve continuare come se niente fosse fino a che l'utente ha immesso 10 valori.

Esercizio 2

Modifica il programma sviluppato nell'esercizio 1, in modo che invece di cercare il massimo e il minimo, calcoli la media dei numeri forniti in ingresso.

Esercizio 3: Bonus! La soluzione corretta a questo esercizio deve essere inviata entro le ore 23 di martedì 26/10/2004 all'indirizzo sassi@dti.unimi.it

Scrivere la classe CercaNumeroRipetizioni che chiede all'utente di inserire una stringa composta solo di due caratteri: A, B. Il programma deve restituire il numero massimo di ripetizione consecutive per ciascuno dei due. (Esempio: AAABAABBBBA, A->3 ripetizioni consecutive, B->4 ripetizioni consecutive).

Esercizio 4

Scrivere la classe Persona con le variabili istanza: nome, cognome. La classe deve avere i costruttori opportuni e il metodo getNomeCognome() che restituisca una stringa con il nome ed il cognome dello studente uniti (es. LuigiRossi). Creare la sottoclasse Studente con la variabile istanza aggiuntiva matricola. Scrivere un breve metodo main() che utilizzando la classe studente chieda all'utente nome, cognome e numero di matricola, crei un oggetto studente e quindi stampi la stringa: "NomeCognomeMatricola" (dove ovviamente Nome, Cognome e matricola sono quelli inseriti).

©2004 Roberto Sassi