Introduzione all'Ereditarietà in Java
- L'ereditarietà in Java consente di creare gerarchie di classi, promuovendo la riusabilità del codice e la specializzazione delle classi.
- Una superclasse definisce tratti comuni, mentre una sottoclasse estende la superclasse aggiungendo attributi unici.
- Una variabile di riferimento di una superclasse può fare riferimento a un oggetto di sottoclasse, ma l'accesso ai membri è limitato al tipo della variabile di riferimento.
- I membri dichiarati come
private
in una superclasse non sono accessibili dalle sottoclassi. - L'ereditarietà è un concetto fondamentale della programmazione orientata agli oggetti, che consente la creazione di classificazioni gerarchiche e la specializzazione delle classi.
L'ereditarietà è uno dei pilastri della programmazione orientata agli oggetti perché consente la creazione di classificazioni gerarchiche.
Usando l'ereditarietà, è possibile creare una classe generale che definisce tratti comuni a un insieme di elementi correlati. Questa classe può quindi essere ereditata da altre classi più specifiche, ognuna delle quali aggiunge ciò che le è unico.
Nella terminologia di Java, una classe che viene ereditata è chiamata superclasse. La classe che effettua l'ereditarietà è chiamata sottoclasse. Pertanto, una sottoclasse è una versione specializzata di una superclasse. Essa eredita tutti i membri definiti dalla superclasse e aggiunge i propri elementi unici.
Fondamenti dell'ereditarietà
Per ereditare una classe, è sufficiente incorporare la definizione di una classe in un'altra usando la parola chiave extends
.
Per comprenderne il funzionamento, prendiamo in esame un breve esempio. Il programma seguente crea una superclasse chiamata A
e una sottoclasse chiamata B
. Si noti come la parola chiave extends
venga utilizzata per creare una sottoclasse di A
.
// Un semplice esempio di ereditarietà.
// Creare una superclasse.
class A {
int i, j;
void mostraIJ() {
System.out.println("i e j: " + i + " " + j);
}
}
// Creare una sottoclasse estendendo la classe A.
class B extends A {
int k;
void mostraK() {
System.out.println("k: " + k);
}
void somma() {
System.out.println("i+j+k: " + (i + j + k));
}
}
class EreditarietaSemplice {
public static void main(String[] args) {
A superOg = new A();
B subOg = new B();
// La superclasse può essere utilizzata da sola.
superOg.i = 10;
superOg.j = 20;
System.out.println("Contenuto di superOg: ");
superOg.mostraIJ();
System.out.println();
/* La sottoclasse ha accesso a tutti i membri pubblici
della sua superclasse. */
subOg.i = 7;
subOg.j = 8;
subOg.k = 9;
System.out.println("Contenuto di subOg: ");
subOg.mostraIJ();
subOg.mostraK();
System.out.println();
System.out.println("Somma di i, j e k in subOg:");
subOg.somma();
}
}
Output del programma:
Contenuto di superOg:
i e j: 10 20
Contenuto di subOg:
i e j: 7 8
k: 9
Somma di i, j e k in subOg:
i+j+k: 24
Come si può vedere, la sottoclasse B
include tutti i membri della sua superclasse A
. Ciò spiega perché subOg
può accedere a i
e j
e chiamare mostraIJ()
. Inoltre, all'interno di somma()
, i
e j
possono essere richiamati direttamente, come se facessero parte di B
.
Anche se A
è una superclasse per B
, rimane comunque una classe indipendente e autonoma. Essere una superclasse per una sottoclasse non implica che la superclasse non possa essere usata da sola. Inoltre, una sottoclasse può diventare a sua volta superclasse di un'altra sottoclasse.
La forma generale di una dichiarazione di classe che eredita da una superclasse è mostrata qui:
class nomeSottoclasse extends nomeSuperclass {
// corpo della classe
}
È possibile specificare una sola superclasse per ogni sottoclasse che creiamo.
Java non supporta l'ereditarietà di più superclassi in un'unica sottoclasse, chiamata anche ereditarietà multipla.
È però possibile creare una gerarchia di ereditarietà in cui una sottoclasse diventa superclasse di un'altra sottoclasse. Nessuna classe, tuttavia, può essere superclasse di sé stessa.
Accesso ai membri ed ereditarietà
Sebbene una sottoclasse includa tutti i membri della sua superclasse, non può accedere a quei membri della superclasse che sono stati dichiarati come private
. Per esempio, si consideri la seguente semplice gerarchia di classi:
/* In una gerarchia di classi, i membri private
rimangono privati alla loro classe.
Questo programma contiene un errore e non verrà
compilato.
*/
// Creare una superclasse.
class A {
int i; // accesso predefinito
private int j; // privato per A
void impostaIJ(int x, int y) {
i = x;
j = y;
}
}
// j di A non è accessibile qui.
class B extends A {
int totale;
void somma() {
totale = i + j; // ERRORE, j non è accessibile qui
}
}
class Accesso {
public static void main(String[] args) {
B sottoOg = new B();
sottoOg.impostaIJ(10, 12);
sottoOg.somma();
System.out.println("Totale è " + sottoOg.totale);
}
}
Questo programma non verrà compilato perché l'uso di j
all'interno del metodo somma()
di B
provoca una violazione di accesso. Poiché j
è dichiarato private
, è accessibile solo dagli altri membri della sua stessa classe. Le sottoclassi non vi hanno accesso.
private
e l'ereditarietà
Un membro di classe dichiarato private
rimane privato per la sua classe. Non è accessibile da alcun codice esterno alla classe, incluse le sottoclassi.
Un esempio pratico
Esaminiamo ora un esempio più concreto che aiuta a illustrare la potenza dell'ereditarietà.
In questa sezione, l'ultima versione della classe Scatola
sviluppata nelle lezioni precedenti verrà estesa per includere una quarta componente chiamata peso
. La nuova classe conterrà quindi larghezza, altezza, profondità e peso di una scatola.
// Questo programma usa l'ereditarietà per estendere Scatola.
class Scatola {
double larghezza;
double altezza;
double profondita;
// costruttore di copia
Scatola(Scatola ob) { // passa oggetto al costruttore
larghezza = ob.larghezza;
altezza = ob.altezza;
profondita = ob.profondita;
}
// costruttore usato quando tutte le dimensioni sono specificate
Scatola(double l, double a, double p) {
larghezza = l;
altezza = a;
profondita = p;
}
// costruttore usato quando non viene specificata alcuna dimensione
Scatola() {
larghezza = -1; // usa -1 per indicare
altezza = -1; // stato non inizializzato
profondita = -1; // della scatola
}
// costruttore usato quando viene creato un cubo
Scatola(double lato) {
larghezza = altezza = profondita = lato;
}
// calcola e restituisce il volume
double volume() {
return larghezza * altezza * profondita;
}
}
// Qui, Scatola è estesa per includere il peso.
class ScatolaPeso extends Scatola {
double peso; // peso della scatola
// costruttore per ScatolaPeso
ScatolaPeso(double l, double a, double p, double m) {
larghezza = l;
altezza = a;
profondita = p;
peso = m;
}
}
class DemoScatolaPeso {
public static void main(String[] args) {
ScatolaPeso miaScatola1 = new ScatolaPeso(10, 20, 15, 34.3);
ScatolaPeso miaScatola2 = new ScatolaPeso(2, 3, 4, 0.076);
double vol;
vol = miaScatola1.volume();
System.out.println("Volume di miaScatola1 è " + vol);
System.out.println("Peso di miaScatola1 è " + miaScatola1.peso);
System.out.println();
vol = miaScatola2.volume();
System.out.println("Volume di miaScatola2 è " + vol);
System.out.println("Peso di miaScatola2 è " + miaScatola2.peso);
}
}
Output del programma:
Volume di miaScatola1 è 3000.0
Peso di miaScatola1 è 34.3
Volume di miaScatola2 è 24.0
Peso di miaScatola2 è 0.076
ScatolaPeso
eredita tutte le caratteristiche di Scatola
e vi aggiunge la componente peso
. Non è necessario che ScatolaPeso
ricrei tutte le funzionalità presenti in Scatola
; può semplicemente estendere Scatola
per i propri scopi.
Un vantaggio fondamentale dell'ereditarietà consiste nel fatto che, una volta creata una superclasse che definisce gli attributi comuni a un insieme di oggetti, la si può utilizzare per generare un numero qualsiasi di sottoclassi più specifiche. Ogni sottoclasse può modellare con precisione la propria classificazione. Per esempio, la classe seguente eredita Scatola
e aggiunge un attributo colore
:
// Qui, Scatola è estesa per includere il colore.
class ScatolaColore extends Scatola {
int colore; // colore della scatola
ScatolaColore(double l, double a, double p, int c) {
larghezza = l;
altezza = a;
profondita = p;
colore = c;
}
}
L'essenza dell'ereditarietà
L'ereditarietà consente di creare una gerarchia di classi in cui una superclasse definisce gli attributi comuni, e le sottoclassi estendono questa superclasse per aggiungere attributi specifici. Questo approccio promuove la riusabilità del codice e la specializzazione delle classi.
Una volta creata una superclasse che definisce gli aspetti generali di un oggetto, tale superclasse può essere ereditata per formare classi specializzate. Ogni sottoclasse aggiunge semplicemente i propri attributi unici. Questa è l'essenza dell'ereditarietà.
Una variabile di superclasse può fare riferimento a un oggetto di sottoclasse
Una variabile di riferimento di una superclasse può ricevere un riferimento a qualsiasi sottoclasse derivata da quella superclasse.
Questo aspetto dell'ereditarietà risulta molto utile in diverse situazioni. Per esempio, si consideri il frammento seguente:
class DemoRif {
public static void main(String[] args) {
ScatolaPeso scatolaPeso = new ScatolaPeso(3, 5, 7, 8.37);
Scatola scatolaSemplice = new Scatola();
double vol;
vol = scatolaPeso.volume();
System.out.println("Volume di scatolaPeso è " + vol);
System.out.println("Peso di scatolaPeso è " + scatolaPeso.peso);
System.out.println();
// assegnare il riferimento ScatolaPeso a un riferimento Scatola
scatolaSemplice = scatolaPeso;
vol = scatolaSemplice.volume(); // OK, volume() definito in Scatola
System.out.println("Volume di scatolaSemplice è " + vol);
/* La seguente istruzione è non valida perché scatolaSemplice
non definisce un membro peso. */
// System.out.println("Peso di scatolaSemplice è " + scatolaSemplice.peso);
}
}
In questo esempio, scatolaPeso
è un riferimento a oggetti ScatolaPeso
, mentre scatolaSemplice
è un riferimento a oggetti Scatola
. Poiché ScatolaPeso
è una sottoclasse di Scatola
, è lecito assegnare a scatolaSemplice
il riferimento contenuto in scatolaPeso
.
È importante comprendere che è il tipo della variabile di riferimento — non il tipo dell'oggetto a cui si riferisce — a determinare quali membri possano essere accessibili.
Quando un riferimento a un oggetto di sottoclasse viene assegnato a una variabile di riferimento di superclasse, si avrà accesso solo alle parti dell'oggetto definite dalla superclasse. Ecco perché scatolaSemplice
non può accedere a peso
anche se, in quel momento, fa riferimento a un oggetto ScatolaPeso
.
Ciò ha senso, perché la superclasse non ha conoscenza di ciò che una sottoclasse vi aggiunge. Per questo motivo l'ultima istruzione del frammento precedente è commentata: non è possibile per un riferimento di tipo Scatola
accedere al campo peso
, poiché Scatola
non lo definisce.
Sebbene l'esempio possa sembrare un po' astratto, presenta numerose applicazioni pratiche. In particolare è alla base del polimorfismo, che vedremo in dettaglio in una lezione successiva.