Campi e Metodi Statici in Java

In questa lezione si esplora l’uso del modificatore static in Java per condividere dati e comportamenti a livello di classe.

Verranno illustrati i campi statici, che garantiscono una sola istanza per l’intera classe, e i metodi statici, che operano senza un oggetto specifico. Attraverso esempi pratici, come le costanti e i metodi factory di Math e NumberFormat, si evidenziano i casi d’uso e le best practice per sfruttare al meglio il modificatore static.

Campi Statici

Se un campo viene dichiarato static, esiste un’unica istanza di quel campo per l’intera classe. Al contrario, ogni oggetto possiede la propria copia di campi d’istanza non statici. Per esempio, si desidera assegnare a ogni dipendente un numero di identificazione univoco. Si aggiunge un campo d’istanza id e un campo statico prossimoId alla classe Dipendente:

class Dipendente
{
    private static int prossimoId = 1;

    private int id;
    ...
}

Ogni oggetto Dipendente possiede ora il proprio campo id, mentre esiste un solo campo prossimoId condiviso tra tutte le istanze della classe.

In altri termini, se sono presenti 1 000 oggetti della classe Dipendente, esistono 1000 campi d’istanza id, uno per ciascun oggetto. Tuttavia, esiste un unico campo statico prossimoId. Anche in assenza di oggetti Dipendente, il campo statico prossimoId è comunque presente: appartiene alla classe, non ai singoli oggetti.

Consiglio

Campi statici

In alcuni linguaggi di programmazione orientati agli oggetti, i campi statici sono detti campi di classe. Il termine “static” è un termine mutuato dal C++.

Implementiamo ora un semplice metodo:

public void impostaId()
{
    id = prossimoId;
    prossimoId++;
}

Si ipotizzi di impostare il numero di identificazione per mario, un oggetto di tipo Dipendente:

mario.impostaId();

Il campo id di mario viene quindi impostato al valore corrente del campo statico prossimoId, che viene successivamente incrementato:

mario.id = Dipendente.prossimoId;
Dipendente.prossimoId++;

Costanti Statiche

Le variabili statiche sono piuttosto rare; al contrario, le costanti statiche sono più comuni. Ad esempio, la classe Math, che fa parte della libreria standard di Java, definisce una costante statica:

public class Math
{
    ...
    public static final double PI = 3.14159265358979323846;
    ...
}

Questa costante può essere utilizzata nei programmi come Math.PI.

Se il modificatore static fosse stato omesso, PI sarebbe divenuto un campo d’istanza della classe Math. In tal caso, sarebbe necessario un oggetto di tale classe per accedere a PI e ogni oggetto Math possederebbe la propria copia di PI.

Un’altra costante statica già ampiamente utilizzata è System.out. Essa è dichiarata nella classe System come segue:

public class System
{
    ...
    public static final PrintStream out = ...;
    ...
}

Come ribadito più volte, pubblicare campi accessibili è sconsigliato perché chiunque può modificarli. Le costanti pubbliche (ossia i campi final) sono invece accettabili. Poiché out è dichiarato final, non è possibile assegnargli un altro flusso di stampa:

System.out = new PrintStream(...); // ERRORE — out è final
Consiglio

Eccezione di System.setOut

Un esame della classe System rivela il metodo setOut, che assegna a System.out un flusso differente. Ci si potrebbe chiedere come tale metodo possa modificare il valore di una variabile final. Il metodo setOut è però un metodo native, non implementato nel linguaggio Java. I metodi native possono aggirare i meccanismi di controllo dell’accesso di Java. Si tratta di una soluzione atipica da non emulare nei programmi.

Metodi Statici

I metodi statici non operano su oggetti. Per esempio, il metodo pow della classe Math è statico. L’espressione

Math.pow(x, a)

calcola la potenza x^{a}. Non utilizza alcun oggetto Math per svolgere il compito: in altre parole, non ha un parametro implicito.

I metodi statici possono essere considerati metodi privi del parametro this. (Nei metodi non statici, this fa riferimento al parametro implicito del metodo).

Un metodo statico di Dipendente non può accedere al campo d’istanza id poiché non opera su un oggetto, ma può accedere a un campo statico. Ecco un esempio di metodo statico:

public static int getProssimoId()
{
    return prossimoId; // restituisce il campo statico
}

Per invocare tale metodo si specifica il nome della classe:

int n = Dipendente.getProssimoId();

Omettere il modificatore static sarebbe possibile, ma in tal caso occorrerebbe un riferimento a un oggetto di tipo Dipendente per invocare il metodo.

Consiglio

Oggetti e metodi statici

È lecito usare un oggetto per chiamare un metodo statico. Ad esempio, se mario è un oggetto Dipendente, si può invocare mario.getProssimoId() invece di Dipendente.getProssimoId(). Tuttavia, tale notazione può risultare fuorviante: getProssimoId non valuta affatto mario per calcolare il risultato. È consigliabile usare i nomi delle classi, non degli oggetti, per chiamare metodi statici.

Usare i metodi statici in due casi:

  • Quando un metodo non necessita di accedere allo stato dell’oggetto poiché tutti i parametri richiesti sono forniti esplicitamente (esempio: Math.pow).
  • Quando un metodo deve soltanto accedere a campi statici della classe (esempio: Dipendente.getProssimoId).
Consiglio

static in C++

In C++ i campi e i metodi statici funzionano in modo analogo a Java, sebbene la sintassi differisca leggermente. In C++ si utilizza l’operatore :: per accedere a un campo o a un metodo statico al di fuori del suo ambito, ad esempio Math::PI.

Il termine “static” ha una storia singolare. Inizialmente, nel C fu introdotto per indicare variabili locali che non scompaiono al termine del blocco; in quel contesto, il termine ha senso: la variabile persiste ed è presente quando il blocco viene rientrato. Successivamente static acquisì un secondo significato in C, denotando variabili globali e funzioni non accessibili da altri file. Infine, il C++ riutilizzò la parola chiave per un terzo significato, non correlato ai precedenti: variabili e funzioni appartenenti a una classe, ma non a un suo oggetto specifico. È il medesimo significato che static possiede in Java.

Metodi Factory

Un altro uso comune dei metodi statici riguarda le classi come LocalDate e NumberFormat, che impiegano metodi factory statici per costruire oggetti. I metodi factory LocalDate.now e LocalDate.of sono già stati presentati. Di seguito è illustrato come la classe NumberFormat produce oggetti formattatori per stili differenti:

NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter  = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // stampa $0.10
System.out.println(percentFormatter.format(x));  // stampa 10%

Perché la classe NumberFormat non utilizza invece un costruttore? Vi sono due motivi:

  • Non è possibile assegnare nomi ai costruttori. Il nome del costruttore coincide sempre con quello della classe, mentre servono due nomi diversi per ottenere l’istanza valuta e l’istanza percentuale.
  • Usando un costruttore non si può variare il tipo dell’oggetto costruito. I metodi factory restituiscono invece oggetti della classe DecimalFormat, sottoclasse che eredita da NumberFormat (nelle prossime lezioni vedremo i dettagli sull'ereditarietà).

Il metodo main

È possibile invocare metodi statici senza disporre di alcun oggetto. Ad esempio, non si crea mai un oggetto della classe Math per chiamare Math.pow. Per lo stesso motivo, il metodo main è un metodo statico.

public class Applicazione
{
    public static void main(String[] args)
    {
        // costruire qui gli oggetti necessari
        ...
    }
}

Il metodo main non opera su oggetti. All’avvio di un programma, infatti, non esistono ancora oggetti; il metodo statico main viene eseguito e crea gli oggetti richiesti dal programma.

Consiglio

Unit test con main

Ogni classe può disporre di un metodo main, espediente utile per lo unit test delle classi. Ad esempio, è possibile aggiungere un metodo main alla classe Dipendente:

class Dipendente
{
    public Dipendente(String n, double s, int anno, int mese, int giorno)
    {
        nome          = n;
        salario       = s;
        dataAssunzione = LocalDate.di(anno, mese, giorno);
    }
    ...
    public static void main(String[] args) // test unitario
    {
        var e = new Dipendente("Romeo", 50000, 2003, 3, 31);
        e.raiseSalary(10);
        System.out.println(e.getName() + " " + e.getSalary());
    }
    ...
}

Per collaudare la classe Dipendente in isolamento è sufficiente eseguire

java Dipendente

Se Dipendente fa parte di un’applicazione più ampia, si avvia l’applicazione con

java Applicazione

e il metodo main della classe Dipendente non viene eseguito.

Il programma riportato nel codice più in basso fornisce una versione semplificata della classe Dipendente con un campo statico prossimoId e un metodo statico getProssimoId. L’array viene riempito con tre oggetti Dipendente, ai quali si assegnano identificativi progressivi; infine si stampa il prossimo identificativo disponibile per mostrare il funzionamento del metodo statico. Anche la classe Dipendente contiene un metodo main statico per il collaudo unitario. Provare a eseguire entrambi i comandi

java Dipendente
java TestStatico

per verificare i due metodi main.

/**
 * Questo programma dimostra i metodi statici.
 * @version 1.0
 * @author Distortionbyte
 */
public class TestStatico
{
    public static void main(String[] args)
    {
        // riempie l’array staff con tre oggetti Dipendente
        var staff = new Dipendente[3];

        staff[0] = new Dipendente("Mario",   40000);
        staff[1] = new Dipendente("Giovanna",  60000);
        staff[2] = new Dipendente("Serena", 65000);

        // stampa le informazioni su tutti gli oggetti Dipendente
        for (Dipendente e : staff)
        {
            e.impostaId();
            System.out.println("name=" + e.getName()
                                + ",id=" + e.getId()
                                + ",salary=" + e.getSalary());
        }

        int n = Dipendente.getProssimoId(); // chiama il metodo statico
        System.out.println("Next available id=" + n);
    }
}

class Dipendente
{
    private static int prossimoId = 1;

    private String nome;
    private double salario;
    private int id;

    public Dipendente(String n, double s)
    {
        nome    = n;
        salario = s;
        id      = 0;
    }

    public String getName()
    {
        return nome;
    }

    public double getSalary()
    {
        return salario;
    }

    public int getId()
    {
        return id;
    }

    public void impostaId()
    {
        id = prossimoId; // assegna id al prossimo disponibile
        prossimoId++;
    }

    public static int getProssimoId()
    {
        return prossimoId; // restituisce il campo statico
    }

    public static void main(String[] args) // test unitario
    {
        var e = new Dipendente("mario", 50000);
        System.out.println(e.getName() + " " + e.getSalary());
    }
}