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 la classe Razionale il cui stato è rappresentato da due interi (numeratore e denominatore) e un booleano che vale vero se qualche volta al denominatore del razionale è stato assegnato un valore 0 – ma tale valore non viene però registrato. Inoltre tramite il booleano errato memorizziamo il fatto che al numero sia stato assegnato un valore errato (denominato pari a zero) - in effetti una soluzione più efficace consiste nel ricorrere all'utilizzo delle eccezioni ma non nello scrivere in output (le classi sono utilizzate da altre classi che non sanno leggere!). Il denominatore è sempre positivo per cui il segno del numero razionale dipende dal numeratore. Esistono tre costruttori: uno da una coppia di interi, il secondo per costruire interi e il terzo per costruire un razionale a partire da un altro razionale. Per ogni operazione esistono due versioni: una di oggetto, in cui un operando è l’oggetto stesso che viene modificato dall’operazione, e la seconda di classe con due operandi che restituisce un nuovo oggetto risultato.

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

 

  1. Realizzare una classe JAVA Orario e una classe TestOrario che la testi. La classe Orario sarà composta di campi ora, minuti, secondi e implementerà i seguenti metodi:
    1. Orario()  // inizializza l’ora a mezzanotte.
    2. Orario(int o, int m, int s)        // inizializza l’ora a o, i minuti a m e i secondi a s
    3. public void Leggi()    //legge l’ora da tastiera
    4. public void Scrivi()   //scrive l’ora nel formato       10:23:35
    5. public static boolean corretta(int o, int m, int s)        // verifica che l’orario inserito sia corretto (o:m:s)
    6. public void AumentaSecondo()                     //aumenta l’orario di 1 secondo
    7. public void AumentaSecondi(int s)               //aumenta l’orario di s secondi
    8. public boolean Mezzogiorno()                      //restituisce true se siamo a mezzogiorno