Ancora su inizializzazione di variabili locali

Come abbiamo visto, ci sono due tipi di variabili in Java, che sono trattate in modo diverso:
  • variabili dichiarate localmente nei metodi (o costruttori): variabili locali e parametri formali
  • variabili dichiarate in una classe (all'esterno dei metodi), cioè le variabili statiche e le variabili di istanza
  • .


Variabili locali dei metodi
  • vengono allocate nel record di attivazione relativo alla chiamata del metodo (sullo stack);

  • devono essere inizializzate esplicitamente, altrimenti il compilatore segnala un errore.
Programma non corretto: num non inizializzata
public class Tabellina { 
    public static void main(String[] args) { 
        int num;  // NON equivale a int num = 0 
        while (num<=0) { 
            System.out.print("Dammi un numero >0: "); 
            num = Input.readInt(); 
        } 
        for (int i=1; i<=10; i++) 
            System.out.println(i * num);
    }
}
Errore segnalato dal compilatore:
> javac Tabellina.java
 
Tabellina.java:4: variable num might not have been initialized
        while (num<=0) {
               ^
1 error



Inizializzazione di variabili statiche e di istanza

Le variabili statiche e di istanza
  • vengono allocate in una zona di memoria chiamato heap;

  • vengono sempre inizializzate, anche se non viene fatto esplicitamente.

I valori default sono:

  • 0 per variabili numeriche,
  • false per variabili di tipo boolean,
  • null per variabili di oggetto (inclusi array, stringhe,...).


Come abbiamo visto, ci sono tre modi per inizializzare una variabile d'istanza o statica:

  • Con assegnamento esplicito all'interno di un costruttore;
  • Con inizializzazione esplicita nella dichiarazione;
  • Inizializzazione con valori default.
Classe che impiega tutti e tre i modi di inizializzare
Inizializzazione
public class Punto3d { 

    public double x;
    public double y = 3.0;
    public double z; 

    public Punto3d(double val) { 
        z = val; 
    }
}
Con il comando

...
Punto3d p = new Punto3d(5.0);
...

otteniamo:

p.x == 0.0;
p.y == 3.0;
p.z == 5.0.




La primitiva new

Il processo di creazione di un oggetto consiste in:
  • allocare lo spazio di memoria necessario a contenere l'oggetto,
  • inizializzare le variabili di istanza dell'oggetto.
La quantità di memoria necessaria per contenere un oggetto è determinata univocamente dalla dichiarazione della classe (più precisamente, dal numero e dal tipo delle variabili di istanza). Naturalmente lo stesso vale anche per gli array.

Lo spazio necessario viene allocato nello heap.

Come sappiamo, un oggetto viene creato con la primitiva new, la cui sintassi è:

new <nomeClasse> (<parametri>);

La primitiva new fa concettualmente tre cose:

  • crea l'oggetto allocando la memoria necessaria;
  • invoca sull'oggetto il costruttore della classe <nomeClasse>, passandogli i <parametri> (la selezione del costruttore avviene in base alla firma, come una qualsiasi invocazione di metodo);
  • restituisce un puntatore all'oggetto creato.

Ad esempio:

  BankAccount account = new BankAccount(5412.5);
  // crea un nuovo conto con saldo iniziale 5412.5
  // e ne memorizza il riferimento nella variabile account

Vediamo più in dettaglio cosa sono i metodi costruttori.




Costruttori

I costruttori si dichiarano all'interno di una classe essenzialmente come i metodi, ma rispettando le seguenti regole:
  1. il nome del costruttore deve coincidere con quello della classe;
  2. il tipo del costruttore non deve essere specificato. (Attenzione, questo non significa che il tipo del costruttore è void: concettualmente, il costruttore restituisce sempre l'oggetto appena creato.)
  3. il modificatore static non può essere usato per un costruttore.

Come per i metodi, anche ai costruttori si applica l'overloading: una classe può avere più costruttori purché abbiano firma diversa (cioè i parametri formali devono differire nel tipo e/o nel numero).

Esempio: costruttori della classe BankAccount
  public BankAccount(double initialBalance) { ... }

  public BankAccount() { ... }


Ma cosa succede se...

  • Nella dichiarazione del costruttore non rispetto le regole (2) o (3)?
Viene considerata come una normale dichiarazione di un metodo avente come nome proprio il nome della classe. Quindi si applicano tutte le regole viste.

Esempi di "falsi costruttori"
public class FakeBankAccount {  
    
    private double balance;  
    
    public void FakeBankAccount() {  
        balance = 0; 
        // compila, ma e' un metodo, non un costruttore
   } 
    
    public static FakeBankAccount(double initialBalance) { 
        balance = initialBalance;
        // segnala sempre errore
    } 

    public static void FakeBankAccount(double initialBalance) { 
        balance = initialBalance;
        // segnala errore perche' accede a variabile d'istanza
    } 

    public FakeBankAccount FakeBankAccount(double initialBalance) { 
        balance = initialBalance;
        return this;
        // compila correttamente, ma a che serve?
    } 
    ....
}



Il costruttore default

Ogni classe ha un costruttore default che inizializza le variabili d'istanza con il corrispondente valore default:
  • 0 per variabili numeriche,
  • false per variabili booleane,
  • null per variabili di oggetto.

Questo costruttore non ha parametri, ed è disponibile solo se nella classe non è definito nessun costruttore.

Se invece nella classe è definito almeno un costruttore, allora il costruttore default non è più utilizzabile: l'operatore new  chiama sull'oggetto appena creato il costruttore determinato dalla lista dei parametri attuali (se non esiste, il compilatore segnala un errore del tipo "cannot resolve symbol").

Uso corretto del costruttore default
public class Punto { 

    public double x, y;

    ... // altri metodi ma 
    ... // nessun costruttore

}
Il comando

...
Punto p = new Punto();
...

è corretto. Avremo:

p.x == 0.0;
p.y == 0.0;

Uso errato del costruttore default
public class Punto { 

    public double x, y;

    public Punto(double vx, double vy) {
        x = vx;
        y = vy; 
    }

    ... // altri metodi 

}
Il comando

...
Punto p = new Punto();
...

è ERRATO! Il costruttore default
non è più disponibile.

Invece il comando

...
Punto p = new Punto(4.3, 3.7);
...

è OK.

In questo caso, se il programmatore vuole continuare a rendere disponibile anche un costruttore senza parametri, deve dichiararlo esplicitamente (come abbiamo fatto nella classe BankAccount).




this...

In ogni corpo di un metodo o di un costruttore, è sempre disponibile la variabile predefinita this che denota il parametro implicito, cioè l'oggetto che sta eseguendo il metodo (o costruttore). In altre parole, this è un parametro implicito di ogni metodo e riferisce l'oggetto sul quale il metodo è stato invocato.

In generale si evita di prefiggere this ai nomi delle variabili e metodi di istanza, perché essi possono essere riferiti direttamente con il loro nome senza ambiguità.

Senza this
Con this (sintassi più pesante)
public class Punto { 

    public double x, y;

    public String toString() {
        return "(" + x + "," + y + ")"; 
    }
 
    public void print() { 
        System.out.println(toString()); 
    } 

    ... // altri metodi 

}
public class Punto { 

    public double x, y;

    public String toString() {
        return "(" + this.x + "," + this.y + ")"; 
    }
 
    public void print() { 
        System.out.println(this.toString()); 
    } 

    ... // altri metodi 

}

Ma in alcuni casi può essere utile. Ad esempio è indispensabile usare this quando si usano gli stessi nomi delle variabili d'istanza come nomi dei parametri di un costruttore :
 

Senza this
Con this (costruttore più chiaro)
public class Punto { 

    public double x, y;

    public Punto(double vx, double vy) {
        x = vx;
        y = vy; 
    }

    ... // altri metodi

}
public class Punto { 

    public double x, y;

    public Punto(double x, double y) {
        this.x = x;
        this.y = y; 
    }

    ... // altri metodi

}



... e this()

All'interno di un costruttore esiste anche un altro uso dell'identificatore this: viene utilizzato non come variabile ma come invocazione di un altro costruttore della classe.

Attenzione: questo uso è consentito solo come prima istruzione di un altro costruttore.

Nonostante queste restrizioni, l'uso di this come costruttore è molto diffuso in pratica perché consente di scrivere codice non ridondante, riutilizzando le inizializzazioni di costruttori sovraccaricati.

Con this ma senza this()
Con this e this()
public class Punto { 

    public double x, y;

    public Punto() { 
        this.x = 0; 
        this.y = 0; 
        System.out.print("ho costruito ");
        System.out.print("il punto ");
        print(); 
    } 

    public Punto(double x,double y) { 
        this.x = x; 
        this.y = y;
        System.out.print("ho costruito ");
        System.out.print("il punto ");
        print(); 
    } 

    ... // altri metodi

}
public class Punto { 

    public double x, y;

    public Punto() { 
        this(0,0);
    } 

    public Punto(double x,double y) { 
        this.x = x; 
        this.y = y;
        System.out.print("ho costruito ");
        System.out.print("il punto ");
        print(); 
    } 

    ... // altri metodi

}