Utilizzo di super in Java

Concetti Chiave
  • La parola chiave super in Java consente di accedere ai membri della superclasse e di chiamare i suoi costruttori.
  • super() deve essere la prima istruzione in un costruttore di sottoclasse.
  • super può essere utilizzato per accedere a variabili e metodi della superclasse che sono stati oscurati nella sottoclasse.
  • È possibile passare un oggetto della sottoclasse a un costruttore della superclasse, consentendo l'accesso ai membri della superclasse.

Utilizzo di super

Nei precedenti esempi le classi derivate da Scatola non erano state implementate nel modo più efficiente e robusto possibile.

Per esempio, il costruttore di ScatolaPeso inizializzava esplicitamente i campi larghezza, altezza e profondità definiti in Scatola. In questo modo si duplicava codice già presente nella superclasse, con conseguente inefficienza, e si lasciava intendere che la sottoclasse dovesse poter accedere a tali membri.

Esistono però situazioni in cui vogliamo creare una superclasse che mantenga i dettagli della propria implementazione (ovvero i propri dati) privati. In tal caso la sottoclasse non avrebbe modo di accedere o inizializzare direttamente tali variabili.

Poiché l'incapsulamento è un attributo fondamentale della programmazione orientata agli oggetti, non sorprende che Java offra una soluzione. Ogni volta che una sottoclasse deve riferirsi alla propria superclasse immediata, può farlo mediante la parola chiave super.

super presenta due forme generali:

  1. Chiamare il costruttore della superclasse.
  2. Accedere a un membro della superclasse che è stato oscurato da un membro della sottoclasse.

Di seguito analizziamo entrambi gli utilizzi.

Utilizzo di super per chiamare i costruttori della superclasse

Una sottoclasse può invocare un costruttore definito nella superclasse tramite la forma:

super(elencoArgomenti);

dove elencoArgomenti specifica gli argomenti richiesti dal costruttore della superclasse. super() deve sempre essere la prima istruzione eseguita all'interno del costruttore della sottoclasse.

Per comprendere l'uso di super(), consideriamo questa versione migliorata di ScatolaPeso:

// ScatolaPeso ora usa super per inizializzare gli attributi di Scatola.
class ScatolaPeso extends Scatola {
    double peso; // peso della scatola

    // inizializza larghezza, altezza e profondità tramite super()
    ScatolaPeso(double w, double h, double d, double m) {
        super(w, h, d); // chiama il costruttore della superclasse
        peso = m;
    }
}

ScatolaPeso() invoca super() con gli argomenti w, h e d, attivando così il costruttore di Scatola, che inizializza larghezza, altezza e profondità. ScatolaPeso si limita a inizializzare il campo specifico peso, lasciando alla superclasse la gestione dei propri dati e rendendo possibile dichiararli private se necessario.

Nell'esempio precedente super() è stato chiamato con tre argomenti. Poiché i costruttori possono essere sovraccaricati, super() può essere invocato con qualunque forma definita nella superclasse: verrà eseguito il costruttore che corrisponde alla lista di argomenti.

Di seguito presentiamo un'implementazione completa di ScatolaPeso che fornisce costruttori per tutti i modi in cui è possibile creare una scatola; in ogni caso super() viene chiamato con gli argomenti appropriati. Si noti che larghezza, altezza e profondità sono stati resi private in Scatola.

// Implementazione completa di Scatola e ScatolaPeso.
class Scatola {
    private double larghezza;
    private double altezza;
    private double profondità;

    // costruttore copia
    Scatola(Scatola ob) { // passa l'oggetto al costruttore
        larghezza  = ob.larghezza;
        altezza    = ob.altezza;
        profondità = ob.profondità;
    }

    // costruttore con tutte le dimensioni specificate
    Scatola(double w, double h, double d) {
        larghezza  = w;
        altezza    = h;
        profondità = d;
    }

    // costruttore senza dimensioni specificate
    Scatola() {
        larghezza  = -1; // -1 indica non inizializzato
        altezza    = -1;
        profondità = -1;
    }

    // costruttore per un cubo
    Scatola(double lato) {
        larghezza = altezza = profondità = lato;
    }

    // calcola e restituisce il volume
    double volume() {
        return larghezza * altezza * profondità;
    }
}

// ScatolaPeso implementa tutti i costruttori.
class ScatolaPeso extends Scatola {
    double peso; // peso della scatola

    // costruttore copia
    ScatolaPeso(ScatolaPeso ob) { // passa l'oggetto al costruttore
        super(ob);
        peso = ob.peso;
    }

    // costruttore con tutti i parametri specificati
    ScatolaPeso(double w, double h, double d, double m) {
        super(w, h, d); // chiama il costruttore della superclasse
        peso = m;
    }

    // costruttore di default
    ScatolaPeso() {
        super();
        peso = -1;
    }

    // costruttore per un cubo
    ScatolaPeso(double lato, double m) {
        super(lato);
        peso = m;
    }
}

class DimostrazioneSuper {
    public static void main(String[] args) {
        ScatolaPeso miaScatola1 = new ScatolaPeso(10, 20, 15, 34.3);
        ScatolaPeso miaScatola2 = new ScatolaPeso(2, 3, 4, 0.076);
        ScatolaPeso miaScatola3 = new ScatolaPeso();           // costruttore di default
        ScatolaPeso mioCubo     = new ScatolaPeso(3, 2);
        ScatolaPeso miaCopia    = new ScatolaPeso(miaScatola1);
        double vol;

        vol = miaScatola1.volume();
        System.out.println("Il volume di miaScatola1 è " + vol);
        System.out.println("Il peso di miaScatola1 è " + miaScatola1.peso);
        System.out.println();

        vol = miaScatola2.volume();
        System.out.println("Il volume di miaScatola2 è " + vol);
        System.out.println("Il peso di miaScatola2 è " + miaScatola2.peso);
        System.out.println();

        vol = miaScatola3.volume();
        System.out.println("Il volume di miaScatola3 è " + vol);
        System.out.println("Il peso di miaScatola3 è " + miaScatola3.peso);
        System.out.println();

        vol = miaCopia.volume();
        System.out.println("Il volume di miaCopia è " + vol);
        System.out.println("Il peso di miaCopia è " + miaCopia.peso);

        vol = mioCubo.volume();
        System.out.println("Il volume di mioCubo è " + vol);
        System.out.println("Il peso di mioCubo è " + mioCubo.peso);
        System.out.println();
    }
}

Questo programma produce il seguente output:

Il volume di miaScatola1 è 3000.0
Il peso di miaScatola1 è 34.3

Il volume di miaScatola2 è 24.0
Il peso di miaScatola2 è 0.076

Il volume di miaScatola3 è -1.0
Il peso di miaScatola3 è -1.0

Il volume di miaCopia è 3000.0
Il peso di miaCopia è 34.3

Il volume di mioCubo è 27.0
Il peso di mioCubo è 2.0

Si presti particolare attenzione a questo costruttore in ScatolaPeso:

// crea un clone di un oggetto
ScatolaPeso(ScatolaPeso ob) { // passa l'oggetto al costruttore
    super(ob);
    peso = ob.peso;
}

Si osservi che a super() viene passato un oggetto di tipo ScatolaPeso e non di tipo Scatola. Ciò invoca comunque il costruttore Scatola(Scatola ob). Come detto in precedenza, una variabile di superclasse può fare riferimento a qualunque oggetto derivato da quella classe; è dunque possibile passare a Scatola un oggetto di tipo ScatolaPeso. Naturalmente Scatola conosce soltanto i propri membri.

Rivediamo i concetti chiave relativi a super(). Quando una sottoclasse chiama super(), viene invocato il costruttore della sua superclasse immediata. Ne consegue che super() fa sempre riferimento alla superclasse immediatamente sopra la classe chiamante, anche in gerarchie a più livelli. Inoltre, super() deve sempre essere la prima istruzione eseguita all'interno di un costruttore di sottoclasse.

Un secondo utilizzo di super

La seconda forma di super funziona in modo analogo, con la differenza che fa sempre riferimento alla superclasse della sottoclasse in cui viene usata. La sintassi generale è:

super.membro

dove membro può essere un metodo o una variabile d'istanza.

Questa forma di super risulta utile quando nella sottoclasse i nomi dei membri nascondono quelli omonimi definiti nella superclasse. Consideriamo la seguente gerarchia di classi:

// Uso di super per superare l'occultamento di nomi.
class A {
    int i;
}

// Creiamo una sottoclasse estendendo la classe A.
class B extends A {
    int i; // questo i occulta l'i in A

    B(int a, int b) {
        super.i = a; // i in A
        i = b;       // i in B
    }

    void mostra() {
        System.out.println("i nella superclasse: " + super.i);
        System.out.println("i nella sottoclasse: " + i);
    }
}

class UsaSuper {
    public static void main(String[] args) {
        B sottoOg = new B(1, 2);

        sottoOg.mostra();
    }
}

Questo programma visualizza:

i nella superclasse: 1
i nella sottoclasse: 2

Sebbene la variabile d'istanza i in B occulti quella in A, super consente l'accesso all'i definita nella superclasse. Come si vedrà, super può essere usato anche per chiamare metodi che risultano nascosti da una sottoclasse.