Ruolo delle classi in Java

Le classi in Java possono avere due ruoli:

Classi come prototipi (template) di oggetti:
In questo caso:
  • possiamo creare nuove istanze della classe;
  • ogni istanza ha un proprio stato interno;
  • i metodi vengono invocati sugli oggetti.

Come esempio, si consideri la classe BankAccount:

  • possiamo creare nuovi oggetti, es.
    BankAccount b = new BankAccount(1460.5);
  • lo stato di un oggetto BankAccount è dato dal valore della variabile di istanza balance;
  • le funzionalità vengono invocate su un oggetto specifico, es.
    b.deposit(125.25);
    System.out.println(b.getBalance());
Classi come contenitori di variabili e metodi statici:
In questo caso:
  • stato e funzionalità sono associati alla classe stessa, e non ai suoi oggetti;
  • i metodi vengono invocati sulla classe.

Come esempio, si considerino le classi ArrayUtils, MyRecursiveMethods e java.lang.Math [locale, Medialab, Sun]:
  • non avrebbe senso creare istanze di queste classi;
  • le classi ArrayUtils e MyRecursiveMethods contengono solo metodi, non variabili e quindi non hanno stato; analogamente, lo stato della classe java.lang.Math è determinato dal valore delle costanti matematiche messe a disposizione (es. Math.PI);
  • le funzionalità vengono invocate sulla classe, magari passando opportuni parametri, es.
    ArrayUtils.print({1,2,3,4,5});

    int i = MyRecursiveMethods.factorial(5);

    double d = Math.sqrt(125.25);

Attenzione: Vedremo che questi due ruoli (prototipo / contenitore) possono anche essere presenti contemporaneamente nella medesima classe.






Classi come prototipi

Nel primo ruolo, una classe descrive le caratteristiche comuni di una collezione di oggetti.

La dichiarazione di classe introduce un nuovo tipo di dato (la classe stessa), i cui elementi saranno i suoi oggetti.

Nella classe vengono dichiarati:

  • le variabili di istanza, che costituiscono lo stato degli oggetti;
  • i metodi di istanza, che realizzano le funzionalità degli oggetti;
  • i costruttori, che determinano le modalità di creazione degli oggetti.
Quando un oggetto viene creato (es. con l'ausilio del comando new) gli viene associata una copia di ogni variabile di istanza.


Per accedere ad una variabile di istanza di un oggetto, si usa la sintassi
<oggetto>.<nomeVariabile>
e analogamente per invocare un metodo, si scrive
<oggetto>.<nomeMetodo>(<lista_parametri>)
Ad esempio,
 
BankAccount conto1 = new BankAccount(1000);
conto1.withdraw(500);
// crea un oggetto
// chiamata di metodo





Variabili e metodi statici

In una classe possiamo anche dichiarare uno stato e delle funzionalità che sono associati alla classe stessa, e non ai suoi oggetti. Si possono infatti dichiarare:
  • variabili statiche, che costituiscono lo stato della classe;
  • metodi statici, che realizzano le funzionalità della classe.
A volte, variabili e metodi statici vengono chiamati di classe.


Ad esempio, abbiamo visto in diverse classi la dichiarazione del metodo

  public static void main (String[] args) { ... }

che viene invocato quando si esegue la classe con il comando java.



Per accedere ad una variabile statica di una classe, si usa la sintassi
<nomeClasse>.<nomeVariabile>
e analogamente per invocare un metodo statico, si scrive
<nomeClasse>.<nomeMetodo>(<lista_parametri>)
Ad esempio, tutti i metodi della classe ArrayUtils sono statici:

public class Test3 { 
    public static void main (String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        ArrayUtils.print(array);
        ArrayUtils.reverse(array);
        ArrayUtils.print(array);
    }
}




Dichiarazione di una classe

Lo schema generale di dichiarazione di una classe in Java è il seguente:
 
<modificatori> class <nome-classe> {
<costruttori>
<variabili statiche>
<metodi statici>
<variabili d'istanza>
<metodi d'istanza>
}

Quindi in una classe che definisce un prototipo per le sue istanze, oltre a variabili d'istanza, metodi d'istanza e costruttori possono essere dichiarati anche variabili e metodi statici.

Variabili e metodi (siano essi d'istanza o statici) sono chiamati anche membri della classe.

NOTA: L'ordine delle dichiarazioni all'interno del corpo di una classe non è importante.




Esempio: la classe Student

La classe Student contiene variabili e metodi sia statici che di istanza.

public class Student { 

    // variabili d'istanza 
    private String name;  // nome 
    private int ID;       // matricola 
    public double test1, test2, test3; 
                         // voti per tre esami 

    // variabile statica: memorizza il piu' piccolo 
    // numero di matricola disponibile 
    private static int nextUniqueID = 1;

    // costruttore: fornisce il nome per lo 
    // studente, e gli assegna una matricola unica 
    public Student(String theName) { 
        name = theName; 
        ID = nextUniqueID; 
        nextUniqueID++;
    } 

    // metodo d'istanza: calcola la media 
    public double getAverage() {
        return (test1 + test2 + test3) / 3; 
    } 

    // metodo statico: restituisce il prossimo 
    // numero di matricola disponibile 
    public static int getUniqueID() {
        return nextUniqueID; 
    }

    // nuovi metodi ausiliari per accedere alle  
    // variabili d'istanza private 
    public String getName() {
        return name; 
    }
    public int getID() { 
        return ID;
    }
}

Si osservi che per garantire l'incapsulamento, le variabili name ed ID sono dichiarate private: vengono inizializzate al momento della creazione, e possono essere accedute solo in lettura con i metodi accessori getName() e getID(). Le altre variabili d'istanza sono dichiarate public per non appesantire troppo il codice.




"Snapshot" di una computazione


Si osservi che:
  • Esiste una sola classe Student (con la variabile statica nextUniqueID), ma ci sono più oggetti di tipo Student (ognuno con le sue variabili di istanza).
  • Le variabili statiche possono essere considerate come globali (condivise da tutti gli oggetti della classe).
  • Un metodo statico (come getUniqueID) appartiene alla classe e quindi non è parte di nessun oggetto. In particolare non ha accesso alle variabili d'istanza (a quale oggetto apparterrebbero?).
  • Come abbiamo già visto, un metodo statico può essere invocato anche se non esiste alcun oggetto della classe. Per invocarlo si usa la sintassi:

    <nomeClasse>.<nomeMetodo>(<listaParametri>)





Un esempio di manipolazione di oggetti

La seguente sequenza di comandi crea la collezione di oggetti vista prima:

public class StudentTest { 
    public static void main(String[] args) {

        Student std  = new Student("John Smith");
        /* Dichiara la variabile std, e la inizializza
         * con un riferimento ad un nuovo oggetto della
         * classe Student, avente nome "John Smith"
         */

        Student std1 = new Student("Mary Jones");
        /* Dichiara std1, e la inizializza con un 
         * riferimento al nuovo studente "Mary Jones"
         */

        Student std2 = std1;
        /* Dichiara std2, e la inizializza con un 
         * riferimento ALLO STESSO OGGETTO riferito 
         * da std1
         */

        Student std3 = null;
        /* Dichiara std3, e lo inizializza a null
         */
    } 
}

Si noti ancora una volta che le operazioni su variabili di tipo oggetto sono operazioni su riferimenti, non sugli oggetti stessi. Quindi, ad esempio, l'effetto del comando:

    Student std2 = std1;
non è la creazione di una copia dell'oggetto, ma la sua condivisione.
 



Meglio membri statici o di istanza?

Quando si scrivono i metodi e le variabili di una classe, come si decide se devono essere statici o di istanza?

Come regola generale, devono essere statici se codificano le funzionalità o lo stato della classe, e quindi sono significativi anche se non esiste alcun oggetto della classe.

Invece devono essere di istanza se codificano le funzionalità o lo stato degli oggetti.

Vediamo alcuni esempi più specifici, che possono risultare utili nella pratica:

Il metodo main è sempre statico.  Si tratta chiaramente di una funzionalità della classe: viene invocato per iniziare l'esecuzione, prima della creazione di qualunque oggetto.
Una variabile deve essere di istanza se, concettualmente, il suo valore può essere diverso per oggetti diversi. 

Esempio: coordinate di un punto.

Se fossero dichiarate statiche, tutti i punti avrebbero le stesse coordinate.
Una variabile dovrebbe essere statica se il suo valore non cambia da un oggetto all'altro. 

Esempi: una costante, oppure il "numero di istanze della classe che sono state create".

Il compilatore non protesta se vengono dichiarate come variabili di istanza, ma il programma risulta concettualmente meno chiaro, e a volte errato (mantenere la consistenza del dato può essere difficile).
Se un metodo accede ad una variabile d'istanza, non può essere statico. 

Esempio: toString, equals,...

Il compilatore segnalerebbe un errore.
Se un metodo non accede ad alcuna variabile d'istanza, dovrebbe essere statico. In caso contrario il compilatore non segnala errori, ma il programma è concettualmente poco chiaro.

 



Modificatori di visibilità

Abbiamo visto spesso che una dichiarazione di variabile o metodo (sia statico che di istanza) può essere preceduta dai modificatori public oppure private (oppure da nessuno dei due). Cosa significano? Quali sono le differenze?
 
 
Modificatore
Accessibilità:
lettura e scrittura per variabili (<obj>.<variabile>)
invocazione per metodi (<obj>.<metodo>(<parametri>))
private
Accessibile solo all'interno della classe di <obj>
nessuno
Accessibile solo nel package che contiene la classe di <obj>
protected
Accessibile nel package che contiene la classe di <obj>, e in tutte le classi che ereditano da essa
public
Sempre, cioè ovunque la classe di <obj> sia accessibile

Nella tabella i modificatori sono elencati in ordine crescente di visibilità. Si noti che:

  • Per garantire l'incapsulamento delle informazioni, le variabili devono essere dichiarate private;
  • Il nome del modificatore protected è fuorviante, perché consente una visibilità persino maggiore dell'assenza di modificatore.




Ancora sull'incapsulamento...

Supponiamo di non aver rispettato il principio di incapsulamento nella definizione della classe Student, cioè di aver dichiarato le variabili name ed ID pubbliche:

public class Student { 

    // variabili d'istanza 
    public String name;  // nome 
    public int ID;       // matricola 

    ... // tutto come prima 
}

Con l'uso del costruttore già visto, le variabili name e ID verrebbero inizializzate al momento della creazione. Ma poiché le variabili d'istanza sono dichiarate public, è possibile una sequenza di comandi come
 

  Student std  = new Student("John Smith");
  Student std1 = new Student("Mary Jones");
  std1.name = "Paolo Rossi";
  std1.ID = std.ID;
  // adesso ci sono due studenti diversi
  // con la stessa matricola!

che porterebbe la computazione in uno stato concettualmente inconsistente. Invece avendole dichiarate private siamo sicuri che il valore iniziale delle due variabili non potrà essere modificato.