Paola Magillo, Univestita' di Genova, Corso di Programmazione II per SMID, a.a. 2007-2008.

Lezione 02:

CLASSI E OGGETTI IN JAVA

Riepilogo

Che cosa e' una classe

Una classe e' l'analogo di un tipo ma comprende anche le funzioni che agiscono su quel tipo.
E' come un record dove alcuni campi sono variabili ed altri sono funzioni.

Analogia tra classe e modulo:

Che cosa e' un oggetto

Come si dice "un valore di un tipo" cosi' si dice "un oggetto di una classe".
Si dice che un oggetto e' istanza della sua classe.

Un oggetto ha:

L'identita' di un oggetto e' la locazione di memoria dove stanno scritti i suoi dati. I valori contenuti in questa locazione possono cambiare nel tempo ma la locazione e' sempre la stessa. Un oggetto e' la "nobilitazione" di un puntatore (per chi sa che cosa sono).

Identita' e' rappresentata da numero detto OID (Object IDentifier, identificatore di oggetto).

Esempio visto: classe rettangolo in Java

La classe "rettangolo" ha due interi per lunghezza e larghezza, un metodo "area" che ritorna l'area del rettangolo e un metodo "print" che stampa il rettangolo.

class Rectangle
{
  int length, width;
  int area() { return length * width; }
  void print()
  { System.out.println("Rectangle " + length + " x " + width); }

  public static void main(String[] argc)
  {
    Rectangle r = new Rectangle();
    int a;
    r.length = 3; r.width = 5;
    a = r.area();
    r.print();
    System.out.println("Area = " + a);
  }
}

L'argomento implicito e "this"

Abbiamo detto che in una chiamata di metodo (es. r.area() dove r e' un rettangolo), l'oggetto a cui appartiene il metodo (qui r) e' argomento implicito del metodo.

All'interno dell'implementazione del metodo l'argomento privilegiato e' lasciato implicito, ma posso anche esplicitarlo usando la parola chiave "this".
Es. nel rettangolo e' equivalente scrivere:

  int area() { return length * width; }
  int area() { return this.length * this.width; }

Usare "this" e' indispensabile quando si vuole ritornare questo oggetto come risultato. Esempio:

  Rectangle bigger(Rectangle r)
  { if (this.area() > r.area()) return this; else return r; }

Costruttore

Metodo speciale che ogni classe ha, serve per fabbricare nuove istanze. Serve per allocare e inizializzare lo stato di un oggetto al momento della sua creazione. Puo' avere parametri a piacere ma non ha valore di ritorno.

Se non lo scrivo nella dichiarazione della classe, il sistema mette automaticamente un costruttore di default: senza argomenti, crea istanza mettendo tutte le variabili a zero. Si chiama con questa sintassi:

Rectangle r = new Rectangle();

Posso scrivere io un costruttore, anche con parametri. Es. per il rettangolo:

  Rectangle(int l, int w) { length = l;  width = w; }

Si chiama con questa sintassi:

Rectangle r = new Rectangle(5,12);

Se scrivo il mio costruttore, perdo quello di default. Se lo voglio, devo ridefinirlo. Es. nel caso del rettangolo:

  Rectangle() { length = width = 0; }

Nota: una classe puo' avere piu' costruttori con parametri diversi. E' un caso di polimorfismo (ved. tra poco).

Oggetto nullo

La parola chiave null denota oggetto nullo, inesistente.

Esempi di inizializzazione per una variabile oggetto:
es.1: Rectangle r = new Rectangle(); la variabile r punta a un nuovo rettangolo appena costruito (cioe' contiene il suo OID).
es.2: Rectangle r = null; la variabile r punta a nessun oggetto.
es.3: Rectangle r; e' come averlo inizializzato a null.

E' errore accedere agli attributi dell'oggetto nullo o chiamare metodi sull'oggetto nullo.

Se ho inizializzato r come in es.2 o es.3, poi scrivere r.width oppure r.area() provoca errore.

Polimorfismo

In linguaggio tradizionale non posso avere funzioni diverse (cioe' con diversi tipi per argomenti e/o risultato) e con nome uguale.
In linguaggio object oriented si puo'. Si chiama "polimorfismo".

Esempio: nella classe rettangolo ora abbiamo due costruttori.

Polimorfismo vale non solo per i costruttori. Es. nel rettangolo aggiungo funzione "assegna dimensioni" che ha due forme:

  void setSize(int l, int w)  {  length = l; width = w;  }
  void setSize() {  length = width = 0;  }

Naturalmente posso avere funzioni con stesso nome in classi diverse. E questo non e' polimorfismo (e' come avere campi con stesso nome in tipi record diversi).

Gestione della memoria

Dalla sua costruzione, un oggetto ha sua propria identita' = locazione di memoria dove viene memorizzato.

Linguaggi o.o. tengono una tabella degli oggetti esistenti. L'indice nella tabella e' l'identita' dell'oggetto (OID = object identifier).

Identita' resta inalterata in seguito a modifiche dello stato dell'oggetto (= valori dei suoi attributi).
Modifiche cambiano contenuto della memoria ma non sua locazione.

Esempio:

 1. Rectangle r1 = new Rectangle(2,3);
 2. Rectangle r2 = new Rectangle(5,3);
 3. r1.setLength(5);
 4. Rectangle r3 = r1;
 5. r3.setWidth(4);
 6. r1 = new Rectangle(1,8);
 7. r3 = new Rectangle(10,15);

Un oggetto non e' la variabile che lo denota.

Piu' variabili possono "puntare" lo stesso oggetto (r1 ed r3 dopo istruzione 4).

Un oggetto esiste indipendentemente da quali variabili lo puntanto (dopo istruzione 6 l'oggetto nato come r1 e' puntato solo da r3).

Java, quando un oggetto non e' piu' puntato da nessuna variabile, lo cancella: libera la sua locazione di memoria e la rende disponibile per altri oggetti, cancella il suo OID (es. dopo istruzione 7).
Questo meccanismo si chiama garbage collection (= raccolta della spazzatura).

Garbage collection

In molti linguaggi O.O. incluso Java c'e' garbage collection = gestione automatica di allocazione e deallocazione della memoria dinamica.

La memoria di un oggetto viene allocata alla sua creazione (chiamata al costruttore della classe con istruzione "new" ) e viene deallocata automaticamente quando non e' piu' accessibile attraverso nessuna variabile.

Attributi e metodi di istanza / di classe

In Java variabili e funzioni di classe sono contrassegnate da parola chiave "static".

Esempio: nella classe "rettangolo" posso mettere variabile di classe

  static int minimumDimension = 0;
e usarla nelle funzioni che assegnano lunghezza e larghezza:
  void setLength(int l)
  { if (l>=minimumDimension) length = l;
    else System.out.println("Invalid length!"); }
se a un certo punto decido di accettare solo rettangoli con lati lunghi almeno 10, basta mettere minimumDimension = 10.

Posso renderla una costante (cioe' il suo valore non potra' piu' essere cambiato) premettendo la parola chiave "final":

  static final int minimumDimension = 0;

Convenzione: i nomi di costanti sono a lettere tutte maiuscole (come le macro in C). Quindi meglio scrivere:

  static final int MINIMUM_DIMENSION = 0;

Visibilita' di attributi e metodi

Classe = Modulo come scatola chiusa. Che cosa si vede da fuori la scatola?
Attributi e metodi possono essere dichiarati come pubblici o privati.

In java devo ripetere le parole chiave "public" e "private" prima di ogni variabile o funzione. Dove non specificato, si intende "public".

Incapsulazione

Si dice che una classe incapsula lo stato dei suoi oggetti quando tutte le variabili che descrivono lo stato sono private, percio' da fuori si possono leggere ed assegnare solo mediante le funzioni definite nella classe.

Buona norma: definire attributi privati con metodi "set" e "get" pubblici. Es. per il rettangolo:

class Rectangle
{
  private int length, width;
  public void setLength (int l)  { length = l; }
  public void setWidth (int w)  { width = w; }
  public int getLength() { return length; }
  public int getWidth() { return width; }
  ...
}
allora non posso piu' scrivere
  r.length = 3; r.width = 5;
ma devo scrivere invece
  r.setLength(3); r.setWidth(5);

Incapsulazione e' particolarmente utile quando i valori dello stato sono soggetti a vincoli di validita', e quindi e' importante evitare che qualcuno li modifichi in modo incontrollato.
Esempio: dimensioni del rettangolo non possono essere negative:

  public void setLength (int l)
  { if (l>=0) length = l; else System.out.println("Negative length!"); }
  public void setWidth (int w)
  { if (w>=0) width = w; else System.out.println("Negative width!"); }

Metodi ausiliari

Metodi privati tipicamente sono funzioni ausiliarie usate solo dentro la classe per facilitare l'implementazione di altri metodi.

Ereditarieta' (inheritance)

Corrisponde all'idea di un tipo che e' raffinamento di un altro tipo, cioe' gli aggiunge qualcosa.

Una sotto-classe raffina / estende / arricchisce una super-classe aggiungendo nuove variabili e/o nuove funzioni, oppure ridefinendo l'implementazione (cioe' sostituendo il body) di funzioni gia' presenti nella super-classe.

Esempio: Classe Cuboid (coboide o parallelepipedo) aggiunge una dimensione (altezza "height") al rettangolo. Eredita tutte le altre variabili e funzioni dalla classe Rectangle.
Aggiunge anche funzione che calcola il volume del cuboide. E ridefinisce l'implementazione della funzione "print".

class Cuboid extends Rectangle
{
  int height; 
  // attributi length e width sono ereditati
  int volume() 
  { return area() * height; }
  void print()
  { System.out.println("Cuboid " + length + " x " + width + " x " + height);
  }
  // tutti gli altri metodi sono ereditati
}

Nota: la sottoclasse deve ridefinire il costruttore (che non viene ereditato). Nel costruttore della sotto-classe e' possibile (non obbligatorio) richiamare quello della superclasse usando "super".
Esempio per i cuboide:

  Cuboid(int l, int w, int h)
  { super(l,w); height = h; }  

In generale, quando l'implementazione di un metodo viene ridefinita nella sottoclasse, si puo' (se opportuno) chiamare l'implementazione di quel metodo nella superclasse tramite la parola chiave "super", che denota questo stesso oggetto visto come appartenente alla superclasse.
Es. alternativa per metodo di stampa del cuboide:

  void print()
  { System.out.println("Cuboid:");
    System.out.print("Base ");
    super.print(); // stampa il rettangolo di base
    System.out.println("Height: " + height); // stampa altezza
  }

Altra nota: il metodo "main" non viene ereditato. Quindi se voglio poter eseguire la classe Cuboid, devo aggiungere il "main".

Terminologia:

Relazione "is-a"

Se B e' sottoclasse di A, allora tutti gli oggetti di B sono anche oggetti di A.

Un oggetto di classe B e' un (is a) oggetto di classe A.

Esempio: un cuboide e' anche un rettangolo.

Dovunque si puo' usare un oggetto della super-classe si puo' usare anche un oggetto della sottoclasse.
Se B e' sottoclasse di A posso mettere un oggetto di classe B dentro una variabile di classe A.

Rectangle r = new Cuboid(12,6,3);

Nonostante l'oggetto sia della sotto-classe, finche' sta in variabile della super-classe gli posso applicare solo metodi della super-classe. Quello che la sottoclasse ha aggiunto viene "dimenticato".

Binding

Binding = associazione tra nome di funzione e sua implementazione.
Quando viene chiamata una funzione che ha implementazione diversa nella super-classe e nella sotto-classe, come viene determinata l'implementazione da usare? quella della super- o quella della sotto-classe?

Esempio:

Rectangle r;
r = new Cuboid(12,5,20);
r.print();

Java ha binding dinamico.

Visibilita' di attributi e metodi

Variabili e funzioni oltre che private e pubbliche possono essere protette: le puo' usare solo la classe stessa oppure le sue sottoclassi. Quelle private le puo' usare solo la classe stessa. Riassumendo:

Nell'esempio del rettangolo e del cuboide:
Se in Rectangle dichiaro privati gli attributi length e width, non li posso usare nel corpo di funzioni della sottoclasse Cuboid.
Meglio dunque dichiararli protetti.