Ereditarietà e Polimorfismo. Eccezioni

Giovedì, 19 Ottobre 2006

OBIETTIVI DELLA LEZIONE

In questa lezione:

  1. Capiremo il concetto di ereditarietà
  2. Vedremo che cosa si intende per polimorfismo
  3. Capiremo il meccanismo delle eccezioni

Ereditarietà

Perchè costruire classi? Per riutilizzarle espandendole (non modificandone il codice!). Per espandere una classe si utilizza la parola chiave extends. Vediamolo insieme in queste slides.

class ContoCorrenteBancario extends ContoBancario {
  // variabili istanza:
  private double tassoInteresse;

  // costruttori(sono metodi istanza)
  public ContoCorrenteBancario() {
    super(5.0);
    this.tassoInteresse = 0.01;
  }

  public ContoCorrenteBancario(double tassoInteresseIniziale) {
    this();
    this.tassoInteresse = tassoInteresseIniziale;
  }

  public ContoCorrenteBancario(double sommaIniziale, double tassoInteresseIniziale) {
    super(sommaIniziale);
    this.tassoInteresse = tassoInteresseIniziale;
  }

  // metodi istanza
  public void aggiungiInteressi(int numeroMesi) {
    double interessiMaturati = (numeroMesi / 12.0) * this.tassoInteresse * this.saldoConto();
    this.deposita(interessiMaturati);
  }
}
class ProvaConto2 {
  public static void main(String[] args) {
    ContoCorrenteBancario contoLuigi = new ContoCorrenteBancario();
    ContoCorrenteBancario contoGino = new ContoCorrenteBancario(1000.0, 0.02);

    // Trasferiamo 100.0 dal conto di Gino a quello di Luigi
    ContoBancario.trasferisci(100.0, contoGino, contoLuigi);

    // E' trascorso un anno, Luigi e Gino maturano interessi. 
    contoLuigi.aggiungiInteressi(12);
    contoGino.aggiungiInteressi(12);

    System.out.println("Saldo di Gino: " + contoGino.saldoConto());
    System.out.println("Saldo di Luigi: " + contoLuigi.saldoConto());
  }
}

Cosa significa espandere una classe? Data una classe iniziale, che chiamiamo superclasse, costruiamo una classe derivata, detta sottoclasse, che la estenda e la specializzi. La sottoclasse eredita tutte le variabili ed i metodi della superclasse. Non dobbiamo riscriverli!

La sottoclasse può (deve nel caso del costruttore) ridefinire alcuni metodi perché agiscano con le nuove variabili introdotte.

Nell'esempio abbiamo visto due nuovi metodi:

Overridding del metodo equals()

allorquando si debbano confrontare due oggetti per verificarne l'uguaglianza dobbiamo necessariamente confrontarne il contenuto di tutti i campi. 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 ridefinito e "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");
    }
  }
}

Polimorfismo e interfaccie

Il termine polimorfismo deriva dal greco e significa "molte forme". Cerchiamo di conprendere cosa questo abbia a che fare con i nostri programmi Java in queste slides.

  class ContoCorrenteBancarioNumerato extends ContoCorrenteBancario {
  // variabile istanza:
  private int numeroConto;
  // variabile classe:
  private static int ultimoNumeroContoAssegnato = 0;

  // costruttori(sono metodi istanza)
  public ContoCorrenteBancarioNumerato() {
    super();
    ultimoNumeroContoAssegnato++;
    this.numeroConto = ultimoNumeroContoAssegnato;
  }

  public ContoCorrenteBancarioNumerato(double tassoInteresseIniziale) {
    super(tassoInteresseIniziale);
    ultimoNumeroContoAssegnato++;
    this.numeroConto = ultimoNumeroContoAssegnato;
  }

  public ContoCorrenteBancarioNumerato(double sommaIniziale, double tassoInteresseIniziale) {
    super(sommaIniziale, tassoInteresseIniziale);
    ultimoNumeroContoAssegnato++;
    this.numeroConto = ultimoNumeroContoAssegnato;
  }

  // metodi istanza
  public int getNumeroConto() {
    return this.numeroConto;
  }
}
class ProvaConto3 {
  public static void main(String[] args) {
    ContoCorrenteBancarioNumerato contoLuigi = new ContoCorrenteBancarioNumerato(0.03);
    ContoCorrenteBancarioNumerato contoMario = new ContoCorrenteBancarioNumerato();
    ContoCorrenteBancario contoGino = new ContoCorrenteBancario(1000.0, 0.02);

    // Trasferiamo 100.0 dal conto di Gino a quello di Luigi
    ContoBancario.trasferisci(100.0, contoGino, contoLuigi);

    System.out.println("Saldo di Gino: " + contoGino.saldoConto());
    System.out.println("Saldo del conto numero " + contoLuigi.getNumeroConto() +
      " di Luigi: " + contoLuigi.saldoConto());
    System.out.println("Saldo del conto numero " + contoMario.getNumeroConto() +
      " di Mario: " + contoMario.saldoConto());
  }
}

Interfacce

In Java, l'elemento centrale del linguaggio è il concetto di Classe. Ma talvolta il livello di astrazione che si vuole realizzare è superiore. Ad esempio, si pensi di voler realizzare un metodo che sia in grado di trovare il maggiore di un insieme di oggetti. Gli oggetti su cui si vorrebbe applicare il metodo sono molto diversi tra di loro ma certamente in comune hanno almeno il fatto che è possibile in qualche modo "misurarli" (ad esempio potremmo voler trovare l'oggetto "automobile" che abbia il bagagliaio più capiente, o l'oggetto "albero" con il fusto più alto, etc...). Dovremmo scrivere un metodo "cercaMassimo" diverso per ciascuna classe!

Java fortunatamente fornisce un'ulteriore strumento per fronteggiare queste situazioni: le interfacce.

Una interfaccia è l'elenco di un insieme di funzionalità che richiediamo ad una classe sotto forma di metodi. Ecco, ad esempio, la definizione di una interfaccia per il tipo Misurabile.

public interface Misurabile {
double getMisura();
}

La dichiarazione di interfaccia elenca tutti i metodi richiesti dall'interfaccia; questa interfaccia richiede un solo metodo, ma, in generale, un'interfaccia ne può richiedere più d'uno.

Un'interfaccia è simile a una classe, ma ci sono alcune differenze importanti:

Una volta definita una interfaccia, possiamo costruire una classe con un metodo che ordini oggetti di tipo "Misurabile". Oggetti di questo tipo non esistono, ma il metodo potrà essere utilizzato indifferentemente su tutti gli oggetti di classi che implementino l'interfaccia.

Cosa significa implementare una interfaccia? Vuol dire che la nuova classe che costruiamo (la classe "Albero", ad esempio) dovrà fornire una implementazione di tutti i metodi specificati dall'interfaccia e dovrà avvisare il compilatore dichiarandolo:

class Albero implements Misurabile {
private double altezza;
private double diametroBase;

...

public double getMisura() {
  return this.altezza;
}

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 che è discusso anche in queste slides.

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.

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.

Lab

Esercizio 1: Classi [Soluzione]

Scrivete una nuova classe Cerchio che estende la classe Ellisse dell'esempio visto la lezione scorsa. Scrivete gli opportuni costruttori e introducete i nuovi metodi di istanza getRaggio() e setRaggio(). Costuite la classe di prova ProvaRaggio che chieda all'utente il raggio e restituisca l'area di un cerchio.

Esercizio 2 [Soluzione]

Scrivere la classe Persona con le variabili istanza: nome, cognome. La classe deve avere i costruttori opportuni e deve ridefinire il metodo toString() della superclasse Object in modo che restituisca una stringa con il nome ed il cognome dello studente uniti (es. “LuigiRossi”).

Creare la sottoclasse Studente che estenda la classe Persona con la variabile istanza aggiuntiva matricola. Inoltre ridefinire anche per questa classe il metodo toString().

Infine, scrivere un breve metodo main() che utilizzando la classe studente chieda all'utente nome, cognome e numero di matricola, quindi, dopo aver creato un oggetto Studente, informi l’utente del codice dello studente (la stringa: "NomeCognomeMatricola").

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

  }
}

Esercizio 3 [Soluzioni: A, B C D]

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 4 [Soluzione]

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

Esercizio 5 [Soluzione]

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

©2006 Roberto Sassi