OBIETTIVI DELLA LEZIONE
In questa lezione:
Vediamo alcuni errori che è facile fare lavorando con le classi Java.
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"); }
Una soluzione più efficace ed elegante è quella di sovrascrivere il metodo equals() della superclasse Object. Quando in una sottoclasse definiamo un metodo che ha lo stesso nome di un metodo appartenente ad una superclasse, il vecchio metodo viene "nascosto" dal nuovo (cioè viene invocato quello della sottoclasse).
class Punto3D { ... // Tutto quello che già c'era nella classe Punto3D /* Sovrascriviamo il metodo Object.equals() */ public boolean equals(Object altroPunto) { // Notate che abbiamo utilizzato lo stesso parametro esplicito della superclasse: Object altroPunto // Se altroPunto è null il metodo ritorna false (equals è definito per oggetti non nulli) if(altroPunto == null) return false; else { // altroPunto è per ora un riferimento ad un oggetto di classe Object // (anche se l'oggetto a cui punta è un Punto3D Punto3D altro = (Punto3D) altroPunto; if( (this.getClass() == altro.getClass()) && (this.x == altro.x) && (this.y == altro.y) && (this.z == altro.z) ) return true; else return false; } } public static void main (String[] args) { Punto3D questo = new Punto3D(1.0,2.0,3.0); Punto3D quello = new Punto3D(1.0,2.0,3.0); if(questo.equals(quello)) { // Giusto System.out.println("I due punti coincidono"); } } }
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
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);
Punto3D quello = new Punto3D(1.0,2.0,3.0); setX(10.0); // Sbagliato, su quale oggetto deve operare?
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.
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 linguaggi 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.
Ogni metodo in cui pensiamo possa essere lanciata una eccezione, può gestirla in due modi:
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); try { risultato = dividendo/divisore; JOptionPane.showMessageDialog(null, "Il risultato della divisione è " + risultato); } catch(ArithmeticException e) { JOptionPane.showMessageDialog(null, "La divisione per 0 non è definita."); } System.exit(0); } }All'interno del blocco try mettiamo tutte quelle istruzioni che temiamo possano lanciare una eccezione. Se si verifica una eccezione si interrompe l'esecuzione del blocco try e il controllo viene passato al blocco catch opportuno, se esiste. Il problema della divisione per zero è risolta, ma ad esempio se l'utente inserisce la lettera 't' invece di un numero, JVM protesta con:
java.lang.NumberFormatException: For input string: "t" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48) at java.lang.Integer.parseInt(Integer.java:468) at java.lang.Integer.parseInt(Integer.java:518) at Dividi.main(Dividi.java:12)Per risolvere anche questo problema, possiamo inserire nel blocco protetto anche le operazioni di input utente:
import javax.swing.*; class Dividi { public static void main (String[] args) { String stringaInput; int dividendo, divisore, risultato; try { 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); } catch(ArithmeticException e) { JOptionPane.showMessageDialog(null, "La divisione per 0 non è definita."); } catch(NumberFormatException e) { JOptionPane.showMessageDialog(null, "Dividendo e divisore devono essere numeri."); } System.exit(0); } }
Quando si presenta una eccezione l'esecuzione del blocco try si interrompe bruscamente. Supponiamo invece che ci sia qualcosa nel blocco try che vogliamo essere sicuri venga fatto, ci siano o meno delle eccezioni: si utilizza il blocco opzionale finally. Quando può essere utile? Ad esempio se vogliamo essere sicuri di chiudere un file, una connessione o spegnere il forno a microonde.
try { 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); } catch(ArithmeticException e) { JOptionPane.showMessageDialog(null, "La divisione per 0 non è definita."); } catch(NumberFormatException e) { JOptionPane.showMessageDialog(null, "Dividendo e divisore devono essere numeri."); } finally { System.out.println("Questo è un messaggio che ci tenevo proprio a scrivere."); }
import javax.swing.*; class Dividi { public static void main (String[] args) { String stringaInput; int dividendo, divisore, risultato; try { stringaInput = JOptionPane.showInputDialog("Inserisci il dividendo"); dividendo = Integer.parseInt(stringaInput); stringaInput = JOptionPane.showInputDialog("Inserisci il divisore"); divisore = Integer.parseInt(stringaInput); risultato = eseguiDivisione(dividendo, divisore); JOptionPane.showMessageDialog(null, "Il risultato della divisione è " + risultato); } catch(ArithmeticException e) { JOptionPane.showMessageDialog(null, "La divisione per 0 non è definita."); } catch(NumberFormatException e) { JOptionPane.showMessageDialog(null, "Dividendo e divisore devono essere numeri."); } System.exit(0); } public static int eseguiDivisione(int a, int b) throws ArithmeticException { if(b == 0) throw new ArithmeticException(); else return a/b; } }Il metodo che lancia una eccezione lo deve dichiarare subito dopo la lista dei parametri espliciti, tramite la parola chiave throws. L'eccezione viene invece lanciata con l'operatore throw.
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); } }
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
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 che estanda la classe Persona
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).
©2005 Roberto Sassi