Menu


Trascrizione diapositive

1. Polimorfismo

  • Complessità: ALTA.


2. Polimorfismo

  • Il polimorfismo è uno tra i concetti più complessi di Java, esso sarà di difficile apprendimento, quindi si ritiene necessario un approccio graduale all’argomento, il lettore è invitato alla massima attenzione riguardo la serie di concetti che verranno esplicitati nella maniera più chiara possibile.
  • Si conoscono due forme di polimorfismo:
    - Polimorfismo dei metodi
    - Polimorfismo dei dati
  • Il polimorfismo dei metodi si suddivide in:
    - Overload
    - Override
  • Invece il polimorfismo dei dati si suddivide in:
    - Parametri polimorfi
    - Collezioni eterogenee


3. Overload

  • Fino ad ora ogni volta che abbiamo descritto un metodo, ci siamo soffermati in modo particolare sul suo identificatore (nome del metodo), effettivamente esso è stato molto importante perché ha permesso ad un qualsiasi oggetto di invocare una determinata azione.
  • Bisogna però comprendere che in una classe possono convivere più metodi con lo stesso identificatore, apparentemente quest’ultima affermazione può sembrare singolare, in quanto è normale domandarsi: se un oggetto prova a invocare un metodo, prendendo per buona la precedente dichiarazione, l’oggetto quale metodo dovrà eseguire?  La risposta è da ricercare nella lista dei parametri.
  • In un metodo, la coppia costituita dall’identificatore e dalla lista dei parametri è detta firma, essa permette ad un oggetto di invocare senza ambiguità l’azione desiderata.
  • L'overload quindi si basa sulla scrittura di più metodi identificati dallo stesso nome che però hanno, in ingresso, parametri diversi.


4. Overload

  • Segue un semplice esempio di overload:

[Download]

class SommaAritmetica {

public int somma(int x, int y) {
  return x+y;
}


public float somma(float x, float y) {
  return x+y;
}


public float somma(int x, float y) {
  return x+y;
}


public float somma(float x, int y) {
  return x+y;
}


public double somma(int x, double y, int z) {
  return x+y+z;
}

}

Si invoca così...

[Download]

public class Programma {
  public static void main(String [] args) {
    SommaAritmetica operazione = new SommaAritmetica();
    System.out.println(operazione.somma (2.50f, 2.50f));
    System.out.println(operazione.somma (5, 5));
    System.out.println(operazione.somma (5, 2.50f));
    System.out.println(operazione.somma (2.50f, 5));
    System.out.println(operazione.somma (5, 2.50, 5));
  }
}



5. Overload

  • Nell’esempio precedente è facile constatare che sono presenti cinque metodi che hanno lo stesso nome, quindi svolgono lo stesso compito ma in modo diverso.
  • Chiaramente se l’overload non fosse presente in Java, avremmo dovuto specificare un nome diverso per ogni metodo.
  • A questo punto, sperando che il lettore abbia compreso l’utilità di questa tecnica, elenchiamo nel dettaglio (con relativi esempi) i criteri di distinzione riguardanti la lista dei parametri:
    - Tipo [Es. somma(int x, int y) è diverso da somma(float x, float y)].
    - Numero [Es. somma(int x, int y) è diverso da somma(int x, double y, int z)].
    - Posizione [Es. somma(int x, float y) è diverso da somma(float x, int y)].


6. Varargs

  • In Java dalla versione 5 è stata introdotta la possibilità di utilizzare i cosiddetti varargs.
  • I varargs permettono ad un metodo presente in una determinata classe, di accettare, in ingresso, un numero imprecisato di parametri, evitando in questo modo di abusare della tecnica dell’overload.
  • Chiaramente l’utilizzo dei varargs è circoscritto alle esigenze del programmatore stesso, quindi, ci sono casi in cui si ritiene necessario utilizzare varargs, mentre ci sono altri casi in cui non si può prescindere dall'utilizzare la tecnica dell’overload.


7. Varargs

  • Segue un semplice esempio di varargs:

[Download]

class SommaAritmetica {
public double somma(double... parametriDinamici) {
double risultato = 0.0D;
for (double tmp : parametriDinamici) {
    risultato+=tmp;
}
return risultato;
}

}

Si invoca così...

[Download]

public class Programma {
  public static void main(String [] args) {
    SommaAritmetica operazione = new SommaAritmetica();
    System.out.println(operazione.somma (2.50f, 2.50f));
    System.out.println(operazione.somma (5, 5));
    System.out.println(operazione.somma (5, 2.50f));
    System.out.println(operazione.somma (2.50f, 5));
    System.out.println(operazione.somma (5, 2.50, 5));
  }
}

In questo caso possiamo notare che; nei parametri di tipo «int» e di tipo «float» è stata effettuata la cosiddetta promotion, il risultato restituito su schermo sarà chiaramente di tipo «double». Possiamo notare altresì che il varargs all’interno del metodo è considerato come un array.


8. Varargs

  • E’ possibile dichiarare un unico varargs a metodo.
  • Inoltre è possibile dichiarare altri parametri oltre l’unico varargs, ma il varargs deve occupare l’ultima posizione tra i parametri.


9. Override

  • Con il termine override si intende la riscrittura della firma e del tipo di ritorno di un determinato metodo (presente nella classe principale) da parte di una classe derivata.
  • Dobbiamo tenere conto del fatto che; in una classe non possono coesistere più metodi con la stessa firma e che nel caso di classi indipendenti, cui ciascuna classe definisce un metodo con la stessa firma e con lo stesso tipo di ritorno non si può parlare di override.
  • Quindi l’override implica il meccanismo di ereditarietà, giacché la riscrittura del metodo avviene in una sottoclasse.


10. Override

  • Segue un semplice esempio di override:

[Download]

(Prima parte del codice)

class Dipendente {

private String nome;
private int oreLavorativeMensili;
private int retribuzioneOraria;
public void setNome(String nome) {
    this.nome = nome;
}
public String getNome() {
    return nome;
}
public void setOreLavorativeMensili(int oreLavorativeMensili) {
    this.oreLavorativeMensili = oreLavorativeMensili;
}
public int getOreLavorativeMensili() {
    return oreLavorativeMensili;
}
public void setRetribuzioneOraria(int retribuzioneOraria) {
    this.retribuzioneOraria = retribuzioneOraria;
}
public int getRetribuzioneOraria() {
    return retribuzioneOraria;
}
public int stipendio() {
    return oreLavorativeMensili * retribuzioneOraria;
}
}


11. Override

  • Segue un semplice esempio di override:

[Download]

(Seconda parte del codice)

class ResponsabileProgetto extends Dipendente {

private int bonus;

public void setBonus(int bonus) {
    this.bonus = bonus;
}

public int getBonus() {
    return bonus;
}

  @Override
public int stipendio() {
    int stipendioFisso=(getOreLavorativeMensili() * getRetribuzioneOraria());

    return stipendioFisso + bonus;
}

}

Si invoca così...

[Download]

public class Programma {
  public static void main(String [] args) {
    Dipendente giacomo = new Dipendente();
    giacomo.setNome("Giacomo");
    giacomo.setOreLavorativeMensili(150);
    giacomo.setRetribuzioneOraria(10);

    System.out.println(giacomo.stipendio());
   
    ResponsabileProgetto luigi = new ResponsabileProgetto();
    luigi.setNome("Luigi");
    luigi.setOreLavorativeMensili(150);
    luigi.setRetribuzioneOraria(10);
    luigi.setBonus(1000);
    System.out.println(luigi.stipendio());
  }
}



12. Override

  • Nell’esempio mostrato in precedenza notiamo che il «ResponsabileProgetto», oltre ad uno stipendio fisso, ha un attributo che rappresenta un bonus di produzione. Dunque la retribuzione di questo dipendente sarà data dalla somma dello stipendio fisso con l'eventuale bonus.
  • Con l'override, quindi, è possibile riscrivere un metodo di una classe più generalista adattandolo così alla classe più specialista, mantenendo comunque una coerenza per quanto riguarda la semantica del metodo che avrà identica firma e identico tipo di ritorno.


13. Annotazione

  • Il programmatore quando si accinge ad effettuare un override può facilmente incorrere in errore.
  • Infatti è possibile che la riscrittura di un metodo sia effettuata in modo superficiale, magari non rispettando i caratteri maiuscolo/minuscolo, oppure digitando, in ingresso, parametri differenti rispetto a quelli presenti nella superclasse (in questo caso si darà luogo ad un overload).
  • Per evitare di incorrere in questi intoppi si usa una novità introdotta dalla versione 5 di Java, chiamata Annotazione.
  • Un’annotazione permette non soltanto di prendere nota di un qualcosa, ma di evitare efficacemente di incorrere in errori di programmazione.
  • Infatti anteponendo prima del metodo su cui vogliamo effettuare una riscrittura, la dicitura «@Override» (come nell’esempio precedente), nel caso violassimo nella sintassi del metodo summenzionato qualche regola dell’override, il compilatore javac ci comunicherà che la tecnica non è stata ultimata nella maniera corretta.


14. Polimorfismo per dati

  • Per polimorfismo per dati s’intende essenzialmente il processo operativo per il quale si ritiene necessario dichiarare classe principale, un oggetto istanziato a partire da una classe derivata, fino ad ora per istanziare e dichiarare abbiamo scritto in questo modo:
ClassePrincipale punto1= new ClassePrincipale(); // Dichiarazione ed istanza
  • Adesso è necessario comprendere che possiamo scrivere anche in questo modo:

ClassePrincipale punto1= new ClasseDerivata(); // Dichiarazione ed istanza

Bisogna notare che per «ClasseDerivata» s’intende una sottoclasse che eredita da una superclasse e che invece per «ClassePrincipale» si intende una superclasse dalla quale la sottoclasse «ClasseDerivata» eredita.
Bisogna ricordare per quanto riguarda «ClassePrincipale()» e «ClasseDerivata()», che l’istanza deve essere intesa come una semplice chiamata al metodo costruttore.



15. Polimorfismo per dati

  • Il processo operativo espresso nella diapositiva mostrataci in precedenza ha però un limite importante, in quanto, aver memorizzato in una variabile non primitiva di tipo «ClassePrincipale», un indirizzo di memoria riferito ad una «ClasseDerivata», fa si che dall’oggetto creato non possiamo accedere a metodi e quindi a variabili dichiarate per la prima volta nella sottoclasse.


16. Collezioni eterogenee

  • Per collezione eterogenea si intende il processo mediante il quale riusciamo a leggere l’indirizzo di memoria di una variabile non primitiva, per capire come in realtà è stata istanziata.
  • Procediamo creando i seguenti oggetti:

[Download]

public class Programma {

public static void main(String [] args) {
Dipendente giacomo = new Dipendente();   
Dipendente luigi = new ResponsabileProgetto();
Dipendente fabio = new Collaudatore();
luigi.nome="Luigi";
fabio.stipendio(luigi); // Esempio di chiamata al metodo
System.out.println (luigi.stipendio+"€");
}
}


17. Collezioni eterogenee

  • Vediamo adesso come è stato composto il metodo pubblico «stipendio()» (non abbiamo effettuato l’incapsulamento per risparmiare spazio):

[Download]

class Dipendente {

public String nome;
public int stipendio;

public void stipendio(Dipendente altro) {
if(altro instanceof Collaudatore) {
    altro.stipendio=800;
}
else if(altro instanceof ResponsabileProgetto) {
    altro.stipendio=2500;
}
else if(altro instanceof Dipendente) {
    altro.stipendio=1500;
}
}

}
class Collaudatore extends Dipendente {}
class ResponsabileProgetto extends Dipendente {}

  • Notiamo che è stata utilizzata una nuova parola chiave di Java, chiamata «instanceof», essa ci permette di verificare come è stato istanziato l’oggetto passato sotto forma di parametro.


18. Collezioni eterogenee

  • Comprensibilmente se avessimo scritto in questo modo:

class Dipendente {

public String nome;
public int stipendio;

public void stipendio(Dipendente altro) {
if(altro instanceof Dipendente) {
    altro.stipendio=1500;
}
else if(altro instanceof Collaudatore) {
    altro.stipendio=800;
}
else if(altro instanceof ResponsabileProgetto) {
    altro.stipendio=2500;
}
}

}
class Collaudatore extends Dipendente {}
class ResponsabileProgetto extends Dipendente {}

Il dipendente «Luigi» sarebbe stato pagato «1500€».



19. Casting di oggetti

  • Precedentemente avevamo affermato che; un oggetto creato con la regola del polimorfismo per dati, non può accedere a metodi e quindi a variabili dichiarate per la prima volta nella sottoclasse.
  • Quest’ultima affermazione è vera ma solo in parte, infatti, eseguendo il meccanismo del casting di oggetti, possiamo ristabilire la piena accessibilità dell’oggetto desiderato.
[Immagine] Casting di oggetti


20. Casting di oggetti

  • Prendiamo come modello il precedente esempio, aggiungendo alla classe «ResponsabileProgetto» una variabile pubblica, cui riusciremo ad accedere:

class Dipendente {

public String nome;
public int stipendio;
public void stipendio(Dipendente altro) {
if(altro instanceof Collaudatore) {
    altro.stipendio=800;
}
else if(altro instanceof ResponsabileProgetto) {
    altro.stipendio=2500;
    if(this instanceof ResponsabileProgetto) {
        ResponsabileProgetto unlock = (ResponsabileProgetto) altro; // Casting di oggetti

        unlock. richiesteRegistrate++;
    }
}
else if(altro instanceof Dipendente) {
    altro.stipendio=1500;
}
}

}
class Collaudatore extends Dipendente {
}

class ResponsabileProgetto extends Dipendente {
    public int richiesteRegistrate = 0;
}

Notiamo che quando un «ResponsabileProgetto» richiede lo «stipendio» suo o di un suo collega di pari grado, l’azione viene tracciata nell’oggetto di spettanza.



21. Casting di oggetti

  • Il meccanismo del casting permette di accedere al contenuto della variabile non primitiva (chiamata nell’esempio: «altro») che contiene come sappiamo l’indirizzo di memoria dell’oggetto, copiando codesto indirizzo in un’altra variabile non primitiva (chiamata nell’esempio: «unlock») dichiarata, questa volta, in modo corretto (cioè esattamente come è stato istanziato l’oggetto all’origine).
  • La conseguenza di questa operazione ci permetterà di accedere a qualsiasi membro della sottoclasse (nel caso dell’esempio: «ResponsabileProgetto»).


22. Invocazione virtuale dei metodi

  • Un’invocazione di un metodo «m» può definirsi virtuale; quando «m» è definito in una classe «A», ridefinito in una classe «B» (override) e invocato su un istanza di «B», dichiarata come «A» (polimorfismo per dati).
  • Quando s’invoca in maniera virtuale il metodo «m», il compilatore pensa di invocare il metodo «m» della classe «A» (virtualmente).
  • In realtà viene invocato il metodo ridefinito nella classe «B».