La modalità di creare oggetti che vediamo in questo capitolo è quella che ci permette sia di scrivere meno codice ripetitivo che di (potenzialmente) avere meno errori creando modelli della realtà e utilizzando parti di programma già scritte.
Le persone, gli studenti e i professori
Una realtà che ci è familiare è quella scolastica: vogliamo rappresentare
le persone che hanno a che fare con la scuola tramite la classe Persona
.
Queste persone possono essere i commessi del bar, il personale negli uffici,
quelli che fanno il servizio al corridoio, il dirigente e così via.
Di queste persone ci interessa il nome, il cognome, l'anno di nascita e il numero di telefono.
La classe, con relativi costruttori, sarà una cosa del tipo:
public class Persona { public String nome; public String cognome; public int annoNascita; public String telefono; public Persona() { nome = "<indefinito>"; cognome = "<indefinito>"; annoNascita = -1; telefono = "<indefinito>"; } public Persona(String nome, String cognome, int annoNascita, String telefono) { this.nome = nome; this.cognome = cognome; this.annoNascita = annoNascita; this.telefono = telefono; } }
Prendiamo in considerazione ora gli alunni: senza dubbio sono persone! Sono persone un po' particolari perché oltre le caratteristiche comuni a tutte le persone, questi frequentano una classe, possono frequentare le lezioni di religione oppure di attività alternativa e quindi possiamo individuare le nuove proprietà: la prima è la classe frequentata, l'altra è la situazione in merito alla religione. Nei linguaggi ad oggetti possiamo indicare il fatto che gli studenti sono persone con particolari caratteristiche tramite il concetto di ereditarietà: si va a creare una nuova classe, in questo caso Studente, e questa non la si fa a partire da zero ma si fa in modo che abbia già in partenza tutto ciò che è nella classe Persona. In altre parole la classe Studente estende la classe Persona. Esplicitiamo tutti questi concetti tramite il seguente:
public class Studente extends Persona { public String classe; public boolean religione; // false in caso si avvalga dell'attività alternativa public Studente() { super(); // Per la spiegazione di super() guarda il paragrafo che segue classe = "<non assegnata>"; religione = true; } public Studente(String nome, String cognome, int annoNascita, String classe, boolean religione) { super(nome, cognome, annoNascita, ""); this.classe = classe; this.religione = religione; } }
In questo esempio Persona
è la superclasse
(o classe padre) di Studente
che viene detta classe figlia.
La parola super
sta ad indicare la superclasse,
la chiamata del metodo super()
è la chiamata del costruttore della superclasse.
Si utilizza super()
in modo tale da invocare il costruttore per le proprietà definite
nella superclasse (che magari potrebbero essere private),
lasciando qui il solo lavoro di dare un valore alle ultime definite.
Si osserva che ci sono 2 invocazioni di super()
: una senza argomenti per chiamare
il primo costruttore di Persona
, quello senza argomenti
e l'altra con quattro argomenti per chiamare il secondo costruttore di Persona
, quello con quattro argomenti.
Osserviamo che nelle 2 classi precedenti tutti i metodi e le proprietà sono definiti pubblici.
Nel capitolo oggetti, al paragrafo Organizzare gli oggetti, si diceva però
che lasciare le proprietà e alcuni particolari metodi liberamente usabili da altre classi potrebbe
non essere un bene. Mettendo il modificatore private
sulle
proprietà della classe si otterrebbe un errore in quanto la classe estesa non potrebbe
accedere alle proprietà della classe padre. La classe derivata è a tutti gli effetti
un'altra classe e quindi nulla traspare.
La soluzione viene nella parola chiave protected
:
sta ad indicare che quanto segue (metodo o proprietà) è visibile solo a tutti i metodi della classe stessa
(ovviamente), a tutte le classi che estendono questa e a tutte quelle classi presenti nello stesso pacchetto.
Nel momento in cui andremo a dichiarare oggetti, dovremo porreal livello di visibilità di ogni
metodo e in particolare delle proprietà.
La classe persona a questo punto sarà:
public class Persona { protected String nome, cognome; protected String ruolo; protected float stipendio; protected boolean categoria; public Persona() { nome = "<indefinito>"; cognome = "<indefinito>"; annoNascita = -1; telefono = "<indefinito>"; } public Persona(String nome, String cognome, int annoNascita, String telefono) { this.nome = nome; this.cognome = cognome; this.annoNascita = annoNascita; this.telefono = telefono; } /* da aggiungere i metodfi get/set per le proprietà */ }
Allo stesso modo si potrà realizzare la classe Professore
estensione di Persona
.
public class Professore extends Persona { protected String materia; protected double stipendio; public Professore() { super(); materia = "?"; stipendio = 0; } public Professore(String nome, String cognome, String materia) { super(nome, cognome, -1, ""); this.materia = materia; } }
Prevalenza
Aggiungiamo un metodo a ciascuna classe per avere una stringa che contenga
in un testo discorsivo tutte le informazioni dell'oggetto. Il metodo avrà, per comodità, lo
stesso nome in tutte le implementazioni: lo chiameremo miPresento()
.
/* nell'oggetto Persona */ public String miPresento() { return "Sono " + nome + " " + cognome + ", nato nel " + annoNascita + " [tel: " + telefono + "]"; }
/* nell'oggetto Professore */ public String miPresento() { return "Sono il prof." + nome + " " + cognome + ", insegno "+materia; }
Nel momento in cui una classe e la sua superclasse presentano ciascuna un metodo
con lo stesso nome e stessi tipi e sequenza di parametri,
il metodo della classe nasconde quello della superclasse, quindi creando un oggetto di tipo
Professore verrà eseguito il miPresento()
definito in Professore
e non quello definito in Persona.
Nell'implementazione che segue, quella di Studente
,
è presente l'annotazione @Override
che sta ad informare il compilatore che il metodo che
segue ha la stessa firma (quindi stesso nome del metodo e stesssa sequenza di tipi di parametri)
di quello nella superclasse. Tale indicazione non è abbligatoria ma ci aiuta
a evitare di scrivere male il nome del metodo e la sequenza dei tipi dei parametri.
@Override public String miPresento() { return "Sono " + nome + " " + cognome + ", frequento la classe "+classe; }
Se si provasse a modificare qualcosa lla firma del metodo miPresento()
come il nome o aggiungendo un qualsiasi parametro, si otterrebbe un errore.
Un metodo che si comporta come miPresento()
viene normalemente chiamato
toString()
ed è definito nella classe java.lang.Object
,
moltissime classi lo ridefiniscono ed è il metodo che viene usato per rappresentare
come stringa qualsiasi oggetto, lo chiameremo sempre così anche noi, quello
definito in Object
stampa il nome della classe e un codice numerico identificativo
dell'oggetto stesso.
In sintesi
Quando quello che vogliamo fare non è creare una nuova classe ma aggiungere/modificare delle funzionalità di una oggetto esistente va usata la parola
chiave extends
nella dichiarazione della classe seguita dal nome della classe che si vuole ampliare/cambiare. In questo caso l'oggetto definito avrà
tutte le proprietà e tutti i comportamenti di quello originale con le aggiunte o le modifiche indicate nella classe.
Ogni classe ne estende obbligatoriamente un'altra, in caso che la parola chiave
extends
non sia presente la classe che si sta definendo extenderà
java.lang.Object
. Ovviamente questo meccanismo forma delle catene:
la classe A estende la classe B che a sua volta estende C e così via. Quando
si invoca un metodo nell'oggetto di tipo A questo potrebbe essere definito
sia in A... che in B che in C. Java cercherà prima il metodo in A,
se non c'è guarderà in B, se non c'è guarderà in C e alla fine in Object.
super
è il riferimento alla classe padre o superclasse e quindi
super()
è il riferimento al relativo costruttore, all'interno delle parentesi ci
saranno gli argomenti del caso.
Una ulteriore attenzione nel momento della progettazione di una classe va fatta su quali proprietà e metodi si vuole siano visibili alle classi che estenderanno la classe che ora stiamo costruendo. Può essere che alcune proprietà e/o metodi preferiamo renderli privati e quindi la classe che andrà ad estendere estesa non potrà usarli direttamente.
Quanti tipi ha un oggetto?
La risposta immediata è: uno! Purtroppo però non è del tutto vero: ovviamente un oggetto è del tipo che si è scritto nella dichiarazione:
Studente mario = new Studente();
mario
è di tipo Studente
ma... non è anche una
Persona
? Si, lo è, in effetti è perfettamente legale scrivere sotto alla riga precedente
Persona anonimo = mario;
In questo modo però utilizzando la variabile anonimo
potrò utilizzare soltanto i metodi definiti in Persona
o una sua superclasse,
non più quelli di Studente
.
È altresì possibile usare la variabile mario
in tutti i contesti in cui
serve di usare una Persona
. Questo spiega perché usando JavaFX per aggiungere
un elemento ad una griglia usando il metodo add()
si possono inserire molte cose
diverse, la dichiarazione del metodo dice:
add(Node figlio, int indiceColonna, int indiceRiga)
Sia Button
che Label
sono sottoclassi di Node
quindi possono essere utilizzate nel metodo add, in verità Label (e neanche Button)
non è una sottoclasse diretta di Node, la gerarchia completa è:
Object→Node→Parent→Region→Control→Labeled→Label Però vista la gerarchia
Label
è anche un Node
.
Risparmio vincolato pluriennale
Riprendiamo l'esempio del risparmio vincolato. In quel caso la somma che si otteneva era data alla fine dell'anno di investimento. Ipotizziamo però che riusciamo a
trattenerci dallo spendere quei soldi e li manteniamo vincolati per un certo numero di anni, diciamo da 1 a 7. Quale sarà la somma totale alla fine?
Di certo ci appoggeremo al codice già scritto nella classe RisparmioVincolato
e quindi creeremo una classe estesa che eredita quanto fatto in
RisparmioVincolato
. Osserviamo che non c'è bisogno di accedere direttamente alle proprietà della superclasse perché basta usare i getter per prenderne i valori
ed i setter per impostarli:
public class RisparmioVincolatoPluriennale extends RisparmioVincolato { private byte durataAnni; //facendola privata eventuali classi che estenderanno questa non avranno accesso alla proprietà. Una scelta. public RisparmioVincolatoPluriennale() { super(); durataAnni = 1; } public RisparmioVincolatoPluriennale(String nome, String cognome, float importo, short anno, double interessiPercentuale, byte durataAnni) { super(nome, cognome, importo, anno, interessiPercentuale); if(durataAnni >=1 && durataAnni <=7) { this.durataAnni = durataAnni; } else { this.durataAnni = 1; } } double fornisciCapitale() { if(durataAnni == 0) { return 0; } // La percentuale di interessi che viene pubblicizzata è riferita ad un periodo di un anno. // Invochiamo il metodo della superclasse che calcola il nuovo capitale a fine periodo, per il numero di periodi indicato double importo = getImporto(); for(int i = 0; i < durataAnni; i++) { setImporto(super.fornisciCapitale()); // viene invocato il metodo della superclasse setImporto() aggiornando il capitale sul quale si fa il calcolo // deve essere invocato fornisciCapitale() della classe padre, per indicare questo fatto si usa super } double capitale = getImporto(); // metodo della classe padre setImporto(importo); // si rimette l'importo al valore iniziale, come se nulla fosse cambiato return capitale; } }
Esercizio
Primo.java
package it.esempio; public class Primo { boolean p; int x; public int leggiVal(){ return x; } public String descrivi(){ return "n:"+x; } }
Secondo.java
package it.esempio; public class Secondo extends Primo { String c; public Secondo(int k, boolean p, String u){ x=k; y=p; c=u; } public String descrivi(){ return c+x+c; } }
Considera il seguente frammento di programma inserito in un'altra classe:
Secondo o;
o = new Secondo(3,false,"#");
String m=o.descrivi();
Quanto vale m?