MokaByte 74- Maggio 2003 
Corso di programmazione Java
Classi, Metodi e Attributi
di
Andrea Gini
Dopo aver introdotto in maniera informale l'uso e la dichiarazione di oggetti in Java, è giunto il momento di discutere in modo esteso e formale tutti gli aspetti della programmazione ad oggetti in java.

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.

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it