Java – La fase di inizializzazione degli oggetti

In Java c’è un aspetto della programmazione ad oggetti che è veramente fondamentale e importante da comprendere bene: la fase di inizializzazione degli oggetti, cioè tutto quello che avviene quando si crea una “istanza” di una classe partendo dalla invocazione new NomeClasse(). In questo articolo cercherò di descrivere in maniera accurata le regole e i concetti sulla inizializzazione degli oggetti.

Innanzitutto, quando si crea una istanza di una certa classe, non è solo coinvolta quella specifica classe che si sta istanziando ma anche tutte le sue superclassi, compresa la ben nota classe java.lang.Object che in Java è la classe “padre” di tutti gli oggetti. Quindi per avere una visione chiara della fase di inizializzazione degli oggetti, è bene vedere fin da subito uno scenario in cui viene applicata la ereditarietà definendo una classe che estende un’altra classe.

Partiamo quindi da due semplici classi, una classe A e una sottoclasse B, molto generiche e senza un significato particolare.

class A {
    private String info = "Classe A";

    public A() {
        System.out.println(info + ", costruttore");
    }
}

class B extends A {
    private String info = "Classe B";
    private double num = Math.random();

    {
        System.out.println(info + ", primo init block");
    }

    public B() {
        System.out.println(info + ", costruttore");
    }

    {
        System.out.println(info + ", secondo init block");
    }
}

A seconda delle conoscenze che avete su Java, potreste trovare qualcosa di “strano” nel codice, in particolare quei blocchi { } all’interno della classe B. Verrà tutto debitamente spiegato in dettaglio nel corso dell’articolo. La prima questione essenziale invece è cosa succede quando si invoca new B().

Quando si esegue un new B(), viene innanzitutto invocato quel costruttore public B(). Le regole di Java impongono che la primissima istruzione contenuta in un costruttore sia sempre obbligatoriamente una delle due seguenti istruzioni:

  • una istruzione super(); (con o senza argomenti) per invocare un costruttore della superclasse
    oppure
  • una istruzione this(); (con o senza argomenti) per invocare un altro costruttore di quella stessa classe

Una invocazione con this() in sostanza permette di “passare la palla” temporaneamente ad un altro costruttore presente nella stessa classe. Questa tecnica è molto utile quando si vuole riutilizzare la logica di inizializzazione di un altro costruttore passando degli argomenti in più (o in meno).

In Java tutti i costruttori di una classe sono concettualmente allo stesso livello, nel senso che non c’è un costruttore che è più privilegiato o ha caratteristiche più particolari rispetto agli altri. In diversi altri linguaggi di programmazione come ad esempio Kotlin e Scala esiste invece una netta distinzione tra il costruttore “primario” (che è uno solo ed ha una sintassi speciale) e altri costruttori “secondari” (o detti “ausiliari”). Nel linguaggio Java non è così, tutti i costruttori hanno la stessa sintassi e le stesse possibilità. Per ogni costruttore si può decidere a priori se invocare un costruttore della superclasse (con la istruzione super(); ) oppure in alternativa un costruttore della stessa classe (con la istruzione this(); ).

Secondo le regole di Java, il flusso di inizializzazione di un oggetto deve in ogni caso arrivare sempre fino alla classe “padre” java.lang.Object. Questo significa che se un costruttore usa this() per invocare un altro costruttore di quella stessa classe, ad un certo punto ci dovrà comunque poi essere un costruttore che esegue un super() in modo da chiamare un costruttore della superclasse. E tutto questo deve continuare fino ad arrivare al costruttore di Object.

Sospendiamo solo per un momento il discorso generale dell’articolo per vedere un caso particolare a cui prestare attenzione. Cosa succederebbe se in una classe ci fossero due costruttori che tentano di “passarsi la palla” tra di loro in maniera ciclica? Per intenderci, una cosa come la seguente:

class Circolare {
    public Circolare(int n) {
        this("A" + n);
    }

    public Circolare(String s) {
        this(s.length());
    }
}

In teoria, se si invocasse new Circolare(100), verrebbe invocato il primo costruttore con 100, poi il secondo con "A100", poi di nuovo il primo con 4, poi il secondo con "A4", ecc...
In pratica, questo non avviene, perché fortunatamente il compilatore Java è sufficientemente smart da riconoscere questo tentativo di invocazione ricorsiva tra costruttori. Se si prova a compilare questa classe si ottiene (output fornito dal Oracle JDK 8):

>javac Circolare.java
Circolare.java:6: error: recursive constructor invocation
        public Circolare(String s) {
               ^
1 error

Il codice in sostanza ... non compila!!

Esiste un’altra regola da tenere sempre ben presente: se il programmatore non inserisce esplicitamente come prima istruzione di un costruttore una invocazione super() oppure this() (con/senza argomenti), il compilatore inserisce automaticamente una invocazione super() senza argomenti. Questa logica serve per garantire che ci sia comunque la chiamata ad un costruttore della superclasse.

A dire il vero, esiste anche un’altra regola fondamentale che riguarda in generale i costruttori e che è bene riportare: se il programmatore non definisce alcun costruttore in una classe, il compilatore ne crea automaticamente uno (il costruttore di “default”) che è senza parametri e contiene solamente una istruzione super() senza argomenti.

Dal momento che il costruttore della classe B di esempio non ha come prima istruzione una invocazione esplicita di super() o this(), il compilatore “traduce” il costruttore di B nel seguente modo:

    public B() {
        super();    // inserito automaticamente dal compilatore
        System.out.println(info + ", costruttore");
    }

Il costruttore di B quindi invoca innanzitutto il costruttore di A. La invocazione super() non passa argomenti e la classe A dispone di un costruttore senza parametri, quindi la chiamata risulta coerente e corretta. All’interno del costruttore di A il compilatore applica la stessa identica logica, cioè la prima istruzione del costruttore deve essere una invocazione super() oppure this() e siccome non c’è esplicitamente una di queste invocazioni, il compilatore traduce il costruttore di A nel seguente modo:

    public A() {
        super();    // inserito automaticamente dal compilatore
        System.out.println(info + ", costruttore");
    }

La classe A estende (implicitamente) java.lang.Object, quindi il costruttore di A come prima cosa invoca il costruttore di Object. È bene chiarire che il costruttore di Object non fa nulla di utile, se si vuole verificare ciò è sufficiente andare a guardare il sorgente della classe Object, che si può rintracciare in diversi modi:

Indipendentemente da come si raggiunge il sorgente di Object, risulterà ben evidente che l’unico suo costruttore è semplicemente un costruttore “vuoto”:

public Object() {}

Questa prima fase della inizializzazione di un oggetto è quindi molto semplice, si parte da un costruttore della classe istanziata e si procede attraverso tutti i costruttori delle superclassi fino al costruttore della classe Object. A questo punto inizia la seconda fase, quella che si potrebbe informalmente definire “discendente”, nel senso che il costruttore di Object termina immediatamente (come detto, non fa nulla di realmente utile) e si deve quindi tornare indietro da tutte le varie invocazioni di super() e da tutti i costruttori.

Questa seconda fase è un po’ più complessa perché ci sono ancora svariate cose da eseguire. Ciascun costruttore può infatti avere, naturalmente, del codice dopo la prima istruzione super()/this() (nel codice di esempio c’è un System.out.println) e inoltre ci sono due cose importanti che devono essere eseguite (se sono presenti, chiaramente):

  • gli inizializzatori delle variabili “di istanza”
  • i blocchi di inizializzazione “di istanza” (in inglese: instance initialization blocks o più brevemente instance init blocks)

Ogni variabile di istanza può avere una inizializzazione esplicita, nella classe B di esempio le due variabili di istanza hanno una inizializzazione che è quella evidenziata qui sotto in giallo:

    private String info = "Classe B";
    private double num = Math.random();

Questa inizializzazione, trattandosi di variabili “di istanza”, avviene ogni volta che si crea un oggetto della classe B. Esiste però un altro costrutto che in generale viene anche eseguito ogni volta che viene istanziato un oggetto: il “blocco di inizializzazione di istanza” (instance initialization block).

I blocchi di inizializzazione “di istanza” sono dei semplici blocchi di codice racchiusi tra le parentesi graffe { } e non possiedono alcun nome. Questo tipo di costrutto è stato introdotto in Java 1.1 principalmente come supporto alle classi anonime (anonymous inner class), che per specifica del linguaggio non possono avere un costruttore esplicito. La introduzione dei blocchi di inizializzazione di istanza permette quindi di inserire del codice di inizializzazione nelle classi anonime, sopperendo così (ma solo in parte) alla mancanza di un costruttore esplicito.

Questo tipo di blocco si può usare non solo nelle classi anonime ma in generale anche in qualunque altro tipo di classe. E attenzione, in una classe ci possono essere più blocchi di inizializzazione, come è evidente nella classe B di esempio. I blocchi di inizializzazione devono trovarsi fuori da costruttori/metodi ma possono essere messi in qualunque punto della classe, prima e/o dopo costruttori/metodi. Chiaramente, se un blocco di inizializzazione deve fare riferimento ad una variabile di istanza, è bene che quest’ultima sia definita prima del blocco di inizializzazione.

Se una normale classe ha più costruttori, i blocchi di inizializzazione di istanza vengono sempre eseguiti indipendentemente da quale è il costruttore invocato. La principale restrizione di questi blocchi è il fatto che non possono accedere ai parametri dei costruttori, dal momento che non possono “sapere” quale costruttore è stato invocato.

La esecuzione degli inizializzatori delle variabili di istanza e dei blocchi di inizializzazione di istanza avviene secondo le seguenti regole:

  • vengono eseguiti in ordine rigorosamente “testuale”, esattamente nell’ordine in cui gli inizializzatori delle variabili e i blocchi di inizializzazione si susseguono nel sorgente andando dall’alto verso il basso
  • vengono eseguiti appena dopo che il “super” costruttore è stato eseguito ma prima di eseguire il resto del codice in un costruttore

Prendendo come esempio di riferimento il costruttore della classe B, la sua logica complessiva risulta quindi la seguente:

    public B() {
        super();    // inserito dal compilatore
            ← qui vengono eseguiti gli inizializzatori delle variabili e i blocchi di inizializzazione
        System.out.println(info + ", costruttore");
    }

È il compilatore che si preoccupa di generare il bytecode per il costruttore di B in modo che esso contenga le operazioni come sono state riportate logicamente nel precedente codice. Se si volesse averne evidenza, sarebbe sufficiente osservare il bytecode generato utilizzando il tool javap del JDK.

Alla luce di tutta la spiegazione fatta e delle regole esposte, riporto quindi di seguito il codice completo delle due classi A e B di esempio indicando con i numeri da 1 a 9 l’ordine di esecuzione delle operazioni quando si invoca un new B() .

class A {
    private String info = "Classe A";   3

    public A() {
        super();    // inserito dal compilatore   2
        System.out.println(info + ", costruttore");   4
    }
}

class B extends A {
    private String info = "Classe B";   5
    private double num = Math.random();   6

    {
        System.out.println(info + ", primo init block");   7
    }

    public B() {
        super();    // inserito dal compilatore   1
        System.out.println(info + ", costruttore");   9
    }

    {
        System.out.println(info + ", secondo init block");   8
    }
}

Conclusioni

Tutta la logica di inizializzazione degli oggetti è stata definita dal linguaggio Java nel modo descritto sostanzialmente per dare l’opportunità a tutte le classi lungo la linea di ereditarietà di inizializzare il proprio stato. In particolare, come si è visto prima, una superclasse viene sempre inizializzata prima della sottoclasse.

Per comprendere appieno la inizializzazione degli oggetti è necessario ricordare sempre le regole che sono riassunte nei seguenti punti:

  1. La prima istruzione di un costruttore deve essere sempre super(); oppure this(); (con o senza argomenti) e se il programmatore non inserisce esplicitamente una di queste istruzioni, il compilatore inserisce in automatico una istruzione super(); senza argomenti
  2. Se il programmatore non definisce esplicitamente alcun costruttore in una classe, il compilatore inserisce automaticamente nella classe un costruttore che è senza parametri e che contiene una istruzione super(); senza argomenti
  3. Se ci sono degli inizializzatori delle variabili “di istanza” e/o dei blocchi di inizializzazione “di istanza”, essi vengono eseguiti in ordine “testuale” poco dopo la terminazione del supercostruttore ma prima di eseguire il resto del costruttore