Nella classe rettangolo
(ved. lezione scorsa)
gli attributi erano a valori di tipi base (interi).
In generale gli attributi possono essere a valori oggetti di altre
(o anche della stessa!) classe.
Clientship (clientela) =
quando una classe definisce un attributo che e' di tipo classe.
La classe A che ha un attributo di classe B e' cliente di B.
Un oggetto di classe A ha per sotto-componente un oggetto di classe B.
class RectanglePair { Rectangle first; Rectangle second; ... }
Ogni classe ha un costruttore di default che crea una nuova istanza
con tutti gli attributi inizializzati a valore nullo, poi posso
definire un costruttore io...
(ved. lezione scorsa).
Valore nullo e' zero per i tipi numerici, false per il tipo boolean.
Per i tipi classe e' null, l'oggetto nullo.
Tra oggetti e' definito l'operatore booleano ==.
Tale operatore confronta gli OID, cioe' le identita' dei due oggetti (le locazioni di memoria).
Risulta vero se hanno la stessa identita' = stessa locazione
di memoria.
Facciamo riferimento al frammento di codice in
lezione scorsa.
Es. dopo istruzione 4, r1==r3 vale vero.
Se due oggetti hanno stato uguale (stesso contenuto della locazione di memoria) ma non stessa identita' (non stessa locazione) risulta falso. Es. dopo istruzione 3, r1==r2 vale falso.
Se voglio funzione che controlli quando due oggetti diversi hanno uguale stato, cioe' che confronti il contenuto delle locazioni di memoria, devo definirla.
Esempio:
boolean isEqual(Rectangle r) { return ( (r.length==length) && (r.width==width) ); }Si chiama con r1.isEqual(r2); Uno dei due rettangoli e' argomento privilegiato (asimmetria).
Alternativa con metodo di classe:
static boolean isEqual(Rectangle r1, Rectangle r2) { return ( (r1.length==r2.length) && (r1.width==r2.width) ); }Simmetria di trattamento fra i due argomenti. Si chiama con Rectangle.isEqual(r1,r2); Oppure con r.isEqual(r1,r2); dove r e' qualsiasi istanza della classe Rectangle (ma quest'ultimo modo e' sconsigliato).
Essendo gli attributi length e width di tipi base (tipo int), le locazioni di memoria di due rettangoli contengono valori base. Percio' l'operatore == nel corpo di isEqual non pone problemi.
Se invece gli attributi fossero oggetti di una classe, si porrebbe il problema se la loro uguaglianza vada controllata con == direttamente oppure di nuovo con isEqual. Otteniamo due diverse nozioni di uguaglianza:
class RectanglePair { Rectangle first; Rectangle second; ... boolean shallowEqual(RectanglePair rp) { return ( (first==rp.first) && (second==rp.second) ); } boolean deepEqual(RectanglePair rp) { return ( first.isEqual(rp.first) && second.isEqual(rp.second) ); } }Shallow: risulta vero se rp e' coppia con esattamente gli stessi rettangoli che ha questa coppia.
Come ci sono varie nozioni di uguaglianza, ci sono anche varie nozioni di copia di oggetti.
Come per shallow / deep equality, se voglio shallow / deep copy devo definire io una funzione che esegue la copia.
Shallow e deep copy coincidono.
Esempio: classe Rectangle,
funzione di copia che crea altro rettangolo uguale a questo:
Rectangle copy() { return new Rectangle(length,width); }Poi posso chiamare r3 = r1.copy(); Dopo, r1==r3 vale falso, ma Rectangle.isEqual(r1,r3) vale vero.
Di solito si scrive un costruttore di copia:
Rectangle(Rectangle r) { length = r.length; width = r.width; }Poi posso chiamare r3 = new Rectangle(r1); Costruisce un oggetto copia.
Se gli attributi sono a loro volta oggetti, la copia puo' essere shallow o deep.
Possiamo scrivere funzioni di copia shallow e deep per classe RectanglePair.
Una classe puo' essere cliente di se stessa. Puo' avere come attributi oggetti della stessa classe (classe definita ricorsivamente).
Un treno e' fatto di tanti vagoni.
Ogni vagone ha un numero e puo' "agganciare" un altro vagone.
Il treno aggancia il primo vagone. Ha inoltre un tipo di treno
(regionale, intercity ecc.), un numero di treno,
una stazione di partenza e una di arrivo.
class Vagone { int numero; /* numero di carrozza */ Vagone prossimo; /* collegamento al prossimo vagone */ Vagone(int num) /* crea il vagone isolato */ { numero = num; prossimo = null; } void aggancia(Vagone vag) { prossimo = vag; } } class Treno { static final int REGIONALE = 1; static final int INTERCITY = 2; /* eccetera per altri tipi di treno... */ int tipo; /* tipo di treno */ int numero; /* numero del treno */ Vagone primo; /* collegamento al primo vagone */ String partenza; /* nome della stazione di partenza */ String arrivo; /* nome della stazione di arrivo */ Treno(int t, int n, String par, String arr) /* crea treno senza vagoni */ { tipo = t; numero = n; partenza = par; arrivo = arr; primo = null; } void aggancia(Vagone vag) { primo = vag; } }
La classe Treno e' cliente di Vagone. Vagone e' cliente di se stessa. Vagone e' classe ricorsiva perche' contiene riferimento a se stessa.
Esempio di uso:
1: Treno t = new Treno(Treno.INTERCITY, 1267, "Genova", "Ventimiglia"); 2: Vagone ultimo; 3: Vagone v1 = new Vagone(210); 4: t.aggancia(v1); 5: ultimo = v1; 6: v1 = new Vagone(211); 7: ultimo.aggancia(v1); 8: ultimo = v1; /* posso attaccare altri vagoni ripetendo le linee di codice 6,7,8 con diversi numeri di carrozza */Aggancio prima il primo vagone e poi dietro tutti gli altri, ogni volta agganciando un nuovo vagone all'ultimo vagone. La variabile ultimo punta sempre l'ultimo vagone e viene spostata ogni volta. La variabile v1 serve a creare un nuovo vagone, che poi sara' agganciato e diventera' l'ultimo.
Attenzione:
Il treno stampa il suo tipo, il suo numero, le due stazioni
di partenza e arrivo, poi dovrebbe stampare i vagoni...
Il treno dice al primo vagone di stampare se stesso.
Questo pensera' a far stampare i vagoni che gli si trovano agganciati
dietro, fino all'ultimo.
Ogni vagone per stamparsi stampa il suo numero di carrozza, poi
se ha agganciato un altro vagone dice a questo di stamparsi,
se invece non ha agganciato nulla (ultimo vagone) non fa altro.
Nella classe Vagone:
void print() { System.out.println("carrozza numero " + numero); if (prossimo!=null) prossimo.print(); }
La stampa di un vagone e' funzione ricorsiva. Ciascun vagone stampa quello seguente, il quale stampa quello dopo ancora... e cosi' ricorsivamente... ci si ferma quando si incontra il vagone nullo.
Nella classe Treno:
void print() { System.out.print("Treno "); switch (tipo_treno) { case REGIONALE: System.out.print("regionale"); break; case INTERCITY: System.out.print("intercity"); break; default: System.out.print("non classificato"); break; } System.out.println(" numero " + numero + " vagoni:"); if (primo!=null) primo.print(); }
Il treno ha tutto cio' che ha un vagone: un numero, un vagone agganciato,
funzioni per aggancio e sgancio.
In piu' ha tipo del treno e le stazioni di partenza e arrivo.
Possiamo rifare il treno come sottoclasse del vagone.
class Treno extends Vagone { static final int REGIONALE = 1; static final int INTERCITY = 2; /* eccetera per altri tipi di treno... */ int tipo_treno; // il numero viene ereditato dal vagone // per primo vagone viene usato l'attributo "prossimo" del vagone String partenza; String arrivo; Treno(int t, int n, String par, String arr) /* crea treno senza vagoni */ { super(n); // chiama costruttore del vagone tipo = t; partenza = par; arrivo = arr; } // metodo aggancia e' ereditato da Vagone void print() /* metodo print e' ereditato da Vagone ma re-implementato */ { System.out.print("Treno "); switch (tipo) { case REGIONALE: System.out.print("regionale"); break; case INTERCITY: System.out.print("intercity"); break; default: System.out.print("non classificato"); break; } System.out.println(" numero " + numero + " vagoni:"); if (prossimo!=null) prossimo.print(); } }
Vale la relazione "is-a" per treno e vagone: un treno e' anche un vagone!