Ripasso delle classi (tratto da una lezione del Prof. Saccà)
- La programmazione ad oggetti in Java
Java è un linguaggio per la programmazione orientata ad oggetti (OO - object oriented) molto potente e molto utile nello sviluppo di programmi moderni affidabili e portabili. La nascita di Java risale al 1991 allorquando Sun Microsystems lanciò un progetto di ricerca interno chiamato Green che ha dato vita a un linguaggio ad oggetti basato su C++, un altro linguaggio ad oggetti, estensione di C, ancora molto utilizzato per la sua efficienza. Java fu presentato ufficialmente nel 1995 e da allora viene utilizzato per diversi scopi, dai contenuti dinamici e interettavi delle pagine Web ad applicazioni aziendali su larga scala e cosi via, in virtù della sua portabilità e nonostante la sua non elevata efficienza.
Fin qui abbiamo utilizzato Java come un semplice linguaggio di programmazione procedurale. La programmazione procedurale si basa su varie unità di programmazione in cui sono definite varie strutture dati e un programma è costituito da una unità principale che viene manda in esecuzione ed eventualmente richiama altre unità (metodi). La programmazione ad oggetti è basata su strutture dati che hanno non solo dati ma anche comportamento, cioè metodi per cui la programmazione consiste nel definire classi (cioè collezioni) di oggetti e l'esecuzione consiste nel richiedere ad un oggetto di eseguire un proprio metodo durante il quale può essere richiesto l'esecuzione di altri metodi dello stesso oggetto o di altri oggetti della stessa classe o di altre classi.
- Le classi in Java
La componente fondamentale di Java come linguaggio OO è la classe che include la definizione sia di dati che di metodi. Le classi vanno definite a gruppi - un gruppo di classi è chiamato package. Una classe ha la seguente sintassi (in verità semplificata):
<classe> ® <tipo-utilizzo> "class" <identificatore> "{" <corpo-classe> "}"
<corpo-classe> ® <definizione-dato> ";" <corpo-classe> | <definizione-metodo> ";" <corpo-classe> | e
<definizione-dato> ® <tipo-utilizzo> <statico-o-no> <tipo-dato> <identificatore>
<definizione-metodo> ® <tipo-utilizzo> <statico-o-no> <tipo-restituito> <identificatore> "(" <argomenti> ")"
"{" <corpo-metodo> "}"
<tipo-utilizzo> ® "public" | "private" | e
<statico-o-no> ® "static" | e
Una classe va definita "public" per essere utilizzata senza restrizione; altrimenti, se non è specificato nulla (cioè caso e), essa può essere utilizzata solo nel suo package; per il momento non usiamo la opzione "private" per le classi. Si noti che, per poter utilizzare in un package A una classe C di un package B, bisogna scrivere nel package A la istruzione: import B.C o import B.* se si vogliono utilizzare tutte le classi del package B.
Per spiegare la semantica delle classi, spieghiamo di seguito the modalità diverse di utilizzo delle classi di cui la terza è la modalità più estesa e pienamente rispondente alle caratteristiche dalla programmazione ad oggetti.
- La classe come modulo
Una primo modo di utilizzo è considerare una classe come un raccoglitore di metodi che abbiano funzionalità simili - si tratta di organizzare un modulo di funzioni. Per ogni metodo vanno specificate, oltre al tipo restituito, il nome e i parametri formali, le seguenti clausole: public (cioè in modo che il metodo sia utilizzabile all’esterno della classe e del package) e static (per permetterne l’utilizzo indipendentemente dall’esistenza di un oggetto di una classe – di questo aspetto parleremo dopo).
Ad esempio potremmo definire una classe UtilitaVettori che contiene vari metodi per la manipolazione di vettori di interi:
public class UtilitaVettore {
public static void insertionSort (int [] V, int in, int fin )
{ // codice del metodo
…
}
public static void bubbleSort (int [] V, int in, int fin )
{ // codice del metodo
…
}
public
static boolean eOrdinato (int [] V, int in, int fin )
{ // codice di un metodo che restituisce true se V è ordinato nel tratto da in a fin
…
}
public static int cerca (int [] V, int in, int fin, int X )
{ // codice di un metodo che restituisce l'indice di X in V se esiste o -1 altrimenti
…
}
public
static void swap (int [] V, int
i, int j )
{ int temp = V[i]; V[i] = V[j]; V[j] = temp; // scambio di due elementi
}
// altri eventuali metodi
…
}
La classe potremmo definirla in un package di utilità di nome ad esempio Utilita. In una classe definita nello stesso package è possibile utilizzare un metodo di UtilitaVettore, ad esempio insertionSort, richiamando il metodo con il comando: UtilitaVettore.insertionSort(V,0,30). Nel caso di utilizzo in una classe di un package diverso, va utilizzato il comando: Utilita.UtilitaVettore.insertionSort(V,0,30), cioè va specificato anche il nome del package.
- La classe come record
Una secondo modo di utilizzo è considerare una classe come un raccoglitore di dati, cioè un record. Ad esempio, si considerino i dati di uno studente: nome, eta, matricola, che si intendono manipolare in un programma. Invece di definirle come tre variabili distinte possiamo raggrupparle come campi in una classe nel seguente modo:
public class Studente {
public
int matricola;
public String nome;
public int eta;
}
Possiamo ora definire una variabile di tipo studente, inizializzarla e poi manipolare i suoi campi:
Studente s; // definizione
s = new Studente(); // inizializzazione
s.matricola = 2040; // assegnazione di un valore a un campo
out.print(s.matricola); // consultazione del valore di un campo
E' anche possibile definire ed inizializzare in una solo istruzione: Studente s = new Studente();
Per poter manipolare i campi è necessario definirli public (o per lo meno non specificare nulla se si vogliono utilizzare solo all'interno del package.
Mentre l'utilizzo di una classe come modulo, pur non essendo esteso, è significativo, l'utilizzo della classe come record dovrebbe essere evitato in un linguaggio OO come Java e si dovrebbe ricorre al terzo utilizzo descritto di seguito.
- La classe per la definizione di Oggetti
Una classe viene anche e più spesso utilizzata per definire oggetti caratterizzati da uno stato (descritto da un certo numero di campi) che viene inizializzato da metodi chiamati costruttori, metodi che hanno stesso nome della classe e non restituiscono nulla pur non richiedendo la clausola void.
Per l’oggetto vanno poi tipicamente definiti metodi di consultazione dello stato e metodi di aggiornamento dello stato. Si noti che è possibile avere metodi con lo stesso nome a patto che essi differiscono per qualche parametro formale. I campi vanno dichiarati private (non obbligatorio ma fortemente consigliato) in modo che possano essere utilizzati solo attraverso i metodi di aggiornamento e di consultazione.
Ad esempio la seguente classe definisce oggetti di tipo Data, il cui stato è costruito da tre campi (tre interi corrispondenti a giorno, mese ed anno). E’ di norma consigliato definire i campi private in modo che essi siano accessibili direttamente all’interno della classe in modo da evitare una manipolazione inopportuna di una data (ad esempio fissare un giorno superiore a 31).
Definiamo due costruttori: uno che genera una data passata in ingresso e il secondo che genera una data prefissata (ad esempio 1-1-2000). Ambedue i costruttori sono dichiarati public in modo che siano utilizzabili all’esterno della classe. Introduciamo inoltre anche un certo numero di metodi sia di consultazione sia di aggiornamento. Anche tali metodi sono dichiarati public in modo che siano utilizzabili all’esterno della classe. Per tali metodi non utilizziamo la clausola static in quanto essi si riferiscono a uno specifico oggetto di tipo data. Tale clausola potrebbe essere usata per metodi (detti di classe) che non si riferiscono ad alcun oggetto ma che sono comunque inclusi nella classe perché hanno qualche relazione con esse, ad esempio il metodo corretta per verificare se una tripletta (giorno, mese, anno) corrisponde ad una data. Inoltre abbiamo due metodi static per verificare se un anno è un fine secolo (ad esempio 1900, 2000) o è un anno bisestile (divisibile per 4 o, se fine secolo, divisibile per 400). Di tali metodi c’è anche una versione non static che si riferisce a un oggetto data per cui non va passato l’anno essendo esso implicito.
public class Data {
final
static int maxgiorno[]={31,28,31,30,31,30,31,31,30,31,30,31};
private int giorno, mese, anno;
// costruttori
public
Data (int g, int m, int a ) { setData(g,m,a); }
public Data () { giorno = mese = 1; anno = 2000; }
// metodi di consultazione diretta
public int getGiorno () { return giorno; }
public int getMese () { return mese; }
public int getAnno () { return anno; }
// metodi di consultazione indiretta
public boolean eFineSecolo () { return eFineSecolo(anno); }
public
boolean eBisestile () { retrun eBisestile(anno) }
public
String toString () {
String
dat=giorno+"-"+mese+"-"+anno;
return dat;
}
// metodi di aggiornamento (restituiscono false se aggiornamento errato)
public
boolean setData ( int g, int m, int
a ) {
{ if ( corretta (g,m,a) ) {
giorno = g; mese = m; anno = a; return true;
}
else {
giorno = mese = 1; anno = 2000; return false;
}
}
public
boolean setGiorno ( int g ) {
{ if ( corretta (g,mese,anno) ) {
giorno =
g; return true;
}
else
return false;
}
public
boolean setMese ( int m ) {
{ if ( corretta (giorno,m,anno) ) {
mese =
m; return true;
}
else
return false;
}
public
boolean setAnno ( int a ) {
{ if ( corretta (giorno,mese,a) ) {
anno =
a; return true;
}
else
return false;
}
// metodo di aggiornamento indiretto
public void incrGiorno ( ) {
{ if ( corretta (giorno+1,mese,anno) )
giorno++;
else {
giorno =1;
if ( corretta (giorno,mese+1,anno) )
mese++;
else {
mese = 1; incrAnno();
}
}
}
// metodi di classe
public
static boolean corretta ( int g, int m, int
a ) {
{
if ( g < 1 || g > 31 || m < 1 || m > 12 )
return false;
if ( g == 29
&& m==2 && eBisestile(a) )
return true;
if ( g >maxgiorno[m-1])
return false;
return true;
}
public
static boolean eFineSecolo
( int a ) { return a % 100 == 0; }
public
static boolean eBisestile (
int a ) {
return a % 4 == 0 && (!eFineSecolo(a) || a % 400 == 0);
}
} // fine classe
Esempi di utilizzo:
Data d, e;
out.print(d.getAnno()); // errato – d non è stato ancora istanziato
if ( Data.corretta(29,2,2000) ) // corretto perché è un metodo di classe
out.print("La data 29-02-2000 e' corretta<BR>");
else
out.print("La data 29-02-2000 non e' corretta<BR>");
d = new Data(1,2,2001);
out.print ("Nuova data="+d.toString());
out.print("<BR> Anno="+d.getAnno()+"<BR>");
e = new Data();
out.print ("Data di default "+e.toString());
Di
seguito viene presentata
public class Razionale {
private num, denom; private boolean errato;
public Razionale ( int n, int d ) { setRazionale(n,d);}
public Razionale ( int n ) {
num = n; denom = 1; errore = false;
}
public Razionale ( Razionale r ) {
num = r.num; denom = r.denom; errore = false;
}
// metodi di consultazione diretta
public
int getNum () { return num;
}
public
int getDenom () { return denom; }
public
boolean eErrato () { return
errato; }
// metodi di consultazione indiretta
public
boolean ePositivo () {
return num > 0; }
public
boolean eZero () { return
num == 0; }
public boolean eMaggiore ( Razionale r ) {
return num*r.denom > r.num*denom;
}
public boolean eUguale ( Razionale r ) {
return num*r.denom == r.num*denom;
}
public String toString (){
String raz; raz=num+"/"+denom;
return raz;
}
// metodi di aggiornamento (restituiscono false se aggiornamento errato)
public
boolean setRazionale ( int n, int d ) {
if ( d == 0 ) {
num = denom = 1; errore =
true;
}
else {
num = (d>0)? n: -n; denom = (d>0)? d: -d; errore = false;
}
return !errore;
}
public
void setNum ( int n ) { num
= n; }
public
boolean setDenom ( int d ) {
if ( d == 0 ) {
num = denom = 1; errore =
true;
}
else {
num = (d>0)? num: -num; denom = (d>0)? d: -d; errore = false;
}
return !errore;
}
// metodi di aggiornamento indiretto
public void riduci ( ) {
int k = UTIL.MCD(num,denom);
num /= k; denom /= k;
}
public void cambiaSegno ( ) { num = - num; }
public
boolean inverti ( ) {
if ( num == 0 ) {
num = denom = 1; errore = true;
}
else {
int k = num;
num =
(k>0)? denom: -denom; denom = (k>0)? k: -k;
}
return !errore;
}
public void somma ( Razionale r ) {
int k =
UTIL.mcm(denom, r.denom);
num = (k / denom)*num + (k / r.denom)*r.num; denom = k;
riduci();
}
public void sottrai ( Razionale r ) {
Razionale k = new Razionale(r); k.cambiaSegno();
somma(k);
}
public void moltiplica ( Razionale r ) {
num *= r.num; denom *= r.denom;
riduci();
}
public boolean dividi ( Razionale r ) {
Razionale k = new Razionale(r); k.inverti();
moltiplica(k);
return k.errore;
}
// metodi di classe
public static Razionale piu ( Razionale r1, Razionale r2 ) {
Razionale r = new Razionale(r1);
r.somma(r2);
return r;
}
public static Razionale meno ( Razionale r1, Razionale r2 ) {
Razionale r = new Razionale(r1);
r.sottrai(r2);
return r;
}
public static Razionale per ( Razionale r1, Razionale r2 ) {
Razionale r = new Razionale(r1);
r.moltiplica(r2);
return r;
}
public static Razionale div ( Razionale r1, Razionale r2 ) {
Razionale r = new Razionale(r1);
r.dividi(r2);
return r;
}
}
Si noti che UTIL è una classe in cui abbiamo definito i metodi di classe MCD e mcm, utilizzando l’algoritmo di Euclide.
Esempi di utilizzo:
Razionale a; Razionale b = new Razionale (1,3);
b.moltiplica(new Razionale(2)); // adesso b vale 2/3
a = new Razionale(b); // a vale 2/3
a.dividi(b); // a vale 1
Razionale r = Razionale.piu(a,b); // r vale 5/3
-
Esercizi
di Ripasso