Linguaggio Java

 

per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto!


Corso sul linguaggio Java

di Alessandro Luparello

Per commentare, dare suggerimenti o consigli (molto apprezzati) e per segnalare eventuali errori o
disattenzioni (sempre possibili), puoi inviare una email all'autore (clicca sul nome sopra). Grazie per la collaborazione!

 


Quinta lezione
Iniziamo a lavorare con le classi ed i metodi
Lezione precedente | Pagina principale | Lezione successiva

cosa diremo

1. Il centro di tutto sta nelle classi: definizione!
2. Approfondiamo i mezzi per l'interazione tra gli oggetti: i metodi e la loro implementazione

nella prossima lezione: Sperimentiamo gli oggetti: introduzione ad Array e liste!


 

Classi: creazione ed utilizzo

Di creazione di classi abbiamo già parlato in lezioni precedenti, ma mai in modo completo e rigoroso (piccole introduzioni erano indispensabili per procedere con la spiegazione). Ora è giunto il momento di affrontare il problema.

Definizione di Classe :
Definire una classe è molto semplice, già nelle passate lezioni abbiamo fatto diversi esempi ed abbiamo accennato molte delle cose che qui riporteremo (per avere una visione complessiva, sintetica e completa del concetto e dell'applicabilità delle classi).
Allora, come si definisce una classe?
Semplicemente utilizzando la parola
class seguita dal nome che vogliamo dare alla classe stessa.
In genere (per default) le classi ereditano tutte dalla classe
Object, qualora si volesse indicare una precisa superclasse dalla quale ereditare, bisognerebbe (come detto, anche questo, in precedenti lezioni) aggiungere la parola extends seguita dal nome della superclasse.

  class Albero {
...
}

class Albero extends Vegetale {
...
}

E dentro?
Ovviamente, perchè la classe possa fare qualcosa ed integrarsi (eventualmente) in un programma più ampio, deve avere un contenuto. Qual'è il contenuto delle classi? Ormai dovreste sapere rispondere... Si tratta di variabili e metodi!

Variabili
Cominciamo con l'introdurre le variabili. Si è già diffusamente parlato delle variabili locali e delle variabili istanza e di classe, non staremo a ripetere tutto. Però qualcosa è bene sottolinearla, in modo da rimarcare alcuni concetti che, anche se semplici, risultano spesso abbastanza delicati (specie quando arriva il momento, abbastanza importante, di applicarli).
Qual'è la differenza principale tra variabili locali e variabili istanza? Il posto dove sono dichiarate! Le prime (variabili locali) sono dichiarate all'interno di un metodo, e solo lì hanno valore (all'esterno del metodo sono addirittura sconosciute ed è possibile creare variabili con il loro stesso nome), le seconde (variabili istanza) sono definite all'esterno dei metodi (ma all'interno della classe) ed hanno valore, quindi, su tutta la classe.
Riporto l'esempio che viene fatto sul libro di testo che uso come riferimento principale:

In ogni caso non è possibile omettere le parentesi tonde a destra della denominazione della classe; queste possono essere vuote (come nei primi due esempi) o possono contenere dati per l'inizializzazione dell'istanza (il numero ed il tipo di tali argomenti sono definiti in un particolare metodo della classe, che prende il nome di costruttore).

 
class Bicicletta extends MezzoDiTrasporto {
	String  tipoBici;     // tipo di bicicletta: ad es. Corsa o Mountain
	int numRapportiAnt;   // numero dei rapporti anteriori (numero corone)
	int numRapportiPost;  // numero dei rapporti posteriori (numero pignoni)
	int rapportoAnt;      // rapporto anteriore in uso
	int rapportoPost;     // rapporto posteriore in uso
}

Possiamo fare subito due importanti osservazioni:

  1. Il testo che vedete a destra delle singole linee è chiamato commento e viene individuato dal simbolo //; tutto quello che, nella stessa riga, segue il simbolo di inizio commento, viene considerato dal compilatore come commento (appunto) e quindi non viene analizzato. I commenti sono estremamente utili in un programma (specie per quelli che si sviluppano su molte linee di codice, ma non solo) ed è un'ottima abitudine (stile) di programmazione, quella di farne largo (ed oculato, ovviamente) uso (è provato, infatti, che persino lo stesso autore ha enormi difficoltà a leggere un codice, comprenderne il funzionamento, le scelte fatte, eventuali errori, ecc...).
    L'uso di commenti diviene poi indispensabile (ed addirittura insufficiente) quando si lavora in team su un unico progetto comune.

  2. Alcune variabili, basta leggerne (dal commento; ecco che già torna utile!) il significato, in realtà, una volta assegnate per una ben determinata istanza, non variano più. Mi riferisco, ad esempio, al tipo di bicicletta... una bici da corsa resta bici da corsa, una montain bike, resta mountain bike, qualunque cambiamento possa subire...
    Sarebbe utile poter "fissare" alcune variabili, in modo da rimarcare il fatto che il loro valore, una volta assegnato, non può più essere modificato. Questo è possibile, con l'uso delle
    costanti.

Visto che abbiamo intravisto la possibilità di utilizzo delle costanti, passiamo subito ad introdurle.
Come si definisce una costante all'interno di una classe? Innanzi tutto diciamo subito che solo le variabili istanza e di classe (quindi non le variabili locali) possono essere forzate ad essere costanti. Per far si che una variabile assuma le caratteristiche di una costante (scusate il gioco di parole, ma non si può che dire così), è necessario anteporre (nella dichiarazione della stessa) la parola chiave
final (che sottolinea come il valore assunto sia appunto quello finale, cioè definitivo e quindi non modificabile).

Ad esempio:

 
final String tipoBici = "Mountain";
final int maxValue    = 10000;

Infine, come terzo ed ultimo tipo di variabile, ci sono le variabili di classe. Queste, come in lezioni precedenti abbiamo già detto (con una trattazione ampia, della quale qui riassumiamo solamente i punti principali), sono variabili visibili a tutta una classe e ad ogni sua istanza; possono quindi essere considerate più generali delle variabili istanza.
Le variabili di classe possono servire per le comunicazioni tra istanza distinte della stessa classe o (meglio ancora) per tenere traccia di una sorta di "stato globale", relativo a tutta una famiglia di oggetti.
Per dichiarare una variabile di classe, si premette la parola
static alla dichiarazione:

  static int numAutomobili;
static final int maxAutomobili = 100;

Metodi
I metodi, come già detto, definiscono il "comportamento" degli oggetti (dalla loro creazione.. all'eventuale distruzione!), e sono (o dovrebbero essere in una buona programmazione) l'
unico modo per accedere ai dati. Ma come si lavora con i metodi?
Innanzi tutto vediamo come definirli:
la definizione di un metodo comprende quattro parti essenziali: 1. nome del metodo stesso, 2. tipo di oggetto (o tipo primitivo) del valore restituito dal metodo (per chi avesse dimestichezza con la programmazione procedurale e non ad oggetti.. si tanga conto che il metodo è molto più simile ad una funzione che ad una procedura), 3. elenco di parametri (con il relativo tipo, ovviamente), 4. corpo del metodo (ovvero l'insieme delle istruzioni che ne definiscono il comportamento).
Diciamo subito che, in realtà, vi possono essere altre parti nella definizione di un metodo ma, visto che si tratta di specifiche "avanzate", credo sia opportuno trattarle successivamente.

Una definizione di metodo semplice ha la forma:

 
tipoRisultato nome (tipo1 arg1, tipo2 arg2, tipo3 arg3,... ) {
  ...
  corpo
  ...
}

dove tipoRisultato e tipo1, tipo2,... possono essere tipi primitivi; se il metodo non deve restituire alcun valore (può capitare, naturalmente), si considera come tipoRisultato il termine void (che indica, appunto, l' "assenza" di tipo).

A questo punto ci si potrebbe chiedere: ma se necessitiamo che il metodo restituisca più di un valore?
Bene! Basta considerare, come tipo in uscita, un array. Ancora non abbiamo parlato di array in Java (li introdurremo ed analizzeremo nella prossima lezione) ma basti pensare che si tratta di un insieme di variabili (che devono però essere di tipo omogeneo).

Se il metodo restituisce un valore (cioè se è stato dichiarato di tipo primitivo o come array), il suo corpo deve contenere l'istruzione return, che provvede (appunto) alla restituzione del valore, ecco come:

 
int somma (int add1, int add2) {
  int risultato;

  risultato=add1+add2;
  return risultato;
}
int somma (int add1, int add2) {
  int risultato=add1+add2;

  return risultato;
}
int somma (int add1, int add2) {
  return risultato=add1+add2;
}

Naturalmente i tre modi (visti sopra) di scrivere questo (semplicissimo) metodo sono equivalenti. Forse il primo è più utile "didatticamente" per capire il significato della parola chiave return ma il terzo è decisamente più semplice e compatto.

Passiamo all'elenco dei parametri. Questo è formato da una serie di dichiarazioni di variabili, separate da virgole, racchiusa tra parentesi tonde e poste a destra del nome del metodo. I parametri diventano variabili locali nel corpo del metodo i cui valori iniziali sono proprio quelli passati all'atto della chiamata del metodo.

Per quel che riguarda il corpo del metodo, ivi possono trovarsi istruzioni, espressioni, chiamate ad altri metodi (anche a metodi di altri oggetti), cicli, ecc...

Osservazione: Nella lezione precedente abbiamo parlato di come richiamare variabili o metodi di una classe (riferendosi ad un oggetto particolare), e questo viene fatto con la notazione puntata esprimendo il nome dell'oggetto prima ed il nome della variabile (o del metodo) dopo. Ma se all'interno di un metodo fosse necessario riferirsi all'oggetto stesso (ad esempio per richiamare un altro metodo o per accedere ad una variabile istanza o per passare l'oggetto stesso ad un altro metodo)?
In questi casi si utilizza la parola chiave
this, che si riferisce (appunto) all'oggetto corrente.

Ecco un esempio:

 
t = this.x;
// a t è assegnata la variabile istanza x dell'oggetto
// corrente
this.metodoJ(2,3); // richiama il metodo metodoJ della stessa classe
OggA.metA (this); // richiama il metodo metA dell'oggetto oggA passandogli
// l'oggetto corrente come parametro
return this; // restituisce l'oggetto corrente

In molti casi però la parola this può essere omessa (e considerata "sottintesa"): le variabili istanza ed i metodi dello stesso oggetto possono riferirsi senza necessità di ulteriori specificazioni (anche se scrivere this, anche in questi casi, non è errato).
Ecco come è possibile quindi riscrivere alcuni degli esempi precedenti:

 
t = x;
// a t è assegnata la variabile istanza x dell'oggetto
// corrente
metodoJ(2,3); // richiama il metodo metodoJ della stessa classe

E' bene precisare però che l'omissione di this è possibile (nei casi indicati) solo se non esistono variabili (o metodi) locali con lo stesso nome di quelli richiamati. Inoltre, poichè this è un riferimento all'istanza corrente di una classe, naturalmente non può essere utilizzato in caso di metodi di classe (qeuelli indicati con static).

Consiglio: Il mio consiglio è quello, specie per i principianti, di utilizzare sempre la parola
this, anche nei casi particolari in cui può essere omessa. Questo sia per non fare confusione che per una sorta di maggiore "pulizia" ed omogeneità nella scrittura.

 

Metodi e loro utilizzo nei programmi Java

Riporto adesso una espressione a mio parere molto significativa in relazione ai metodi ed alla loro importanza all'interno dei programmi Java (e di tutti i programmi object-oriented in generale): "Si può dire che i metodi siano l'elemento principale di ogni linguaggio orientato agli oggetti. Mentre le classi e gli oggetti formano l'intelaiatura, e le variabili di classe ed istanza memorizzano gli attributi e lo stato di una classe o di un oggetto, i metodi definiscono il comportamento di un oggetto e le sue interazione con gli altri" (Lemay, Parkins, Java1.1).

Già in precedenza (specie in questa stessa lezione) sono stati introdotti i metodi e sono state anche fornite le basi necessarie per la stesura dei primi programmi Java. Adesso però cercheremo di focalizzare bene l'attenzione sugli aspetti che rendono i metodi più potenti e flessibili. Ecco quali sono queste caratteristiche:

Analizziamo tali aspetti separatamente.

Overloading dei metodi:
E' data la possibilità, in Java, di definire più metodi con lo stesso nome ma con funzionalità (e quindi corpo) differenti. Per far questo è però necessario che i metodi non abbiano la stessa intestazione (si definisce intestazione il nome e l'elenco dei parametri), quindi che non abbiano gli stessi parametri (in tipo o numero):
Java infatti utilizza il numero ed il tipo di argomenti per scegliere quale definizione del metodo eseguire.
A che cosa può servire la possibilità di dare stesso nome a metodi differenti? Non corriamo il rischio di rendere meno chiaro e leggibile il codice?
Ovviamente no! Così sarebbe se decidessimo di chiamare con lo stesso nome metodi molto diversi (anche concettualmente) tra loro (ad esempio, un metodo che serve per contare gli elementi di un insieme ed un metodo per creare una form sullo schermo!). La caratteristica di poter "sovraccaricare" un metodo (cioè creare, con lo stesso nome, metodi diversi) è anzi data al programmatore per risolvere il problema di dover necessariamente utilizzare nomi diversi per effettuare operazioni simili.

  Un esempio:

Si supponga di voler disegnare un poligono regolare di N lati. Senza la possibilità di effettuare overloading sarebbe stato necessario creare metodi con nomi di questo tipo: disegnaTriangolo (a,b,c), disegnaQuadrato (a,b,c,d), disegnaPentagono (a,b,c,d,e), ecc.. Con l'overloading è possibile utilizzare il nome disegna (,,, ...) per ogni poligono regolare...
La differenza? Sta sul numero dei parametri; a seconda di questo, il compilatore Java richiama automarticamente la porzione di codice relativo al tracciamento del triangolo, del quadrato, ecc...


Metodi costruttori:
Abbiamo accennato ad essi già nella
lezione precedente. Sono metodi particolari che vengono utilizzati per "inizializzare" un oggetto al momento della sua creazione (assegnando alcuni valori di default ai dati, ad esempio). Un metodo costruttore non può essere richiamato all'interno di un programma, ma viene utilizzato direttamente (se esiste) dal compilatore Java all'atto della creazione dell'oggetto in esame.

Vantaggi nell'uso di metodi costruttori:
Definendo metodi costruttori per una classe, si possono impostare i valori iniziali delle variabili istanza, richiamare certi metodi, richiamare metodi di altri oggetti o calcolare determinate proprietà iniziali di un oggetto.. tutte cose svolte "automaticamente" dal costruttore quando si instanzia un nuovo oggetto (con l'istruzione, ricordiamo,
new). In assenza di metodi costruttori in una classe, alla creazione di un oggetto sovente deve seguire l'inizializzazione "esplicita" (non più automatica, quindi) di tutti i parametri ad esso relativi.

Come si distingue un metodo costruttore dagli altri metodi? Attraverso due caratteristiche unicamente associabili a tale particolare metodo:

  Facciamo adesso un piccolo esempio che potete provare voi stessi:
class Persona {
   String nome;
   int anni;

   Persona (String n, int a) {    	// metodo costruttore
      nome = n;			  // inizializza nome
      anni = a;			// inizializza età
   }

   void stampaPersona () {
      System.out.print ("Ciao, io mi chiamo "+nome);
      System.out.println (" ed ho "+anni+" anni.");
      System.out.println ("------");
   }

   public static void main (String arg[]) {
      Persona p;	  		// p è di tipo Persona

      p = new Persona ("Alessandro",25);    // p è Alessandro ed ha 25 anni
      p.stampaPersona ();		  // stampa dati di Alessandro
      p = new Persona ("Maria",24);	  // adesso p è Maria, 24 anni
      p.stampaPersona ();		  // stampa dati di Maria
   }
}
Il risultato dell'esecuzione del programma dovrebbe essere il seguente:
	Ciao, io mi chiamo Alessandro ed ho 25 anni.
	 ------
	 Ciao, io mi chiamo Maria ed ho 24 anni.
	 ------

Possono crearsi più costruttori per una stessa classe, utilizzando il metodo dell'overloading precedentemente descritto. In tal modo è possibile anche differenziare l'inizializzazione dell'oggetto che si intende instanziare, in base ai parametri passati all'istruzione new.


Metodi distruttori (o conclusivi)
:
Tali metodi sono un po' il "contrario" dei metodi costruttori: un costruttore inizializza l'oggetto, il metodo conclusivo viene richiamato invece prima che l'oggetto venga rimosso (non serve più!).
Per definire un metodo conclusivo per una classe, è necessario ridefinire (perchè già definito nella classe Object, come vedremo) il metodo
finalize() con la seguente signature:

 
protect void finalize() throws Throwable {
  super.finalize();
}

La parte throws Throwable si riferisce al trattamento di eventuali errori (eccezioni), questo sarà argomento di lezioni più avanzate.

Il corpo del metodo dovrebbe contenere tutte quelle operazioni di "pulizia" da compiere prima della rimozione di un oggetto, cioè: eliminare riferimenti ad altri oggetti, liberare risorse esterne occupate,... I metodi conclusivi sono quindi utilizzati per ottimizzare la rimozione di un oggetto ma non lo rimuovono automaticamente (nel senso che è comunque necessario che vengano eliminati tutti i riferimenti a quell'oggetto, perchè venga definitivamente rimosso).
Tale metodo, generalmente, non è necessario; lo si utilizza solo in casi particolari dei quali parleremo in lezioni di livello più avanzato.

Ridefinizione di metodi:
Allora, partiamo dal presupposto che, quando si richiama un metodo, Java cerca la sua definizione nella classe di quest'ultimo e, se non la trova (con la signature corretta: nome del metodo, tipo restituito, numero e tipo di parametri, devono coincidere!), risale la gerarchia delle classi fino a trovare un metodo con quel nome:
l'editarietà permette di utilizzare un metodo nelle sottoclassi senza doverne duplicare il codice.
Può però essere utile che un oggetto rispondesse a un metodo, ma "reagisse" in modo diverso (rispetto oggetti di altre sottoclassi della stessa classe "antenata"). In tal caso basta "ridefinire" il metodo in esame, cioè inserire nella classe attuale un metodo con la stessa signature dell'altro.
Alla chiamata del metodo, la versione definita nella classe attuale viene individuata prima di eventuali versioni definite in superclassi (che risultano quindi "nascoste" o "mascherate").

Perchè dovrebbe essere utile ridefinire un metodo già presente in una superclasse?

Per il secondo punto (il primo è già stato esaminato: riscrivendo il metodo, il precedente viene "nascosto"), c'è un modo per evitare di duplicare il codice del metodo della superclasse, ed è quello di utilizzare la parola chiave super (che consente di risalire la gerarchia).

 
void MetodoProva (String a, int b) {
  ....         //istruzioni aggiuntive per arricchire il comportamento
  super.MetodoProva (a,b); 	// richiama il codice del metodo (dalla superclasse)
  ....	//ancora istruzioni per arricchire il metodo
}

Osserviamo che la parola super è simile a this, a differenza di quest'ultima, super si riferisce alla superclasse invece che alla classe corrente.

torna sopra


Bibliografia

 

Questo articolo è stato scaricato dal Club di informatica
Pagina curata da:
Alessandro Luparello