1
Riflessione:
Ordinamento di oggetti
SETTIMANA 7
Ordinamento di oggetti
Si sono visti algoritmi di ordinamento su array di numeri
Ma spesso bisogna ordinare dati più complessi
ad esempio stringhe, ma anche oggetti di altro tipo
Gli algoritmi di ordinamento che abbiamo esaminato effettuano confronti tra numeri
Si possono usare gli stessi algoritmi per ordinare oggetti, a patto che questi siano tra loro confrontabili
C’è però una differenza
confrontare numeri ha un significato matematico ben definito
confrontare oggetti ha un significato che dipende dal tipo di oggetto, e a volte può non avere significato alcuno
3
Ordinamento di oggetti
Confrontare oggetti ha un significato che dipende dal tipo di oggetto
quindi la classe che definisce l’oggetto deve anche definire il significato del confronto
Consideriamo la classe String
essa definisce il metodo compareTo che attribuisce un significato ben preciso all’ordinamento tra stringhe
• l’ordinamento lessicografico
Possiamo quindi riscrivere, ad esempio, il metodo selectionSort per ordinare stringhe invece di
ordinare numeri, senza cambiare l’algoritmo
SelectionSort per stringhe
public class ArrayAlgs{...
public static void selectionSort(String[] v, int vSize) {
for (int i = 0; i < vSize - 1; i++)
{ int minPos = findMinPos(v, i, vSize-1);
if (minPos != i) swap(v, minPos, i);
}
} //abbiamo usato due metodi ausiliari, swap e findMinPos private static void swap(String[] v, int i, int j)
{ String temp = v[i];
v[i] = v[j];
v[j] = temp;
}
private static int findMinPos(String[] v, int from, int to) { int pos = from;
String min = v[from];
for (int i = from + 1; i <= to; i++) if (v[i].compareTo(min) < 0) { pos = i;
min = v[i]; } return pos;
} ...
5
Ordinamento di oggetti
Si possono riscrivere tutti i metodi di ordinamento e ricerca visti per i numeri interi ed usarli per le stringhe
Ma come fare per altre classi?
Possiamo ordinare oggetti di tipo BankAccount in ordine, ad esempio, di saldo crescente?
• Bisogna definire nella classe BankAccount un metodo analogo al metodo compareTo della classe String
• Bisogna riscrivere i metodi perché accettino come parametro un array di BankAccount
Non possiamo certo usare questo approccio per
qualsiasi classe, deve esserci un sistema migliore!
In effetti c'è, ma dobbiamo prima studiare l'ereditarietà e l'uso di interfacce in Java ...
... poi torneremo sul problema di ordinare oggetti generici
7
Ereditarietà (capitolo 10)
L’ereditarietà
L’ereditarietà è uno dei principi basilari della programmazione orientata agli oggetti
L’ereditarietà è un paradigma che supporta l’obiettivo di riusabilità del codice
si usa quando si deve realizzare una classe ed è già disponibile un’altra classe che rappresenta un
concetto più generale
9
L’ereditarietà
Supponiamo di voler realizzare una classe SavingsAccount per rappresentare un conto bancario di risparmio,
Dovrà avere un tasso di interesse annuo determinato al momento dell’apertura
Dovrà avere un metodo addInterest per accreditare gli interessi sul conto
Un conto bancario di risparmio ha tutte le stesse caratteristiche di un conto bancario, più alcune altre caratteristiche che gli sono peculiari
Allora possiamo riutilizzare il codice già scritto per la classe BankAccount
La classe BankAccount
public class BankAccount { public BankAccount() { balance = 0;
}
public void deposit(double amount) { balance = balance + amount;
}
public void withdraw(double amount) { balance = balance - amount;
}
public double getBalance() { return balance;
}
private double balance;
11
La classe SavingsAccount
public class SavingsAccount
{ public SavingsAccount(double rate) { balance = 0;
interestRate = rate;
} public void addInterest()
{ deposit(getBalance() * interestRate / 100);
}
private double interestRate;
public void deposit(double amount) { balance = balance + amount;
} public void withdraw(double amount) { balance = balance - amount;
} public double getBalance() { return balance;
}
private double balance;
}
Riutilizzo del codice
Come avevamo previsto, buona parte del codice scritto per BankAccount ha potuto essere copiato nella definizione della classe SavingsAccount
Abbiamo poi fatto tre cambiamenti
abbiamo aggiunto una variabile di esemplare
• interestRate
abbiamo modificato il costruttore
• ovviamente il costruttore ha anche cambiato nome
abbiamo aggiunto un metodo
• addInterest
13
Riutilizzo del codice
Copiare il codice è già un risparmio di tempo, però non è sufficiente
Cosa succede se modifichiamo BankAccount?
ad esempio, modifichiamo withdraw in modo da impedire che il saldo diventi negativo
Dobbiamo modificare di conseguenza anche SavingsAccount
molto scomodo…
fonte di errori…
L’ereditarietà risolve questi problemi
Riscriviamo la classe SavingsAccount usando il meccanismo dell'ereditarietà
Dichiariamo che SavingsAccount è una classe derivata da BankAccount (extends)
eredita tutte le caratteristiche di BankAccount
specifichiamo soltanto le peculiarità di SavingsAccount
SavingsAccount
public class SavingsAccount extends BankAccount { public SavingsAccount(double rate)
{ interestRate = rate;
} public void addInterest()
{ deposit(getBalance() * interestRate / 100);
} private double interestRate;
15
Come usare la classe derivata
Oggetti della classe derivata SavingsAccount si usano come se fossero oggetti di BankAccount
con qualche proprietà in più
La classe derivata si chiama
sottoclasse
La classe da cui si deriva si chiama superclasse
SavingsAccount acct = new SavingsAccount(10);
acct.deposit(500);
acct.withdraw(200);
acct.addInterest();
System.out.println(acct.getBalance());
330
Osservazioni su SavingsAccount
Il metodo addInterest usa i metodi getBalance e deposit di BankAccount
Non specifica un parametro implicito
Cioè il parametro implicito è this
Il metodo addInterest invoca getBalance e deposit invece che leggere/scrivere direttamente il campo balance
Questo perché il campo balance è definito come private in BankAccount
Mentre addInterest è definito in SavingsAccount e non ha accesso a balance
17
La superclasse universale Object
In Java, ogni classe che non deriva da nessun’altra deriva implicitamente dalla superclasse
universale del linguaggio, che si chiama Object
Quindi, SavingsAccount deriva da BankAccount, che a sua volta deriva da Object
Object ha alcuni metodi, che vedremo più avanti (tra cui toString), che quindi sono ereditati da tutte le classi in Java
l’ereditarietà avviene anche su più livelli, quindi
SavingsAccount eredita anche le proprietà di Object
Ereditarietà
Sintassi:
Scopo:
definire la classe NomeSottoclasse che deriva dalla classe NomeSuperclasse
definendo nuovi metodi e/o nuove variabili, oltre ai suoi costruttori
Nota: una classe estende sempre una sola altra classe
Nota: se la superclasse non è indicata esplicitamente, il compilatore usa implicitamente java.lang.Object
class NomeSottoclasse extends NomeSuperclasse { costruttori
nuovi metodi nuove variabili }
19
Terminologia e notazione
Confondere superclassi e sottoclassi
Dato che oggetti di tipo SavingsAccount sono
un’estensione di oggetti di tipo BankAccount
più “grandi” di oggetti di tipo BankAccount, nel senso che hanno una variabile di esemplare in più
più “abili” di oggetti di tipo BankAccount, perché hanno un metodo in più
perché mai SavingsAccount si chiama sottoclasse e non superclasse?
è facile fare confusione
verrebbe forse spontaneo usare i nomi al contrario...
21
Confondere superclassi e sottoclassi
I termini superclasse e sottoclasse derivano dalla teoria degli insiemi
I conti bancari di risparmio (gli oggetti di tipo
SavingsAccount) costituiscono un sottoinsieme dell’insieme di tutti i conti bancari (gli oggetti di tipo BankAccount)
BankAccount SavingsAccount
Diagrammi e gerarchie di ereditarietà
Abbiamo già visto diagrammi di classi per visualizzare accoppiamento tra classi
In un diagramma di classi l’ereditarietà viene rappresentata mediante una freccia con la punta a “triangolo vuoto”, diretta verso la superclasse.
Abbiamo già visto diagrammi di ereditarietà…
La gerarchia delle classi di eccezioni
Le gerarchie delle classi del pacchetto io
• InputStream, OutputStream, Reader, Writer
23
Gerarchia delle eccezioni
È tutto chiaro? …
1. Quali sono i campi di esemplare presenti in un oggetto di tipo SavingsAccount?
2. Elencare quattro metodi che possono essere invocati con un oggetto di tipo SavingsAccount
3. Se la classe Manager estende Employee, qual
è la superclasse e quale la sottoclasse?
25
Metodi di una sottoclasse
Metodi di una sottoclasse
Quando definiamo una sottoclasse, possono verificarsi tre diverse situazioni per quanto riguarda i suoi metodi
nella sottoclasse viene definito un metodo che nella superclasse non esisteva
• Ad esempio il metodo addInterest di SavingsAccount
un metodo della superclasse viene ereditato dalla sottoclasse
• Ad esempio i metodi deposit, withdraw, getBalance di SavingsAccount, ereditati da BankAccount
un metodo della superclasse viene sovrascritto nella sottoclasse
• Vediamo ora un esempio anche di questo caso
27
Sovrascrivere un metodo
La possibilità di sovrascrivere (override) un metodo della superclasse, modificandone il comportamento quando è usato per la sottoclasse, è una delle
caratteristiche più potenti di OOP
Per sovrascrivere un metodo bisogna definire nella sottoclasse un metodo con la stessa firma di quello definito nella superclasse
tale metodo prevale su quello della superclasse quando viene invocato con un oggetto della
sottoclasse
Sovrascrivere un metodo: esempio
Vogliamo modificare la classe SavingsAccount in modo che ogni operazione di versamento abbia un costo (fisso) FEE, che viene automaticamente
addebitato sul conto
I versamenti nei conti di tipo SavingsAccount si fanno però invocando il metodo deposit di
BankAccount, sul quale non abbiamo controllo
Possiamo però sovrascrivere deposit, ridefinendolo in SavingsAccount
In più aggiungiamo una costante di classe FEE, che contenga l'importo del costo fisso da addebitare
29
Sovrascrivere un metodo: esempio
Sovrascriviamo il metodo deposit
Quando viene invocato deposit con un oggetto di tipo SavingsAccount, viene invocato il metodo deposit definito in SavingsAccount e non quello definito in BankAccount
nulla cambia per oggetti di tipo BankAccount, ovviamente
public class SavingsAccount extends BankAccount { ...
public void deposit(double amount) { withdraw(FEE);
... // aggiungi amount a balance } ...
private final static double FEE = 2.58; // euro }
Sovrascrivere un metodo: esempio
Proviamo a completare il metodo
dobbiamo versare amount e sommarlo a balance
non possiamo modificare direttamente balance, che è una variabile privata in BankAccount
l’unico modo per aggiungere una somma di denaro a balance è l’invocazione del metodo deposit
Così non funziona, perché il metodo diventa ricorsivo con ricorsione infinita!!! (manca il caso base…)
public class SavingsAccount extends BankAccount { ...
public void deposit(double amount) { withdraw(FEE);
deposit(amount); // NON FUNZIONA }}
31
Sovrascrivere un metodo: Esempio
Ciò che dobbiamo fare è invocare il metodo deposit di BankAccount
Questo si può fare usando il riferimento implicito super, gestito automaticamente dal compilatore per accedere agli elementi ereditati dalla superclasse
public class SavingsAccount extends BankAccount { ...
public void deposit(double amount) { withdraw(FEE);
super.deposit(amount); //invoca deposit della superclasse }}
Invocare un metodo della superclasse
Sintassi:
Scopo: invocare il metodo nomeMetodo della
superclasse anziché il metodo con lo stesso nome (sovrascritto) della classe corrente
super è un riferimento all'oggetto parametro implicito del metodo, come il riferimento this
super viene creato al momento dell'invocazione del metodo, come il riferimento this
Però super tratta l'oggetto a cui si riferisce come se fosse un esemplare della superclasse: questa è l'unica differenza tra super e this
super.nomeMetodo(parametri)
33
Campi di esemplare di una sottoclasse
Ereditare campi di esemplare
Quando definiamo una sottoclasse, possono
verificarsi solo due diverse situazioni per quanto riguarda i suoi campi di esemplare
nella sottoclasse viene definito un campo di esemplare che nella superclasse non esisteva
• Ad esempio il campo interestRate di SavingsAccount
un campo della superclasse viene ereditato dalla sottoclasse
• Ad esempio il campo balance di SavingsAccount, ereditato da BankAccount
Non è possibile sovrascrivere un campo della superclasse nella sottoclasse
35
Ereditare campi di esemplare
Cosa succede se nella sottoclasse viene definito un campo omonimo di uno della superclasse?
È un’operazione lecita
Ma è sconsigliabile
Un oggetto della sottoclasse avrà due campi di
esemplare con lo stesso nome ma in generale valori diversi
In particolare il nuovo campo di esemplare mette in ombra il suo omonimo della superclasse
Ereditare campi di esemplare
In questo caso la compilazione ha successo
Ma viene aggiornata la variabile balance sbagliata!
Gli altri metodi, ereditati da BankAccount, faranno riferimento alla variabile balance ereditata da
BankAccount
public class SavingsAccount extends BankAccount { ...
public void deposit(double amount) { withdraw(FEE);
balance = balance + amount;
} ...
private double balance; // Meglio non farlo!
}
37
Mettere in ombra campi ereditati
Perchè Java presenta questa apparente “debolezza”?
Chi scrive una sottoclasse non deve conoscere niente di quanto dichiarato private nella superclasse, né scrivere codice che si basa su tali caratteristiche: incapsulamento!
quindi è perfettamente lecito definire e usare una variabile balance nella sottoclasse
• sarebbe strano impedirlo: chi progetta la sottoclasse non sa che esiste una variabile balance nella superclasse!
Ma non si può usare nella sottoclasse variabili omonime di quelle della superclasse e sperare che siano la stessa cosa
Costruttori di una
sottoclasse
39
Costruttori di una sottoclasse
Che ne è delle altre variabili di esemplare?
In questo caso balance
Possiamo utilizzare i costruttori della superclasse per inizializzarle?
public class SavingsAccount extends BankAccount { public SavingsAccount(double rate)
{
interestRate = rate;
} ...
}
Costruttori di una sottoclasse
Come per i metodi, possiamo usare il riferimento super per riferirci ai costruttori della superclasse
La parola chiave super seguita da parentesi tonde indica una chiamata al costruttore della superclasse
Tale chiamata deve essere il primo enunciato del costruttore della sottoclasse
public class SavingsAccount extends BankAccount { public SavingsAccount(
double initialBalance, double rate) { super(initialBalance);
interestRate = rate;
}...
}
41
Costruttori di una sottoclasse
E se non invochiamo un costruttore della superclasse?
Allora viene invocato il costruttore predefinito della superclasse (quello privo di parametri espliciti)
Se questo non esiste (tutti i costruttori della
superclasse richiedono parametri espliciti) viene generato un errore in compilazione
public class SavingsAccount extends BankAccount { public SavingsAccount(double rate)
{ //super(); invocato automaticamente interestRate = rate;
}...
}
È tutto chiaro? …
1. Perché il costruttore di SavingsAccount visto prima non invoca il costruttore della
superclasse?
2. Quando si invoca un metodo della superclasse
usando l’enunciato super, l’invocazione deve
essere il primo enunciato del metodo della
sottoclasse?
43
Esercizio: la classe CheckingAccount
La classe CheckingAccount
public class CheckingAccount extends BankAccount { public CheckingAccount(double initialBalance)
{ super(initialBalance); // costruisci la superclasse transactionCount = 0; // azzera conteggio transaz.
}
public void deposit(double amount) //SOVRASCRITTO!!
{ transactionCount++;
super.deposit(amount); // aggiungi amount al saldo }
public void withdraw(double amount) //SOVRASCRITTO!!
{ transactionCount++;
super.withdraw(amount); // sottrai amount dal saldo } // continua
45
La classe CheckingAccount
// continua public void deductFees() //NUOVO METODO
{ if (transactionCount > FREE_TRANSACTIONS) {
double fees = TRANSACTION_FEE *
(transactionCount – FREE_TRANSACTIONS);
super.withdraw(fees);
}
transactionCount = 0;
}
//nuovi campi di esemplare private int transactionCount;
private static final int FREE_TRANSACTIONS = 3;
private static final double TRANSACTION_FEE = 2.0;
}
La superclasse BankAccount
public class BankAccount { public BankAccount() { balance = 0; }
public BankAccount(double initialBalance) { balance = initialBalance; }
public void deposit(double amount) { balance = balance + amount; } public void withdraw(double amount) { balance = balance - amount; } public double getBalance()
{ return balance; }
public void transfer(double amount, BankAccount other) { withdraw(amount);
other.deposit(amount);
} private double balance;
47
È tutto chiaro? …
1. Perché il metodo withdraw di CheckingAccount invoca super.withdraw?
2. Perché il metodo deductFees pone a zero il conteggio delle transazioni effettuate?
Conversioni di tipo tra
superclasse e sottoclasse
49
Conversione fra riferimenti
Un oggetto di tipo SavingsAccount è un caso speciale di oggetti di tipo BankAccount
Questa proprietà si riflette in una proprietà sintattica
una variabile oggetto del tipo di una superclasse può riferirsi ad un oggetto di una classe derivata
non c’è nessuna “conversione” effettiva, cioè non vengono modificati i dati
SavingsAccount collegeFund = new SavingsAccount(10); //tutto ok ...BankAccount anAccount = collegeFund; // questo è lecito!!
Object anObject = collegeFund; // anche questo!!
Conversione fra riferimenti
Le tre variabili, di tipi diversi, puntano ora allo stesso oggetto
SavingsAccount collegeFund = new SavingsAccount(10); //tutto ok ...BankAccount anAccount = collegeFund; // questo è lecito!!
Object anObject = collegeFund; // anche questo!!
51
Conversione fra riferimenti
Tramite la variabile anAccount si può usare l’oggetto come se fosse di tipo BankAccount,
Però non si può accedere alle proprietà specifiche di SavingsAccount
SavingsAccount collegeFund = new SavingsAccount(10);
BankAccount anAccount = collegeFund;
anAccount.deposit(10000); // questo è lecito perché
// deposit è un metodo di BankAccount
anAccount.addInterest(); // questo NON è lecito perché
//addInterest NON è metodo di BankAccount
cannot resolve symbol
symbol : method addInterest() location: class BankAccount anAccount.addInterest();
^ 1 error
public class BankAccount { ...
public void transferTo(BankAccount other, double amount) { withdraw(amount); // ovvero, this.withdraw(...)
other.deposit(amount);
} ...
}
Conversione tra riferimenti
Aggiungiamo il metodo transferTo a BankAccount
Per quanto detto finora, il parametro other può anche riferirsi ad un oggetto di tipo SavingsAccount
BankAccount acct1 = new BankAccount(500);
BankAccount acct2 = new BankAccount();
acct1.transferTo(acct2, 200);
System.out.println(acct1.getBalance());
System.out.println(acct2.getBalance()); 300 200
BankAccount other = new BankAccount(1000);
SavingsAccount sAcct = new SavingsAccount(10);
BankAccount bAcct = sAcct;
53
Conversione fra riferimenti
La conversione tra riferimento a sottoclasse e riferimento a superclasse può avvenire anche implicitamente (come tra int e double)
Il compilatore sa che il metodo transferTo richiede un riferimento di tipo BankAccount, quindi
controlla che sAcct sia un riferimento di tipo BankAccount o di una sua sottoclasse
effettua la conversione automaticamente
BankAccount other = new BankAccount(1000);
SavingsAccount sAcct = new SavingsAccount(10);
other.transferTo(sAcct, 500);
Conversione fra riferimenti
Vediamo la conversione inversa di quella vista finora
Ovvero la conversione di un riferimento a superclasse in un riferimento a sottoclasse
Questa non può avvenire automaticamente
Ma ha senso cercare di effettuarla?
In generale non ha senso, perché in generale un oggetto
BankAccount bAcct = new BankAccount(1000);
SavingsAccount sAcct = bAcct;
incompatible types found : BankAccount required: SavingsAccount sAcct = bAcct;
^ 1 error
55
Conversione fra riferimenti
Tale conversione ha senso soltanto se, per le specifiche dell’algoritmo, siamo sicuri che il
riferimento a superclasse punta in realtà ad un oggetto della sottoclasse
Richiede un cast esplicito
Richiede attenzione, perché se ci siamo sbagliati verrà lanciata un’eccezione
SavingsAccount sAcct = new SavingsAccount(1000);
BankAccount bAcct = sAcct;
...SavingsAccount sAcct2 = (SavingsAccount) bAcct;
// se in fase di esecuzione bAcct non punta // effettivamente ad un oggetto SavingsAccount // l’interprete lancia ClassCastException
Polimorfismo ed ereditarietà
57
Polimorfismo
Sappiamo che un oggetto di una sottoclasse può essere usato come se fosse un oggetto della superclasse
Ma quale metodo deposit viene invocato, quello di BankAccount o quello ridefinito in SavingsAccount?
acct è una variabile dichiarata di tipo BankAccount
Ma contiene un riferimento ad un oggetto che, in realtà, è di tipo SavingsAccount!
Questa informazione è disponibile solo in esecuzione (all'interprete Java), non in compilazione
secondo la semantica di Java, viene invocato il metodo deposit di SavingsAccount
BankAccount acct = new SavingsAccount(10);
acct.deposit(500);
acct.withdraw(200);
Polimorfismo
In Java il tipo di una variabile non determina in modo completo il tipo dell’oggetto a cui essa si riferisce
Questa semantica si chiama polimorfismo (“molte
forme”) ed è caratteristica dei linguaggi ObjectOriented
La stessa operazione (ad es. deposit) può essere svolta in modi diversi, a seconda dell'oggetto a cui ci si riferisce
L’invocazione di un metodo è sempre determinata dal tipo dell’oggetto effettivamente usato come parametro implicito, e NON dal tipo della variabile oggetto
La variabile oggetto ci dice cosa si può fare con quell'oggetto (cioè quali metodi si possono utilizzare)
L'oggetto ci dice come farlo
59
Polimorfismo
Abbiamo già visto il polimorfismo a proposito dei metodi sovraccarichi
l’invocazione del metodo println si traduce in una
invocazione di un metodo scelto fra alcuni metodi con firme diverse
il compilatore decide quale metodo invocare
In questo caso la situazione è molto diversa, perché la decisione non può essere presa dal compilatore, ma deve essere presa dall’ambiente runtime
(l’interprete)
Si parla di selezione posticipata (late binding)
Mentre nel caso di metodi sovraccarichi si parla di selezione anticipata (early binding)
Controllo del tipo e instanceof
Se si vuole controllare il tipo dell'oggetto a cui una variabile oggetto si riferisce, si può usare un nuovo operatore: instanceof
È un operatore relazionale (restituisce valori booleani)
Restituisce true se la variabile oggetto (primo argormento sta puntando ad un oggetto del tipo specificato dal secondo argomento
Ad esempio
SavingsAccount collegeFund = new SavingsAccount(10);
BankAccount anAccount = collegeFund;
if (anAccount instanceof SavingsAccount)
SavingsAccount a = (SavingsAccount) anAccount;
Restituisce true
61
Controllo del tipo e instanceof
Sintassi:
Restituisce
true se varOggetto contiene un riferimento ad un oggetto della classe NomeClasse (o una sua sottoclasse)
false altrimenti
In caso di valore restituito true un eventuale cast di
varOggetto ad una variabile di tipo NomeClasse NON lancia l’eccezione ClassCastException
Nota: il risultato non dipende dal tipo di varOggetto, ma dal tipo dell’oggetto a cui essa si riferisce al momento
dell’esecuzione
varOggetto instanceof NomeClasse
Esercizio: polimorfismo
63
La classe AccountTester (vers. 1)
public class AccountTester1
{ public static void main(String[] args)
{ SavingsAccount momsSavings = new SavingsAccount(0.5);
CheckingAccount harrysChecking = new CheckingAccount(100);
// metodi polimorfici di BankAccount e sue sottoclassi momsSavings.deposit(10000);
momsSavings.transfer(2000, harrysChecking);
harrysChecking.withdraw(1500);
harrysChecking.withdraw(80);
momsSavings.transfer(1000, harrysChecking);
harrysChecking.withdraw(400);
// simulazione della fine del mese
momsSavings.addInterest(); //metodo solo di SavingsAccount harrysChecking.deductFees();//metodo solo di CheckingAccount System.out.println("Mom’s savings balance = $"
+ momsSavings.getBalance());
System.out.println("Harry’s checking balance = $"
+ harrysChecking.getBalance());
}}
La classe AccountTester (vers. 2)
public class AccountTester2
{ public static void main(String[] args)
{ BankAccount momsSavings = new SavingsAccount(0.5);
BankAccount harrysChecking = new CheckingAccount(100);
// metodi polimorfici di BankAccount e sue sottoclassi
momsSavings.deposit(10000);//vengono invocati i metodi delle momsSavings.transfer(2000, harrysChecking); //sottoclassi harrysChecking.withdraw(1500); //anche se i riferimenti sono harrysChecking.withdraw(80); //di tipo BankAccount
momsSavings.transfer(1000, harrysChecking);
harrysChecking.withdraw(400);
// simulazione della fine del mese
((SavingsAccount)momsSavings).addInterest(); //e` necessario ((CheckingAccount)harrysChecking).deductFees();//fare i cast System.out.println("Mom’s savings balance = $"
+ momsSavings.getBalance());
System.out.println("Harry’s checking balance = $"
+ harrysChecking.getBalance());
}
65
Riflessione:
Object Oriented Programming Obiettivi e principi di design
OOP obiettivi
Robustezza
Vogliamo scrivere programmi capaci di gestire situazioni inaspettate (ad es. input inattesi)
Requisito particolarmente importante in applicazioni “life
critical”
Adattabilità
Vogliamo scrivere programmi capaci di evolvere (ad es.
girare su diverse architetture o avere nuove funzionalità)
Un concetto correlato è quello di portabilità
Riusabilità
Vogliamo che il nostro codice sia utilizzabile come componente di diversi sistemi in varie applicazioni
Deve essere chiaro cosa il nostro codice fa, e cosa non fa
67
OOP principi di design
Astrazione
“Distillare” i concetti che meglio rappresentano un oggetto o un sistema
Information hiding
Nascondere l’informazione ad utenti esterni, lasciando vedere solo l’interfaccia
In questo modo parti di un programma possono cambiare senza effetti sulle altre
Modularità
Organizzare un sistema software in componenti funzionali separate
Tutti questi principi di design
Sono supportati in un linguaggio di programmazione OO
Aiutano ad ottenere robustezza, adattabilità, riusabilità
OOP Paradigmi
Abbiamo esaminato i tre principali paradigmi di
programmazione che caratterizzano un linguaggio OO
Classi, oggetti, incapsulamento
• L’informazione viene nascosta dentro scatole nere (le classi), e l’accesso ad essa è controllato
Ereditarietà
• Una classe può essere estesa da sottoclassi, che ne ereditano le funzioni e le specializzano
Polimorfismo
• Il tipo di una variabile non determina completamente il tipo dell’oggetto a cui essa si riferisce
69
La superclasse universale Object
La classe Object
Sappiamo già che ogni classe di Java è (in maniera diretta o indiretta) sottoclasse di Object
Quindi ogni classe di Java eredita tutti i metodi della classe Object
Alcuni dei più utili metodi di Object sono i seguenti
Ma perché siano davvero utili, nella maggior parte dei casi bisogna sovrascriverli.
71
Sovrascrivere il metodo toString
Metodi “magici”
Il metodo println, è in grado di ricevere come parametro un oggetto di qualsiasi tipo
Ora siamo in grado di comprendere questa “magia”
qualsiasi riferimento può sempre essere convertito automaticamente in un riferimento di tipo Object
Ma un esemplare di String è anche di tipo Object…
come viene scelto, tra i metodi sovraccarichi, quello giusto quando si invoca println con una stringa?
Il compilatore cerca sempre di “fare il minor numero di conversioni”
viene usato, quindi, il metodo “più specifico”
public void println(Object obj) //invocato per oggetti generici { println(obj.toString()); }
public void println(String s) // invocato per le stringhe { ... }
73
Il metodo toString
Passando un oggetto qualsiasi a System.out.println si visualizza la sua descrizione testuale standard
println invoca toString dell’oggetto, e l’invocazione è possibile perché tutte le classi hanno il metodo
toString, eventualmente ereditato da Object
Il metodo toString di una classe viene invocato implicitamente anche per concatenare un oggetto con una stringa:
La seconda riga viene interpretata dal compilatore come se fosse stata scritta così:
BankAccount a = new BankAccount(1000);
System.out.println(a);
BankAccount acct = new BankAccount();
String s = "Conto " + acct;
String s = "Conto " + acct.toString();
Il metodo toString
Il metodo toString della classe Object ha la firma
L’invocazione di questo metodo per qualsiasi oggetto ne restituisce la descrizione testuale standard
il nome della classe seguito dal carattere @ e dallo hashcode dell’oggetto
• Che è un numero univocamente determinato dall’indirizzo in memoria dell’oggetto stesso
In generale la descrizione testuale standard non è particolarmente utile
È piu utile ottenere una stringa di testo contenente informazioni sullo stato dell’oggetto in esame
public String toString()
BankAccount a = new BankAccount(1000);
System.out.println(a); BankAccount@111f71
75
Sovrascrivere il metodo toString
Sovrascrivere il metodo toString nelle classi che si scrivono è considerato buono stile di programmazione
toString dovrebbe sempre produrre una stringa
contenente tutte le informazioni di stato dell’oggetto
Il valore di tutte le sue variabili di esemplare
Il valore di variabili statiche non costanti della classe
Questo stile di programmazione è molto utile per il debugging ed è usato nella libreria standard
Esempio: descrizione testuale di BankAccount
Bisogna sovrascrivere toString in BankAccount
System.out.println(anAccount);
BankAccount[balance=1500]
Esempio: toString in BankAccount
toString crea una stringa contenente il nome della classe seguito dai valori dei campi di esemplare racchiusi fra parentesi quadre.
public class BankAccount { ...
public String toString()
{ return "BankAccount[balance=" + balance + "]";
}}
77
Materiale di complemento (capitolo 10)
Metodo toString: lo stile adottato
nella libreria standard
79
Esempio: toString in BankAccount
Stile adottato nella libreria standard:
toString crea una stringa contenente il nome della classe seguito dai valori dei campi di esemplare racchiusi fra parentesi quadre.
Problema: devo sovrascrivere toString anche per le sottoclassi, per vedere stampato il nome di classe corretto
public class BankAccount { ...
public String toString()
{ return “BankAccount[balance=” + balance + “]”;
}}
Esempio: toString in BankAccount
È possibile evitare di scrivere esplicitamente il nome della classe all’interno del metodo
Si può usare il metodo getClass, che restituisce un oggetto di tipo classe
E poi invocare il metodo getName sull’oggetto di tipo classe, per ottenere il nome della classe
toString visualizza il nome corretto della classe anche quando viene invocato su un oggetto di una sottoclasse
public String toString()
{ return getClass().getName() +
“[balance=” + balance + “]”;
}
81
Esempio: toString in SavingsAccount
Ovviamente, se si vogliono visualizzare i valori di nuovi campi di esemplare di una sottoclasse bisogna sovrascrivere toString
Bisogna invocare super.toString per ottenere i valori dei campi della superclasse
public class SavingsAccount extends BankAccount { ...
public String toString()
{ return super.toString() +
“[interestRate=” + interestRate + “]”;
} ...
}
Controllo di accesso
83
Controllo di accesso
Java fornisce quattro livelli per il controllo di accesso a metodi, campi, e classi
public
private
protected
package
Finora noi abbiamo sempre usato solo i primi due
Ora siamo in grado di comprendere anche gli altri due
In particolare lo specificatore di accesso protected
Accesso package
Un membro di classe (o una classe) per il quale non viene specificato un identificatore di accesso ha
un’impostazione di accesso predefinita
Accesso di pacchetto
Tutti i metodi delle classi contenute nello stesso pacchetto vi hanno accesso
Può essere una buona impostazione per le classi
Non lo è per le variabili, perché si viola l’incapsulamento dell’informazione
85
Accesso package
Errore comune: dimenticare lo specificatore di accesso per una variabile di esemplare
Questo è un rischio per la sicurezza
Un altro programmatore può realizzare una nuova classe nello stesso pacchetto e ottenere l’accesso al campo di esemplare
È meglio usare (quasi) sempre campi di esemplare private
public class Window extends Container { String warningString;
...
}
Accesso protected
Abbiamo visto che una sottoclasse non può accedere a campi private ereditati dalla propria superclasse
Deve usare i metodi (public) appropriati della superclasse
Esempio: il metodo addInterest di SavingsAccount
public class SavingsAccount extends BankAccount { ...
public void addInterest()
{ deposit(getBalance() * interestRate / 100);
} ...
}
87
Accesso protected
Il progettista della superclasse può rendere accessibile in modo protected un campo di esemplare
Tutte le sottoclassi vi hanno accesso
Attenzione: è una violazione dell’incapsulamento
Il progettista della superclasse non ha alcun controllo sui progettisti di eventuali sottoclassi
Diventa difficile modificare la superclasse: non si
possono modificare variabili protected perché qualcuno potrebbe aver scritto una sottoclasse che ne fa uso
Anche i metodi possono essere definiti protected
possono essere invocati solo nella classe in cui sono definiti e nelle classi da essa derivate
public class BankAccount { ...
protected double balance;
}
Il modificatore di accesso
final
89
Classi e metodi final
Abbiamo già visto che la parola chiave final può essere usata nella definizione di una variabile per impedire successive assegnazioni di valori
Anche un metodo di una classe può essere dichiarato come final
Un metodo di una classe dichiarato final non può essere sovrascritto dalle sottoclassi
Anche una intera classe può essere dichiarata come final
Una classe dichiarata final non può essere estesa
Classi e metodi final
Esempio: la classe String è una classe immutabile
Gli oggetti di tipo String non possono essere modificati da nessuno dei loro metodi
La classe String nella libreria standard è dichiarata in questo modo:
Nessuno può creare sottoclassi di String
Quindi siamo sicuri che riferimenti ad oggetti di tipo String si possono sempre copiare senza rischi di cambiamenti
public final class String { ...
}
91
Classi e metodi final
In alcune circostanze può essere utile dichiarare un metodo come final
Esempio: un metodo per verificare la correttezza di una chiave
In questo modo nessuno può sovrascriverlo con un metodo che restituisca sempre true
public class SecureAccount extends BankAccount { ...
public final boolean checkPassword(String password) { ...
}}
È tutto chiaro? …
1. Quale è una comune motivazione che porta a definire campi di esemplare con accesso di pacchetto?
2. Se una classe dotata di costruttore pubblico ha accesso di pacchetto, chi ne può costruire
esemplari?
93
Interfacce (capitolo 9)
e l’interfaccia Comparable
(sezione 13.8)
95
Realizzare una proprietà astratta
Torniamo al problema enunciato qualche giorno fa:
ordinare oggetti generici
Affinché i suoi esemplari possano essere ordinati, una classe deve definire un metodo adatto a confrontare esemplari della classe, con lo stesso comportamento di compareTo
Gli oggetti della classe diventano confrontabili
gli algoritmi di ordinamento/ricerca non hanno bisogno di conoscere alcun particolare del criterio di confronto tra gli oggetti
basta che gli oggetti siano tra loro confrontabili
Scrivere una interfaccia
Sintassi:
La definizione di una interfaccia in Java serve per definire un comportamento astratto
Una interfaccia deve poi essere realizzata (implementata) da una classe che dichiari di avere tale comportamento
Definire un’interfaccia è simile a definire una classe
si usa la parola chiave interface al posto di class
Un’interfaccia può essere vista come una classe ridotta
non può avere costruttori
non può avere variabili di esemplare
contiene le firme di uno o più metodi non statici (definiti implicitamente public), ma non può definirne il codice
public interface NomeInterfaccia { firme di metodi
}
97
Implementare una interfaccia
Esempio:
L'interfaccia Comparable
Se una classe intende realizzare concretamente un comportamento astratto definito in un’interfaccia, deve
Dichiarare di implementare l’interfaccia
Definire i metodi dell’interfaccia, con la stessa firma
public class BankAccount implements Comparable { ...
public int compareTo(Object other)
{ //other è un Object perché questa è la firma in //Comparable. Però facciamo un cast a BankAccount...
BankAccount acct = (BankAccount) other;
if (balance < acct.balance) return -1;
if (balance > acct.balance) return 1;
return 0;
} ...
}
public interface Comparable
{ int compareTo(Object other);
}
Conversione di tipo tra
classi e interfacce
99
Usare una interfaccia
In generale, valgono le regole di conversione viste nell'ambito dell'ereditarietà
Una interfaccia ha lo stesso ruolo di una superclasse
Una classe che implementa un'interfaccia ha lo stesso ruolo di una sottoclasse che estende la superclasse
È lecito definire una variabile il cui tipo è un'interfaccia
Ma non è possibile costruire oggetti da un’interfaccia
Le interfacce non hanno campi di esemplare nè costruttori
Allora a che serve avere una variabile oggetto il cui tipo è quello di una interfaccia?
new Comparable(); // ERRORE IN COMPILAZIONE Comparable c; // corretto
Conversione da classe ad interfaccia
Una variabile oggetto di tipo Comparable può puntare ad un oggetto di una classe che realizza Comparable
Usando la variabile c non sappiamo esattamente quale è il tipo dell'oggetto a cui essa si riferisce
Non possiamo utilizzare tutti i metodi di BankAccount
Però siamo sicuri che quell'oggetto ha un metodo compareTo
Comparable c = new BankAccount(10); //corretto
double saldo = c.getBalance(); //ERRORE in compilazione
Comparable d = new BankAccount(20);
101
Conversione da interfaccia a classe
A volte può essere necessario convertire un riferimento il cui tipo è un interfaccia in un riferimento il cui tipo è una classe che implementa l'interfaccia
Se siamo certi che c punta ad un oggetto di tipo
BankAccount possiamo creare una nuova variabile acct di tipo BankAccount e fare un cast di c su acct
E se ci sbagliamo? ClassCastException in esecuzione
Comparable c = new BankAccount(10);
double saldo = c.getBalance(); //ERRORE! Come faccio?
Comparable c = new BankAccount(10);
BankAccount acct = (BankAccount) c;
double saldo = anAccount.getBalance(); //corretto
Polimorfismo e interfacce
103
Polimorfismo e interfacce
Possiamo invocare il metodo compareTo perché x è di tipo Comparable
Ma quale metodo compareTo?
• Quello scritto in BankAccount o quello di String?
Il compilatore non ha questa informazione
Il tipo di oggetto a cui x si riferisce dipende dal flusso di esecuzione
In fase di esecuzione la JVM determina il tipo di oggetto a cui x si riferisce e applica il metodo corrispondente
Comparable x;
...if (...) x = new BankAccount(1000);
else x = new String(“”);
...if (x.compareTo(y) > 0) ...
Polimorfismo e interfacce
Il tipo di una variabile non determina in modo completo il tipo dell’oggetto a cui essa si riferisce
Come per l'ereditarietà, questa semantica si chiama polimorfismo
La stessa operazione (compareTo) può essere svolta in modi diversi, a seconda dell'oggetto a cui ci si riferisce
L’invocazione di un metodo è sempre determinata dal tipo dell’oggetto effettivamente usato come parametro implicito, e NON dal tipo della variabile oggetto
La variabile oggetto (interfaccia) ci dice cosa si può fare con quell'oggetto (cioè quali metodi si possono utilizzare)
L'oggetto (appartenente ad una classe) ci dice come farlo
105
Polimorfismo e interfacce
Se la condizione dell'if è vera, x contiene un
riferimento ad un oggetto che, in quel momento dell'esecuzione è di tipo BankAccount!
E l’interprete Java lo sa (il compilatore no)
secondo la semantica di Java, viene invocato il metodo compareTo scritto in BankAccount
Comparable x;
...if (...) x = new BankAccount(1000);
else x = new String(“”);
...if (x.compareTo(y) > 0) ...
Ordinamento di oggetti e
l'interfaccia Comparable
107
L’interfaccia Comparable
L’interfaccia Comparable è definita nel pacchetto java.lang, per cui non deve essere importata né deve essere definita
la classe String, ad esempio, implementa Comparable
Come può Comparable risolvere il nostro problema?
Ovvero definire un metodo di ordinamento valido per tutte le classi?
Basta definire un metodo per ordinare un array di riferimenti ad oggetti che realizzano Comparable, indipendentemente dal tipo
public interface Comparable
{ int compareTo(Object other);}
Ordinamento di oggetti
Tutti i metodi di ordinamento e ricerca che abbiamo visto per array di numeri interi possono essere
riscritti per array di oggetti Comparable
Basta usare le seguenti “traduzioni”
if (a < b) if (a > b) if (a == b) if (a != b)
if (a.compareTo(b) < 0) if (a.compareTo(b) > 0) if (a.compareTo(b) == 0) if (a.compareTo(b) != 0)
109
SelectionSort per oggetti Comparable
public class ArrayAlgs { ...
public static void selectionSort(Comparable[] v, int vSize) { for (int i = 0; i < vSize - 1; i++)
{ int minPos = findMinPos(v, i, vSize-1);
if (minPos != i) swap(v, minPos, i); }
} private static void swap(Comparable[] v, int i, int j) { Comparable temp = v[i];
v[i] = v[j];
v[j] = temp;
} private static int findMinPos(Comparable[] v, int from, int to) { int pos = from;
Comparable min = v[from];
for (int i = from + 1; i <= to; i++) if (v[i].compareTo(min) < 0) { pos = i;
min = v[i]; } return pos;
} ...
}
Ordinamento di oggetti
Definito un algoritmo per ordinare un array di
riferimenti Comparable, se vogliamo ordinare un array di oggetti BankAccount basta fare
Dato che BankAccount realizza Comparable, l’array di riferimenti viene convertito
automaticamente
BankAccount[] v = new BankAccount[10];
...// creazione dei singoli elementi dell’array // ed eventuali modifiche allo stato
// degli oggetti dell’array ...ArrayAlgs.selectionSort(v);
111
Scrivere metodi compareTo
Il metodo compareTo
Il metodo compareTo deve definire una relazione di ordine totale, ovvero deve avere queste proprietà
Antisimmetrica: a.compareTo(b) ≤ 0 b.compareTo(a) ≥ 0
Riflessiva: a.compareTo(a) = 0
Transitiva: a.compareTo(b) ≤ 0 e b.compareTo(c) ≤ 0
• Implica a.compareTo(c)<=0
a.compareTo(b) può restituire qualsiasi valore negativo per segnalare che a precede b
if (a.compareTo(b) == -1) //errore logico!
if (a.compareTo(b) < 0) // giusto!
113
I metodi compareTo e equals
Una classe che implementa Comparable ha i metodi
compareTo (per implementare Comparable)
equals (ereditato da Object)
Il metodo compareTo definito in una classe dovrebbe sempre essere consistente con il metodo equals:
(e1.compareTo((Object)e2) == 0)
• dovrebbe sempre avere lo stesso valore booleano di
e1.equals((Object)e2) per ogni e1 e e2 della classe
Il metodo equals in Object è:
Quindi una classe che implementa Comparable dovrebbe sempre sovrascrivere equals…
Non sempre noi lo faremo
public boolean equals(Object obj) { return (this == obj); }
Comparable e Java >= 5.0
In seguito all’introduzione in Java 5.0 dei tipi generici (che non vediamo), l’utilizzo di Comparable induce il compilatore all’emissione di strane (ma innocue)
“segnalazioni”, che possono essere ignorate
Compilando con l’opzione nowarn si elimina l’emissione delle segnalazioni
Note: MyClass.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
javac –nowarn MyClass.java