La classe è un costrutto che permette di raggruppare
in un pacchetto indivisibile un gruppo di variabili
ed un insieme di metodi che hanno accesso esclusivo
a tali variabili:
public
class nome {
private tipo attributo1;
private tipo attributo2;
public tipo metodo1() {
// corpo del metodo
}
public tipo metodo2(tipo parametro1 , tipo
parametro2) {
// corpo del metodo
}
}
Gli
attributi sono variabili accessibili da qualunque metodo
interno alla classe, ma inaccessibili all'esterno. I
metodi sono delle procedure che possono operare sia
sui dati passati come para-metri, sia sugli attributi
della classe.
NOTA:
la clausola di esclusività di accesso agli attributi
può essere indebolita grazie al modificatore
pu-blic, che concede il permesso di modifica diretta
degli attributi anche a procedure dichiarate al di fuori
della classe di appartenenza. Questa possibilità,
che verrà descritta per completezza nel prossimo
capitolo, viene fortemente sconsigliata, dal momento
che comporta la violazione del più importante
criterio del design Object Oriented: l'Incapsulamento.
Convenzioni di Naming
Esistono delle convenzioni sulle modalità di
attribuzione di nomi a variabili, metodi ed attribu-ti.
Tali convenzioni sono talmente radicate nelle consuetudini
d'uso del linguaggio Java da as-sumere quasi il valore
di leggi:
- Tutti
nomi di classi devono iniziare con una lettera maiuscola
- Metodi,
variabili ed attributi iniziano con una lettera minuscola
- I
nomi composti vengono dichiarati secondo la convenzione
"Camel Case": le parole vengono riportate
in minuscolo, una di seguito all'alta senza caratteri
di separazione, utilizzando un carattere maiuscolo
come lettera iniziale di ogni parola
Queste
semplici regole, universalmente utilizzate nel mondo
Java favoriscono una certa uni-formità al codice
scritto a più mani, e garantiscono una ottima
leggibilità. Ecco di seguito tre identificatori
che seguono le convenzioni di naming appena elencate:
NomeClasse
nomeMetodo() nomeVariabile
Incapsulamento
L'incapsulamento è il principio fondante del
design Object Oriented: esso prevede che il con-tenuto
informativo di una classe rimanga nascosto all'utente,
in modo tale che i metodi risulti-no essere l'unica
via per interagire con gli oggetti corrispondenti. Questo
approccio presenta almeno due grandissimi vantaggi:
anzitutto esso permette al programmatore di disciplinare
l'accesso agli attributi di una classe, in modo da impedire
che ne venga fatto un uso sbagliato; in secondo luogo,
l'incapsulamento permette a chi utilizza una classe
di concentrarsi esclusi-vamente sull'interfaccia di
programmazione, tralasciando ogni aspetto legato all'implemen-tazione.
Quasi
tutti gli oggetti del mondo reale presentano questa
stessa proprietà: quando componiamo un numero
di telefono su un apparecchio diamo il via ad una complessa
operazione, i cui detta-gli ci sono completamente nascosti.
L'unica cosa a cui siamo interessati è che tale
operazione, comunque venga svolta, ci permette di entrare
in contatto con un altro utente.
Costruttore
Il costruttore è un particolare metodo che ha
lo stesso nome della classe e che è privo di
valore di ritorno. Il costruttore permette di inizializzare
i principali attributi di un oggetto in fase di creazione
ricorrendo ad una sola operazione:
public
class MyClass {
private Int att1;
public MyClass(int a) {
att1 = a;
}
public
int getAtt1() {
return att1;
}
}
Il
costruttore deve essere invocato in fase di creazione
attraverso l'operatore new, fornendo tut-ti i parametri
richiesti:
MyClass
c = new MyClass(10);
Come
si può notare nell'esempio, il costruttore fornisce
l'unica via per impostare il valore di attributi privi
di metodo setter, attributi che per questa ragione si
definiscono "immutabili".
Ovviamente non esiste un modo per chiamare il costruttore
di una classe in un momento diver-so dalla sua creazione.
Il costruttore è una componente indispensabile
della classe: se il pro-grammatore non ne definisce
uno esplicitamente, il compilatore aggiunge automaticamente
il costruttore privo di parametri, detto costruttore
di default.
Finalizzatori e Garbage Collection
Ogni oggetto occupa fisicamente una determinata porzione
della memoria nel calcolatore. Dal momento che la memoria
è una risorsa finita, è importante capire
dove fanno a finire gli oggetti dopo aver svolto il
compito per il quale erano stati creati. La gestione
della memoria è uno dei punti di forza di Java:
la pulizia della memoria dagli oggetti non più
utilizzati viene svolto au-tomaticamente durante l'esecuzione
da un apposito strumento detto Garbage Collector (racco-glitore
di rifiuti). Non appena la memoria disponibile scende
al di sotto di una certa soglia, il Garbage Collector
si attiva automaticamente, va in cerca di tutti gli
oggetti non più referenziati e li distrugge,
rilasciando la memoria che essi occupavano.
Figura 1 - Rappresentazione della memoria prima
dell'intervento del Garbage
Figura 2 - Rappresentazione della memoria dopo l'intervento
del Garbage Collector
Prima
di procedere alla distruzione, il Garbage Collector
invoca il metodo finalize() sull'oggetto da rimuovere.
Il programmatore può dichiarare tale metodo nelle
proprie classi, ed inserirvi delle istruzioni da eseguire
subito prima della distruzione dell'oggetto:
public
void finalize() {
att1 = null;
att2 = null;
file.close();
}
I
finalizzatori sono utili in quei contesti in cui la
distruzione di una classe comporta operazioni che non
vengono compiute automaticamente dal garbage collector,
come la chiusura di files o più in generale il
rilascio di risorse di sistema. In tutti gli altri contesti,
esso risulta praticamente inutile, e pertanto si sconsiglia
di dichiararlo nelle proprie classi.
Classe, Istanza e Reference
Per raggiungere il pieno controllo sul linguaggio Java,
è necessario capire la differenza concet-tuale
tra Classe, Istanza e Reference.
La
classe è la matrice sulla quale vengono prodotti
gli oggetti. Essa è come uno stampino, che permette
di plasmare un materiale informe per produrre una molteplicità
di oggetti simili tra loro. Per distinguere la matrice
dal prodotto, il lessico della programmazione Object
Oriented ricorre a due termini: Classe ed Istanza.
La
classe, corrispondente al codice sorgente scritto dal
programmatore, è presente in singola copia nella
memoria del computer. Ogni volta che si ricorre all'operatore
new, viene creata una nuova istanza della classe, ossia
un oggetto di memoria conforme alle specifiche della
classe stessa.
Figura 3: La Classe è come uno stampo
capace di generare un'infinità di
oggetti simili tra di loro ma dotati di nel contempo
di attributi univoci
La
variabile a cui viene associato l'oggetto è d'altra
parte soltanto un reference: essa è simile ad
un telecomando con il quale è possibile inviare
delle direttive all'oggetto vero e proprio. Ogni volta
che si invoca un metodo su una variabile, la variabile
in sé non subisce nessun cam-biamento: la chiamata
di metodo viene inoltrata all'oggetto vero e proprio,
che reagisce all'evento nelle modalità previste
dal codice presente nella classe. Questa architettura
permette di avere più variabili che puntano allo
stesso oggetto: l'oggetto è unico, indipendentemente
dal numero di reference usati per inoltrare le chiamate
a metodo.
Il
reference può anche non puntare alcun oggetto;
in questo caso, esso assume il valore speciale null.
Al momento della dichiarazione, se non viene specificato
diversamente, ogni reference ha valore null.
Figura 4 - E' possibile avere più reference
che puntano ad un unico oggetto, o un reference che
non punta a niente (null)
Ereditarietà
L'ereditarietà è un'altra caratteristica
fondamentale dei linguaggi OO. Grazie all'ereditarietà
possiamo definire una classe come figlia (o sottoclasse)
di una classe già esistente, in modo da estenderne
il comportamento. La classe figlia (o sottoclasse) "eredita"
tutti i metodi e gli attri-buti della superclasse, e
in tal modo ne acquisisce il comportamento.
Si
provi ad immaginare una classe Bicicletta, descritta
di seguito in pseudo codice java:
public
class Bicicletta {
public Color colore;
public Color getColore() {
return colore;
}
public void muoviti() {
sollevaPiediDaTerra();
spingiPedaleDestro();
spingiPedaleSinistro();
...
}
public void frena() {
premiGanasceSuRuota();
}
public void curvaDestra() {
giraManubrioVersoDestra();
}
public void curvaSinistra() {
giraManubrioVersoSinistra();
}
}
Essa
è caratterizzata dall'attributo Colore e dai
metodi muoviti(), frena(), curvaDestra() e cur-vaSinistra().
Un motorino è un mezzo simile ad una bici, ma
dotato di alcuni attributi in più, come la cilindrata
e il numero di targa, e di alcuni metodi non presenti
nella bicicletta, tipo ac-cendiMotore():
public
class Motorino extends Bicicletta {
private
String targa;
private int cilindrata;
public
int getCilindrata() {
return
cilindrata;
}
public String getTarga() {
return targa;
}
public void accendiMotore() {
inserisciMiscelaNelCilindro();
accendiCandela();
}
}
La
classe Motorino viene definita, grazie alla direttiva
extends, come sottoclasse di Bicicletta. La conseguenza
di questa relazione di parentela è che la classe
Motorino eredita, in modo del tutto automatico, tutti
gli attributi e i metodi di Bicicletta, senza la necessità
di riscriverli.
L'ereditarietà
permette di creare delle gerarchie di classi di profondità
arbitraria, simili ad albe-ri genealogici, in cui il
comportamento della classe in cima all'albero viene
gradualmente spe-cializzato dalle sottoclassi. Ogni
classe può discendere da un'unica superclasse,
mentre non c'è limite al numero di sottoclassi
o alla profondità della derivazione.
Figura 10.5 - Un esempio di gerarchia di classi
Dal
codice di una Classe è possibile accedere a metodi
ed attributi pubblici di qualunque su-perclasse, ma
non a quelli privati. Il modificatore protected, che
verrà studiato meglio in segui-to, permette di
definire metodi e attributi accessibili solo dalle sottoclassi.
In
Java ogni classe in cui non sia definito esplicitamente
un padre, viene automaticamente con-siderato sottoclasse
di Object, che pertanto il capostipite di tutte le classi
Java.
Overloading
All'interno di una classe è possibile definire
più volte un metodo, in modo da adeguarlo a con-testi
di utilizzo differenti. Due metodi con lo stesso nome
possono coesistere in una classe a due condizioni:
- Devono
avere lo stesso tipo di ritorno
- Devono
presentare delle differenze nel numero e nel tipo
dei parametri
All'interno
di un metodo è sempre possibile invocare un metodo
omonimo: spesso infatti una famiglia di metodi con lo
stesso nome si limita a fornire differenti vie di accesso
programmati-che ad una unica logica di base. Vediamo
ad esempio una ipotetica classe Quadrilatero, dotata
di un attributo dimensione e di tre metodi setter che
permettano di impostare l'attributo speci-ficando un
apposito oggetto Dimension, una coppia di interi o nessun
parametro per impostare valori di default:
Public
class Quadrilatero {
private Dimension dimensione;
// metodo di base
public void setSize(Dimension d) {
dimensione = d;
}
// accesso con interi
public void setSize(int width,int height)
{
SetSize(new Dimension(width,height));
}
// impostazione di default
public void setSize() {
setSize(new Dimension(100,100));
}
}
In
questo esempio si può notare che solamente il
primo dei tre metodi setSize effettua operi la modifica
diretta dell'attributo dimensione; le altre due versioni
del metodo si limitano a rifor-mulare la chiamata in
modo da renderla adatta al metodo di base.
E'
possibile effettuare anche l'overloading dei costruttori:
public
class MyClass {
private int att1;
private int att2;
public MyClass() {
att1 = 0;
att2 = 0;
}
public MyClass(int a1 , int a2) {
att1 = a1;
att2 = a2;
}
}
Per
raggiungere il pieno controllo in scenari che prevedano
l'overloading di metodi e costrutto-ri, è necessario
comprendere l'uso degli identificatori this e super,
che verranno illustrati nei prossimi paragrafi.
Overridding
Attraverso l'ereditarietà è possibile
estendere il comportamento di una classe sia aggiungendo
nuovi metodi, sia ridefinendo metodi già dichiarati
nella superclasse. Quest'ultima possibilità,
che prende il nome di Overridding, ed è un'ulteriore
proprietà dei linguaggi ad oggetti. Per met-tere
in atto l'Overridding, è sufficiente dichiarare
un metodo di cui esiste già un'implementazione
in una superclasse: la nuova implementazione prenderà
automaticamente la precedente, sovrascrivendone il comportamento.
Nell'esempio del motorino, è naturale pen-sare
alla necessità di fornire una nuova implementazione
del metodo muoviti(), in modo tale da adeguarlo allo
scenario di un mezzo motorizzato:
public
class Motorino extends Bicicletta {
private String targa;
private int cilindrata;
public int getCilindrata() {
return cilindrata;
}
public String getTarga() {
return targa;
}
public void accendiMotore() {
inserisciMiscelaNelCilindro();
accendiCandela();
}
public void muoviti() {
accendiMotore();
premiFrizione();
innestaMarcia(1);
rilasciaFrizione();
....
}
}
La
classe Motorino presente in quest'ultimo esempio presenta
gli stessi metodi ed attributi del-la classe Bicicletta,
i metodi definiti ex novo ed una versione nuova del
metodo muoviti(), già dichiarato nella superclasse.
this
L'identificatore this è un puntatore speciale
alla classe che costituisce l'attuale contesto di pro-grammazione.
Grazie a this è possibile accedere a qualsiasi
metodo o attributo della classe stessa attraverso un'espressione
del tipo:
this.methodo();
L'identificatore
this è indispensabile quando ci si trova a dover
distinguere tra un attributo ed una variabile con lo
stesso nome, come avviene spesso nei metodi setter e
nei costruttori:
public
setAtt1(int att1) {
// assegna il valore della variabile locale
att1
this.att1 = att1; all'attributo omonimo
}
L'identificatore
this può essere usato anche per richiamare un
costruttore; in questo caso la pa-rola this deve essere
seguita dai parametri richiesti dal costruttore in questione
racchiusi tra pa-rentesi, e deve per forza comparire
come prima istruzione di un altro costruttore:
public
class MyClass {
private int att1;
private int att2;
public MyClass() {
// chiama il secondo costruttore
con I parametri di default
this(0,0);
}
public MyClass(int a1 , int a2) {
att1 = a1;
att2 = a2;
}
}
L'identificatore
super
L'identificatore super ha un uso simile a this ma, a
differenza di quest'ultimo, invece di far riferimento
alla classe di lavoro fa riferimento alla superclasse.
Attraverso super è possibile invocare la versione
originale di un metodo sovrascritto, altrimenti inaccessibile:
//
chiamata ad un metodo della classe stessa
metodo();
// chiamata al metodo omonimo presente nella
super.metodo(); superclasse
In
molti casi, i metodi sovrascritti sono estensioni degli
equivalenti metodi della superclasse; in questi casi
si può utilizzare super in modo da richiamare
la versione precedente del metodo, e quindi aggiungere
di seguito le istruzioni nuove:
public
void metodo1() {
// chiamata a metodo1 della superclasse
super.metodo1();
// nuove istruzioni che estendono
// il comportamento del metodo
// omonimo della superclasse
}
Il
principale uso di super è nei costruttori, che
devono necessariamente estendere un costrutto-re della
superclasse. Se non viene specificato diversamente,
il costruttore di una classe viene considerato estensione
del costruttore di default della superclasse (quello
privo di parametri). Se si desidera un comportamento
differente, o se addirittura la superclasse non dispone
di un costruttore di default, occorre invocare in modo
esplicito il supercostruttore desiderato:
public
class ClasseFiglia extends ClassePadre {
private float p3;
public ClasseFiglia(int p1 , String p2 ,
float p3) {
super(p1,p2);
this.p3 = p3;
}
}
Anche
in questo caso, la chiamata al supercostruttore deve
precedere qualsiasi altra istruzione.
Binding dinamico
Ogni classe denota un tipo; tuttavia, se si considerano
le conseguenze dell'ereditarietà si scopre che
ogni classe ha come tipo sia il proprio, sia quello
di tutte le sue superclassi. Grazie a questa proprietà,
una classe può essere utilizzata in qualunque
contesto valido per una qualsiasi delle sue superclassi.
Ad esempio il metodo
public
void parcheggia(Bicicletta b) {
b.muovi();
b.giraSinistra();
...
b.frena();
}
può
lavorare sia su oggetti di tipo Bicicletta che su quelli
di tipo Motorino, una cosa abbastanza intutiva anche
nel mondo reale (un parcheggio per automobili può
andar bene anche per dei taxi, che alla fine sono pur
sempre automobili).
Per
questo stesso motivo è possibile formulare una
dichiarazione del tipo:
Bicicletta
b = new Motorino();
In
questo esempio viene creato un oggetto di tipo Motorino,
che tuttavia viene referenziato da una variabile di
tipo Bicicletta. Quale effetto produce la chiamata di
un metodo su cui sia stato applicato l'Overridding?
Verrà invocata la versione del padre o del figlio?
In
Java i metodi sono legati al tipo dell'istanza, non
a quello del reference: in altre parole, no-nostante
il reference sia di tipo Bicicletta, le chiamate avranno
sempre l'effetto di invocare il metodo valido per il
tipo dell'istanza in questione.
Il binding è l'associazione di un metodo alla
rispettiva classe: in Java il binding viene effettua-to
durante l'esecuzione, in modo tale da garantire che
su ogni oggetto venga invocato il metodo corrispondente
al tipo. Contrariamente a quanto avviene con linguaggi
tipo il C++, non esiste alcun modo per richiamare su
un oggetto un metodo appartenente alla superclasse se
questo è stato sovrascritto. Per questa ragione
la chiamata
b.muovi();
chiamerà
sull'oggetto referenziato dalla variabile b il metodo
muovi() di Motorino dal momen-to che l'oggetto in questione
è di tipo Motorino.
Figura 10.6 - Un reference permette di accedere
unicamente ai metodi
dell'oggetto denotati dal proprio tipo.
Upcastingn Downcasting ed Operatore instan-ceof
Come nei tipi primitivi, l'upcasting o promozione è
automatico: è sempre possibile referenziare un
oggetto con una variabile il cui tipo è quello
di una delle sue superclassi
Motorino
m = new Motorino();
Bicicletta b = m; // downcasting
Se
invece abbiamo un oggetto di un certo tipo referenziato
da una variabile di un supertipo e vogliamo passare
il reference ad un'altra variabile di un tipo più
specializzato, è necessario ri-correre all'operatore
di casting.
Bicicletta
b = new Motorino();
Motorino m = (Motorino)b;
Il
casting è sempre sconsigliato, dal momento che
viola il principio del polimorfismo. D'altra parte,
qualora fosse necessario dover operare un casting, è
possibile ricorrere all'operatore in-stanceof, che permette
di verificare il reale tipo di un oggetto:
If
(b instanceof Motorino)
m = (Motorino)b;
Equals e operatore ==
Come già visto nel caso delle stringhe, quando
si lavora su oggetti è necessario prestare una
grande attenzione a come si usa l'operatore di uguaglianza
==. L'operatore di uguaglianza permette di verificare
l'identità tra due references, ossia la circostanza
in cui essi facciano rife-rimento allo stesso oggetto
in memoria. Qualora si desideri testare la somiglianza
tra due og-getti, ossia la circostanza in cui due oggetti
distinti presentano lo stesso stato, è necessario
ri-correre al metodo equals(). Ad esempio in un caso
del tipo:
Integer
i1 = new Integer(12);
Integer i2 = new Integer(12);
Il
test i1 == i2 darà esito falso, in quanto le
due variabili fanno riferimento a due oggetti distin-ti;
al contrario l'espressione risulta essere vera, dal
momento che i due oggetti hanno lo stesso identico stato.
Figura 7 - Esempi di somiglianza e di identità
tra oggetti
Conclusioni
In questo articolo abbiamo studiato nei dettagli come
si dichiarano le classi, e abbiamo anche approfondito
alcuni dettagli importanti legati all'uso delle classi
nella programmazione. Il prossimo mese studieremo il
contesto statico, le classi astratte e i modificatori
di accesso.
|