Trascrizione diapositive
1. Ereditarietà e incapsulamento
- Complessità: MEDIA.
2. I paradigmi della programmazione ad oggetti
- Se il lettore ha appreso tutti i concetti espressi nelle precedenti unità, sarà in grado di conoscere le basi della programmazione Java. Ciò nonostante è opportuno imparare a scrivere il codice in modo corretto, poiché occorre realizzare applicazioni che siano robuste e sicure.
- Ciò che caratterizza un linguaggio orientato agli oggetti è il supporto che esso offre ai cosiddetti paradigmi della programmazione ad oggetti, essi sono:
- Incapsulamento
- Ereditarietà
- Polimorfismo - È anche necessario comprendere i paradigmi della programmazione procedurale che in Java sono spesso considerati secondari, ma che invece restano molto utili, essi sono:
- Astrazione
- Riuso
3. Astrazione
- L’astrazione è un paradigma della programmazione procedurale che potremmo definire come l’arte di sapersi concentrare solo sui dettagli veramente essenziali per far funzionare correttamente un programma.
- L’astrazione si divide in tre livelli:
- Astrazione funzionale
- Astrazione dei dati
- Astrazione di sistema - Adoperiamo l’astrazione funzionale ogni volta che implementiamo un metodo. Un metodo che come abbiamo già affermato è sinonimo di azione, deve essere implementato in modo efficace. Quando l’azione verrà invocata deve, quindi, restituire in ogni caso il risultato voluto.
- Adoperiamo l’astrazione dei dati ogni volta che definiamo una classe, raccogliendo in essa solo le caratteristiche (attributi) e le funzionalità (metodi) essenziali per gli oggetti.
- Adoperiamo l’astrazione di sistema ogni volta che definiamo tutte le classi essenziali che devono soddisfare gli scopi dell’applicazione stessa.
4. Riuso
- Con il termine riuso di codice si intende la pratica, estremamente comune nella programmazione, di richiamare o invocare parti di codice precedentemente già scritte ogni qualvolta risulti necessario senza, in questo modo, doverle riscrivere daccapo.
- Il riuso è quindi da considerarsi come una conseguenza dell’astrazione e degli altri paradigmi della programmazione ad oggetti, quindi dell’incapsulamento, dell’ereditarietà e del polimorfismo.
5. Incapsulamento
- L’incapsulamento permette di controllare l’accesso alle variabili, mediante l’uso di metodi che possono prevenire la non correttezza dei dati.
A tale scopo verrà trattato in modo approfondito il modificatore di tipo «private».
- La tecnica dell’incapsulamento ci aiuterà ad effettuare operazioni di modifica e manutenzione del codice, perché i cambiamenti sono localizzati e non globali, e quindi si riduce notevolmente il rischio di introdurre errori e di conseguenza il tempo necessario per eseguire le operazioni di test e debug.
6. Incapsulamento
- Supponiamo di voler scrivere un’applicazione che utilizza il seguente modello per definire un oggetto di tipo «Data»:
- class Data {
public int giorno;}
public int mese;
public int anno; - Supponiamo ancora di voler creare un oggetto di tipo «Data», valorizzando al suo interno le variabili definite nella classe, dovremo scrivere in questo modo:
public class Programma {
public static void main(String args[]) {
}
Data unaData = new Data();
unaData.giorno = 12;
unaData.mese = 10;
unaData.anno = 1990;
}
unaData.giorno = 12;
unaData.mese = 10;
unaData.anno = 1990;
7. Incapsulamento
- Ipotizziamo di implementare una funzione che permette di far inizializzare le variabili ad un semplice utente tramite interfaccia grafica. Le variabili adesso verranno inizializzate nel seguente modo:
- Valore «40» per il giorno.
- Valore «80» per il mese.
- Valore «2000» per l’anno. - In questo caso è chiaro a tutti che si tratta di una data non valida, ma il programma (per come è stato concepito) non restituirà alcun messaggio di errore!
- È evidente che manca un controllo per la validazione dei valori che può assumere una variabile d’istanza.
8. Incapsulamento
- A seguito di quanto appena affermato nella precedente slide, la classe «Data» dovrà essere riscritta nel seguente modo:
[Download]
(Prima parte del codice)
class Data {
private int giorno;
private int mese;
private int anno;
public void setGiorno(int g) {
public int getGiorno() {
private int mese;
private int anno;
public void setGiorno(int g) {
if (g > 0 && g<=31) {
}
giorno=g;
} else {
System.out.println(“Giorno non valido.”);
}
public int getGiorno() {
return giorno;
}
9. Incapsulamento
- A seguito di quanto appena affermato nella precedente slide, la classe «Data» dovrà essere riscritta nel seguente modo:
[Download]
(Seconda parte del codice)
public void setMese(int m) {
public int getMese() {
public void setAnno(int a) {
public int getAnno() {
}
if (m > 0 && m<=12) {
}
mese=m;
} else {
System.out.println(“Mese non valido.”);
}
public int getMese() {
return mese;
}public void setAnno(int a) {
anno=a;
}public int getAnno() {
return anno;
}
10. Incapsulamento
- L’incapsulamento come abbiamo forse già intuito dalla stesura del nuovo codice, è da considerarsi un passo importante per rendere più sicuro e robusto il nostro programma. La procedura che abbiamo seguito è semplice:
- Abbiamo dichiarato tutte le variabili di tipo «private».
- Abbiamo implementato dei metodi pubblici per accedere alle variabili «private», sia in scrittura «set» e sia in lettura «get». - Questo modo di lavorare ci permette di usufruire dei seguenti vantaggi:
- Con l’operatore d’appartenenza «.» non possiamo più accedere direttamente al dato perché è dichiarato di tipo «private», ciò è un importante beneficio. L’utente che vuole modificare le variabili d’istanza deve forzatamente invocare un metodo (che volendo può impedire tale modifica). Quindi ogni oggetto d’ora in avanti conterrà valori corretti, di conseguenza l’applicazione funzionerà a dovere!
- Il codice è molto più facile da manutenere e si adatta ai cambiamenti.
11. Incapsulamento
- Di seguito vediamo come dovrà essere riscritto il metodo «main()», conseguentemente alle nuove modifiche effettuate:
[Download]
public class Programma {
public static void main(String args[]) {
}
Data unaData = new Data();
unaData.setGiorno(12);
unaData.setMese(10);
unaData.setAnno(1990);
}
unaData.setGiorno(12);
unaData.setMese(10);
unaData.setAnno(1990);
- L’esecuzione del codice non restituirà alcun messaggio di errore, le variabili d’istanza saranno inizializzate in modo corretto.
- Nel caso avessimo provato ad inizializzare la variabile «giorno» al valore «32», in fase di esecuzione ci sarebbe stato restituito il messaggio «Giorno non valido.».
12. Incapsulamento
- Al lettore dovrebbe adesso risultare chiara l’utilità dei metodi «set» (mutator methods) implementati, potrebbe però avere qualche riserva sui metodi «get» (accessor methods).
- Supponiamo di voler stampare a video la variabile privata «giorno», scrivendo in questo modo (nel metodo «main()») ci verrà restituito un errore in compilazione:
System.out.println(unaData.giorno);
- Questo perché la variabile «giorno» è dichiarata di tipo «private», a questo punto è necessario invocare il metodo «getGiorno()» per restituire la variabile desiderata. L’unica soluzione è quindi:
System.out.println(unaData.getGiorno());
13. Incapsulamento
- Ricordiamo che anche i metodi «get» (accessor methods) potrebbero effettuare controlli, proprio come i metodi «set» (mutator methods).
- È facile immaginare, ad esempio, una classe come questa:
class ContoAziendale {
private String contoAziendale = "500 Euro";
private String codiceSegreto = "NostroConto";
private String codiceUtente;
public void setCodiceUtente(String cod) {
public String getCodiceUtente() {
public String getContoAziendale() {
}private String codiceSegreto = "NostroConto";
private String codiceUtente;
public void setCodiceUtente(String cod) {
codiceUtente = cod;
}public String getCodiceUtente() {
return codiceUtente;
}public String getContoAziendale() {
if(codiceUtente == codiceSegreto) {
}
return contoAziendale;
} else {
return "Codice errato!";
}
14. Incapsulamento
- Ma è anche possibile immaginare una classe che utilizza metodi che fanno uso del modificatore «private», ecco come si potrebbe evolvere l’esempio elaborato precedentemente:
class ContoAziendale {
private String contoAziendale = "500 Euro";
private String codiceSegreto = "NostroConto";
public String getContoAziendale(String codiceUtente) {
private String controllaCodice(String codiceUtente) {
}private String codiceSegreto = "NostroConto";
public String getContoAziendale(String codiceUtente) {
return controllaCodice(codiceUtente);
}private String controllaCodice(String codiceUtente) {
if(codiceUtente==codiceSegreto) {
}
return contoAziendale;
} else {
return "Codice errato!";
}
- Naturalmente dal metodo «main()» possiamo invocare esclusivamente il metodo «getContoAziendale()», passando come parametro il nostro codice segreto che deve essere di tipo «String».
15. Incapsulamento
- Da notare il fatto che a partire da due oggetti istanziati dalla stessa classe possono nell’ambito della classe, accedere direttamente ai rispettivi membri privati come se fossero pubblici. Come ci viene mostrato in questo esempio:
[Download]
class Dipendente {
private String nome;
private int punteggio;
public void setNome(String n) {
public String getNome() {
public void setPunteggio(int p) {
public int getPunteggio() {
public int getDifferenzaPunteggio(Dipendente altro) {
}private int punteggio;
public void setNome(String n) {
nome = n;
}public String getNome() {
return nome;
}public void setPunteggio(int p) {
punteggio = p;
}public int getPunteggio() {
return punteggio;
}public int getDifferenzaPunteggio(Dipendente altro) {
return (punteggio - altro.punteggio);
}
Constatare che nel metodo «getDifferenzaPunteggio()» è possibile accedere direttamente alla variabile privata «punteggio» dell’oggetto passato come parametro, senza dover utilizzare il metodo «getPunteggio()» (anche se usare il metodo è in ogni caso altamente consigliato).
16. Incapsulamento
- Qui di seguito un esempio di metodo «main()» che riguarda la classe «Dipendente» (vista in precedenza). Il risultato restituito dal programma sarà «1», esattamente la differenza tra il punteggio ottenuto dal dipendente «Fabrizio» nei confronti di «Fabio».
[Download]
public class Programma {
public static void main(String args[]) {
}
Dipendente fabrizio = new Dipendente();
Dipendente fabio = new Dipendente();
fabrizio.setNome("Fabrizio");
fabio.setNome("Fabio");
fabrizio.setPunteggio(7);
fabio.setPunteggio(6);
System.out.println(fabrizio. getDifferenzaPunteggio(fabio));
}
Dipendente fabio = new Dipendente();
fabrizio.setNome("Fabrizio");
fabio.setNome("Fabio");
fabrizio.setPunteggio(7);
fabio.setPunteggio(6);
System.out.println(fabrizio. getDifferenzaPunteggio(fabio));
17. Il reference this
- Analizzando ancora una volta l’esempio della classe «Dipendente», ci dobbiamo rendere conto che per ogni variabile d’istanza può essere specificato esplicitamente il nome dell’oggetto di appartenenza (reference). Nello specifico basta osservare la variabile «altro.punteggio» per comprendere che appartiene all’oggetto passato sotto forma di parametro.
- Notiamo sempre dal precedente esempio che per tutte le altre variabili non è stata specificata esplicitamente la propria istanza di appartenenza, in realtà si tratta di un’iniziativa del compilatore Java. Se il programmatore non referenzia espressamente una certa variabile d’istanza, al momento della compilazione il codice sarà modificato dal compilatore stesso, che aggiungerà un reference all’oggetto corrente, per oggetto corrente si intende l’oggetto che invoca il metodo.
- Java introduce una parola chiave che per definizione coincide ad un reference all’oggetto corrente: «this». La parola chiave «this» viene quindi implicitamente aggiunta nel bytecode compilato, per referenziare ogni variabile d’istanza non esplicitamente referenziata.
18. Il reference this
- Per rendere ancora più chiara l’utilità del reference «this» vediamo (con le conoscenze fino ad ora acquisite) come la classe «Dipendente» viene riscritta dal compilatore javac:
class Dipendente {
private String nome;
private int punteggio;
public Dipendente() {
}
public void setNome(String n) {
public String getNome() {
public void setPunteggio(int p) {
public int getPunteggio() {
public int getDifferenzaPunteggio(Dipendente altro) {
}private int punteggio;
public Dipendente() {
}
public void setNome(String n) {
this.nome = n;
}public String getNome() {
return this.nome;
}public void setPunteggio(int p) {
this.punteggio = p;
}public int getPunteggio() {
return this.punteggio;
}public int getDifferenzaPunteggio(Dipendente altro) {
return (this.punteggio - altro.punteggio);
}
Con «public Dipendente()» si intende il costruttore di default aggiunto in automatico dal compilatore se non ne è stato specificato uno esplicitamente, ma la novità su cui dobbiamo porre la nostra attenzione riguarda la parola chiave «this», inserita automaticamente dal compilatore per esplicitare l’appartenenza delle variabili d’istanza all’oggetto che chiama il metodo.
19. Due stili di programmazione a confronto
Senza reference this:
class Dipendente {...
public void setNome(String n) {
public String getNome() {
...
nome = n;
}public String getNome() {
return nome;
}}
Con reference this:
class Dipendente {...
public void setNome(String nome) {
public String getNome() {
...
this.nome = nome;
}public String getNome() {
return this.nome;
}
}
20. Quando utilizzare l’incapsulamento?
- Non ci sono casi in cui è opportuno o meno utilizzare l’incapsulamento. Una qualsiasi classe di un qualsiasi programma dovrebbe essere sviluppata utilizzando l’incapsulamento.
- Anche se all’inizio di un progetto può sembrarci che su determinate classi usufruire dell’incapsulamento sia superfluo, l’esperienza insegna che è preferibile applicarlo in ogni situazione.
21. Ereditarietà
- L'ereditarietà è uno dei concetti fondamentali del paradigma della programmazione ad oggetti.
Essa consiste in un legame che il linguaggio di programmazione, o il programmatore stesso, stabilisce tra due classi. - Se la classe «B» eredita dalla classe «A», si dice che «B» è una classe derivata (o sottoclasse) di «A» e che «A» è una classe principale (o superclasse) di «B».
22. La parola chiave extends
- Consideriamo la seguente classe principale:
class Persona {
public String nome;
public String cognome;
public String CF; // Si intende il codice fiscale
public String numeroDiTelefono;
}public String cognome;
public String CF; // Si intende il codice fiscale
public String numeroDiTelefono;
- Adesso consideriamo quest’altra classe derivata dalla principale (senza l’utilizzo della tecnica di ereditarietà):
class Studente {
public String nome;
public String cognome;
public String CF; // Si intende il codice fiscale
public String numeroDiTelefono;
public int votoIntermedio;
public int votoFinale;
}public String cognome;
public String CF; // Si intende il codice fiscale
public String numeroDiTelefono;
public int votoIntermedio;
public int votoFinale;
- Notiamo che la classe «Persona» e la classe «Studente» rappresentano due modelli in relazione tra loro e quindi dichiarano campi in comune. L’ereditarietà permette di mettere in relazione due classi in maniera tale da implementare nella sottoclasse: le variabili pubbliche ed i metodi pubblici presenti nella superclasse. Nel nostro caso possiamo fare in modo che la classe «Studente» erediti tutte le variabili pubbliche dalla classe «Persona» senza dichiarale esplicitamente…
23. La parola chiave extends
- Alla luce di quanto affermato, la classe «Studente» deve essere riscritta in questo modo:
class Studente extends Persona {
public int votoIntermedio;
public int votoFinale;
}public int votoFinale;
- Dal codice mostrato si evince che nella classe «Studente» sono dichiarate implicitamente le variabili pubbliche presenti nella classe «Persona».
- È doveroso precisare che anche i metodi pubblici dichiarati nella superclasse saranno ereditati dalla sottoclasse; nel caso del nostro esempio, tuttavia, non è stato specificato alcun metodo pubblico nella classe «Persona».
- È evidente il risparmio di codice come anche il risparmio di tempo per quanto riguarda la definizione del modello derivato, il vantaggio è anche quello di una minor probabilità di commettere eventuali errori.
24. Ereditarietà multipla
- In Java non esiste la cosiddetta ereditarietà multipla.
- L’ereditarietà multipla permette ad una classe di ereditare da più classi contemporaneamente. In pratica non è possibile scrivere:
class Idrovolante extends Aereo, Nave {
...
}- Anche se a livello concettuale l’ereditarietà multipla esiste, non possiamo implementarla in un programma, perché potrebbero nascere delle complicazioni tali che il programmatore potrebbe non riuscire a venirne a capo.
25. La classe Object
- Bisogna notare che il compilatore (come oramai sappiamo bene) aggiunge automaticamente del codice.
- È bene sapere, quindi, che ogni classe principale è sottoclasse della classe «Object». Quando scriviamo:
class Persona {
...
}Il compilatore aggiungerà automaticamente:
class Persona extends Object {
...
}- Quindi possiamo affermare che ogni classe è direttamente o indirettamente sottoclasse della classe «Object».
26. Complicazione dell’ereditarietà multipla
Alla luce di quanto affermato:
- Chiameremo la classe «Object» classe «A».
- Chiameremo la classe «Aereo» classe «B».
- Chiameremo la classe «Nave» classe «C».
- Chiameremo la classe «Idrovolante» classe «D».
- Il principale motivo per il quale l’ereditarietà multipla non è stata implementata in Java si chiama diamond problem (problema del diamante), si tratta di un'ambiguità che si verifica quando due classi «B» e «C» ereditano dalla classe «A», e la classe «D» eredita sia da «B» che da «C». Se un metodo in «D» chiama un metodo definito in «A», da quale classe viene ereditato?
- Il problema è chiamato del diamante a causa della forma del diagramma di ereditarietà delle classi.
27. Altre complicazioni dell’ereditarietà multipla
- Se una classe chiamata «D» eredita sia da «B» che da «C» e sia in «B» come anche in «C» viene definito uno stesso metodo pubblico e/o una stessa variabile pubblica, la classe «D» da quale classe erediterà il metodo e/o variabile?
- I paradossi sono numerosi ma per non confondere le idee al lettore meno esperto, è bene specificare che in Java è consentita una particolare forma di ereditarietà multipla chiamata interfaccia.
- L’interfaccia consente di implementare il meccanismo di ereditarietà multipla senza nessun tipo di ambiguità, ma verrà presentata successivamente, in quanto questo argomento non è importante ai fini della comprensione dell’ereditarietà.
28. Quando utilizzare l’ereditarietà?
- Quando si parla di ereditarietà si è spesso convinti che basti avere un certo numero di classi che dichiarano campi in comune.
In realtà prima di implementare il meccanismo di ereditarietà bisogna effettuare anche un altro test chiamato «is-a» relationship. - Ad esempio, data una classe «Telefono» se ne potrebbe derivare la sottoclasse «Cellulare», poiché il cellulare è un caso particolare di telefono.
Questo tipo di relazione supera il test «is-a» (è-un): "un cellulare è-un telefono". - La relazione «is-a» che deve legare una sottoclasse alla sua superclasse viene spesso esplicitata facendo riferimento al cosiddetto principio di sostituzione di Liskov, introdotto nel 1993 da Barbara Liskov e Jeannette Wing. Secondo questo principio, gli oggetti appartenenti a una sottoclasse devono essere in grado di esibire tutti i comportamenti e le proprietà esibiti da quelli appartenenti alla superclasse, in modo tale che usarli in luogo di questi ultimi non alteri la correttezza delle informazioni restituite dal programma. Affinché la classe «Cellulare» possa essere concepita come sottoclasse di «Telefono», per esempio, occorre che un cellulare possa essere usato in tutti i contesti in cui si richiede l'uso di un telefono.
- La relazione «is-a» non richiede che la sottoclasse esponga solo le caratteristiche esibite dalla superclasse. Per esempio, il fatto che un cellulare possa anche inviare SMS non inficia il fatto che esso sia sostituibile a un telefono. Pertanto, la sottoclasse può avere caratteristiche aggiuntive rispetto alla superclasse.
29. Rapporto ereditarietà-incapsulamento
- Dal momento che l’incapsulamento è obbligatorio, mentre l’ereditarietà è un importante aiuto nello sviluppo della nostra applicazione, bisognerà chiedersi cosa comporta l’utilizzo combinato di entrambi i paradigmi.
- Innanzitutto possiamo asserire che da una classe principale incapsulata (con variabili private e metodi pubblici), vengono ereditati in una classe derivata (come dovrebbe essere chiaro al lettore) solo e soltanto i metodi pubblici.
- Quindi una classe derivata che eredita da una classe principale incapsulata, non può agire direttamente sulle variabili private della classe principale ma può comunque accedervi indirettamente invocando eventuali metodi pubblici presenti nella superclasse.