Introduzione
Secondo alcuni sondaggi come quello di JetBrains, la versione 8 di Java è al momento quella più utilizzata in assoluto dagli sviluppatori di tutto il mondo, nonostante si tratti di una release del 2014. Quello che state leggendo è il primo di una serie di articoli intitolata “Superare Java 8”, ispirata ai contenuti del mio ultimo libro “Il nuovo Java”. Questi articoli accompagneranno passo dopo passo il lettore all’esplorazione delle più importanti caratteristiche introdotte a partire dalla versione 9. L’obiettivo è quello di far acquisire la consapevolezza di quanto è importante aggiornare le proprie conoscenze relative a Java, spiegando gli enormi vantaggi che offrono le ultime versioni del linguaggio.
In questo articolo parleremo di una dei tipici dilemmi che si pongono quando si impara Java, nonché una delle domande più frequenti che vengono fatte ai colloqui di lavoro: quale sono le differenze tra una classe astratta ed un’interfaccia. Siccome dalla versione 8 di Java sono cambiate diverse definizioni, questo articolo cercherà di evidenziare tutte le differenze per eliminare ogni dubbio.
Definizione di classe astratta
Una classe astratta è una classe si distingue da una classe ordinaria perché:
- È dichiarata con il modificatore
abstract - Non può essere istanziata
- Può dichiarare metodi astratti (ovvero metodi dichiarati con il modificatore
abstract)
Chiariamo subito che nulla vieta ad una classe astratta di implementare tutti i suoi metodi. Non è vero che una classe astratta debba avere per forza almeno un metodo astratto. È invece vero il contrario: se una classe contiene un metodo astratto allora deve essere dichiarata astratta.
Per il resto è una classe come le altre e può dichiarare costruttori, metodi concreti, variabili, costanti, membri statici, tipi innestati, può implementare interfacce, estendere un’altra classe, essere estesa da altre classi e così via. Un esempio di classe astratta potrebbe essere il seguente:
public abstract class Veicolo {
private String descrizione;
public abstract void accelera();
public abstract void decelera();
// costruttori e metodi setter e getter omessi
}
Notare che i metodi accelera e decelera sono dichiarati astratti. Infatti, stiamo definendo un concetto generico (un veicolo che si muove) non implementabile in maniera concreta (ogni veicolo si muove in maniera diversa).
Quando dichiarare una classe astratta
Abbiamo detto che una classe astratta che dichiara almeno un metodo astratto, deve essere dichiarata astratta anch’essa. Questo è il caso standard, la classe Veicolo per esempio, è astratta perché non sappiamo come definire i suoi metodi accelera e decelera dato che non sappiamo di che veicolo stiamo parlando. Vien da sé che tale classe è stata definita per essere estesa da sottoclassi concrete, per esempio la classe Auto, la classe Nave, e la classe Aereo, ognuna delle quali fornirà un’implementazione diversa ai metodi accelera e decelera con degli override.
A livello di progettazione quindi le classi astratte costituiscono uno strumento fondamentale. Infatti, dichiarare una classe abstract, significa voler indirizzare lo sviluppo verso la creazione di sottoclassi della classe astratta. Questo non solo ci permetterà di risparmiare codice che avremmo dovuto mettere nelle sottoclassi e che invece possiamo mettere a fattor comune nella superclasse astratta, ma soprattutto ci darà la possibilità di trattare gli oggetti delle sottoclassi Auto, Nave, e Aereo, come fossero veicoli (vedi sezione sul polimorfismo).
Pertanto, non ha senso dichiarare una classe astratta se non si ha intenzione di estenderla. Inoltre, è fondamentale aver presente che le classi astratte hanno il compito di rappresentare concetti troppo generici per il contesto in cui si definiscono. In particolare, il modificatore abstract impone un importante vincolo progettuale: la classe non si può istanziare (anche se la classe definisce tutti i suoi metodi).
Concludendo, dobbiamo usare una classa astratta per definire un concetto troppo generico per poter essere istanziato rispetto al contesto del nostro programma, per poi estenderla con classi concrete sfruttando i benefici dell’ereditarietà e del polimorfismo.
Oggetti troppo generici: esempio
Supponiamo di voler creare un’applicazione che permette di comporre e riprodurre musica. Tale applicazione deve definire degli strumenti musicali virtuali, ovvero degli strumenti software che simulano strumenti musicali reali. Per fare ciò, potremmo creare una gerarchia di strumenti musicali, di cui riportiamo una parte di seguito:
public abstract class Strumento { //Classe astratta
private String nome;
private String tipo;
public abstract void suona(String nota); /* Ogni strumento suona in modo
* diverso! Impossibile definire
* questo metodo! */
//resto del codice omesso
}
public class Chitarra extends Strumento { //Classe concreta
private int numeroCorde;
@Override
public void suona(String nota) { //Override del metodo ereditato
//Implementazione del metodo per la chitarra.
}
//resto del codice omesso
}
public abstract class StrumentoAFiato extends Strumento { /* Classe astratta che
* estende la classe
* Strumento */
private String materiale;
/* Metodo suona ereditato ancora astratto e non riscritto
perché troppo generico! */
//resto del codice omesso
}
public class Flauto extends StrumentoAFiato { /* Classe concreta che estende
* StrumentoAFiato */
@Override
public void suona(String nota) {
//Implementazione del metodo per il flauto.
}
//resto del codice omesso
}
In questo esempio abbiamo visto come Strumento sia la superclasse della classe concreta Chitarra (che ridefinisce il metodo suona) e della classe astratta StrumentoAFiato (la quale non ridefinisce il metodo suona che viene ereditato astratto). Infine la classe concreta Flauto estende StrumentoAFiato (che a sua volta estendeva Strumento). Quindi la chitarra è uno strumento, e il flauto è uno strumento a fiato e quindi anche uno strumento. Non possiamo istanziare oggetti dalle due classi astratte, e questo è coerente con la loro astrazione. Infatti, come potremmo implementare il metodo suona della classe StrumentoAFiato? Uno strumento a fiato generico nel mondo reale non esiste: esistono le armoniche, le trombe, i flauti, i clarinetti etc., quindi nel contesto del nostro programma uno strumento a fiato è troppo generico per poter essere definito concretamente.
Polimorfismo
Tuttavia, le classi astratte ci permettono di evitare duplicazioni (notare che ci sono variabili che sono definite nelle classi astratte) e soprattutto di sfruttare il polimorfismo. Per esempio, il nostro programma potrebbe definire una classe Esecutore che si occupa di eseguire le note di un qualsiasi strumento:
public class Esecutore {
public void esegui(Strumento strumento, String nota) {
strumento.suona(nota);
}
// resto del codice omesso
}
Notare che il metodo esegui, potrà eseguire una qualsiasi nota di un qualsiasi strumento. E questo facilita enormemente l’interazione con il nostro software, perché per suonare qualsiasi strumento utilizzeremo sempre lo stesso metodo.
Definizione di interfaccia
Un’interfaccia, al pari di una classe è uno dei 5 tipi definiti dal linguaggio Java (gli altri tipi sono le enumerazioni, le annotazioni e dalla versione 14 in poi i record). La definizione di interfaccia è stata modificata in maniera importante a partire dalla versione 8 di Java. Sino alla versione 7 infatti, il concetto di interfaccia era molto semplice e chiaro, visto che tutti i metodi erano implicitamente pubblici e astratti. Oggi invece un’interfaccia è definita dalle seguenti caratteristiche:
- Si dichiara usando la parola chiave
interface - Non può essere istanziata
- Può estendere altre interfacce
- Una classe può implementare più interfacce
- Può dichiarare:
- Metodi astratti pubblici (non è necessario usare i modificatori
publiceabstractche verranno aggiunti implicitamente dal compilatore) - Metodi di default pubblici, ovvero metodi concreti marcati con il modificatore
default(non è necessario usare il modificatorepublicche verrà aggiunto implicitamente dal compilatore) - Metodi privati concreti (possono essere invocati solamente da metodi di default)
- Metodi statici pubblici o privati (un metodo statico senza specificatori d’accesso sarà implicitamente considerato
publicdal compilatore) - Costanti statiche e pubbliche (non è necessario usare i modificatori
public,finalestatice che verranno aggiunti implicitamente dal compilatore)
- Metodi astratti pubblici (non è necessario usare i modificatori
Non è possibile dichiarare altro all’interno di un’interfaccia.
In realtà esistono altre proprietà avanzate che caratterizzano le interfacce, come quello di poter dichiarare tipi innestati e quello di essere sempre implicitamente statiche quando dichiarate come tipi innestati, ma queste proprietà risultano poco interessanti ai più, perché sono utili solo in casi molto rari. Se siete interessati, trovate questi ed altri argomenti avanzati negli approfondimenti del libro (in inglese) “Java for Aliens”.
Un esempio di interfaccia potrebbe essere il seguente:
public interface Pesabile {
public static final String UNITA_DI_MISURA = "kg";
public abstract double getPeso();
}
che possiamo equivalentemente riscrivere omettendo tutti i modificatori:
public interface Pesabile {
String UNITA_DI_MISURA = "kg";
double getPeso();
}
Le classi però, non possono estendere le interfacce utilizzando la parola chiave extends, ma possono implementarle. Infatti, la parola chiave implements, si usa in maniera molto simile ad extends, e produce lo stesso risultato: quello di ereditare i membri dell’interfaccia implementata. Potremmo quindi utilizzare l’interfaccia dell’esempio, implementandola in una classe Articolo:
public class Articolo implements Pesabile {
private double peso;
private String descrizione;
public Articolo (String descrizione, double peso) {
setDescrizione(descrizione);
setPeso(peso);
}
public double getPeso() {
return peso;
}
// resto del codice omesso
}
Dal punto di vista della progettazione, un’interfaccia è un’evoluzione del concetto di classe astratta. Infatti è possibile obbligare le sottoclassi ad implementare i metodi astratti definiti nelle interfacce, come e meglio di come possiamo fare con le classi astratte.
Dal punto di vista del codice invece, un’interfaccia assomiglia ad una classe senza la sua implementazione interna. Un’interfaccia vuole rappresentare infatti quella che con l’incapsulamento chiamiamo proprio interfaccia pubblica, ovvero quella parte dell’oggetto visibile all’esterno, che nasconde la sua implementazione interna. Quindi è da considerarsi come la metà non implementata di un oggetto.
Differenza nella dichiarazione
L’esempio precedente è un ottimo esempio di interfaccia pre-Java 8. Oggi però, il nome interfaccia, in un certo senso ha perso il significato originario. Infatti, anche se è sempre possibile usare le interfacce dichiarando solo metodi astratti, con l’introduzione dei metodi di default e dei metodi statici, le interfacce ormai sono tecnicamente quasi equivalenti alle classi astratte per quanto riguarda la dichiarazione. Se mettiamo da parte l’uso della parola chiave interface al posto di abstract class, e il fatto che per le interfacce i modificatori possono essere implicitamente dedotti dal compilatore, l’unica importante differenza, è che le interfacce non possono dichiarare variabili d’istanza e costruttori. Per il resto sono due concetti molto simili tecnicamente. Infatti non è possibile istanziare né classi astratte né interfacce. Inoltre, il vantaggio comune che offrono sia le classi astratte sia le interfacce, risiede nel fatto che esse possono obbligare le sottoclassi ad implementare comportamenti. Una classe che eredita un metodo astratto infatti, deve fare override del metodo ereditato oppure essere dichiarata a sua volta astratta. Dal punto di vista della progettazione quindi, questi strumenti supportano l’astrazione in maniera molto simile.
Differenza concettuale
Una delle differenze più importanti e troppo spesso ignorata, è una differenza concettuale. Abbiamo detto che una classe astratta, deve definire un’astrazione troppo generica per essere istanziata nel contesto in cui si dichiara. Un buon esempio è la classe Veicolo:
public abstract class Veicolo {
private String descrizione;
public abstract void accelera();
public abstract void decelera();
// costruttori e metodi setter e getter omessi
}
Possiamo quindi estendere la classe Veicolo per esempio con la classe Aereo, che ovviamente avrà una propria implementazione dei metodi accelera e decelera (ereditati astratti):
public class Aereo extends Veicolo {
@Override
public void accelera() {
// override del metodo ereditato
// implementazione omessa
}
@Override
public void decelera() {
// override del metodo ereditato
// implementazione omessa
}
// altro codice omesso
}
Un’interfaccia dovrebbe astrarre un comportamento che più classi potrebbero implementare, e un comportamento non si dovrebbe istanziare. Infatti non dovrebbero esistere oggetti che rappresentano un comportamento. Spesso infatti le interfacce hanno nomi che richiamano aggettivi e comportamenti (Pesabile, Comparable, Cloneable, etc.). Semmai dovrebbero esistere oggetti che implementano uno più comportamenti.
Per esempio potremmo introdurre un’interfaccia Volante che sarà implementata dalle classi che rappresentano oggetti che volano (si noti come il nome faccia pensare ad un comportamento più che ad un oggetto astratto):
public interface Volante {
void atterra();
void decolla();
}
ogni classe che deve astrarre un concetto di oggetto volante (come un aereo, un drone o anche un uccello), deve implementare l’interfaccia Volante. Riscriviamo quindi la classe Aereo nel seguente modo:
public class Aereo extends Veicolo implements Volante {
public void atterra() {
// override del metodo di Volante
}
public void decolla() {
// override del metodo di Volante
}
public void accelera() {
// override del metodo di Veicolo
}
public void decelera() {
// override del metodo di Veicolo
}
}
Nella figura 1 trovate il relativo diagramma delle classi.

Figura 1 – Diagramma delle classi.
Potremmo quindi creare parametri polimorfi per sfruttare l’interfaccia Volante:
public class TorreDiControllo {
public void autorizzaAtterraggio(Volante v) {
v.atterra();
}
public void autorizzaDecollo(Volante v) {
v.decolla();
}
}
Così facendo, possiamo passare a questi metodi oggetti volanti creati da classi che implementano l’interfaccia Volante.
Ereditarietà Multipla
La più famosa e importante differenza però riguarda l’ereditarietà. Infatti, mentre è possibile estendere una sola classe alla volta, è invece possibile implementare un numero qualsiasi di interfacce. Con l’introduzione dei metodi di default in Java 8, è importante sottolineare che è stata anche introdotta una versione semplificata di quella caratteristica chiamata ereditarietà multipla.
Parliamo di semplificazione perché le classi possono ereditare dalle interfacce solo la loro parte funzionale (i metodi) e non i dati (a parte le eventuali costanti statiche che un’interfaccia può dichiarare). In altri linguaggi invece, esiste l’ereditarietà multipla completa, che porta con sé regole molto complicate per gestire i problemi derivanti dalla sua implementazione.
Era possibile implementare più interfacce anche prima di Java 8 ma si ereditavano metodi astratti che bisognava riscrivere in ogni caso. Con l’avvento dei metodi di default, l’ereditarietà multipla ha assunto un significato diverso rispetto al passato. Quindi, se consideriamo le seguenti interfacce:
public interface Lettore {
default void leggi(Libro libro) {
System.out.println("Sto leggendo: " + libro.getTitolo()
+ " di " + libro.getAutore());
}
}
public interface Programmatore {
default void programma(String linguaggio) {
System.out.println("Sto programmando in " + linguaggio);
}
}
è possibile creare la seguente classe che implementa entrambe le interfacce e ne eredita i metodi:
public class ChiStaLeggendo implements Lettore, Programmatore {
}
Ed eccone un esempio di utilizzo:
public class TestEreditarietaMultipla {
public static void main(String args[]) {
var tu = new ChiStaLeggendo();
var nuovoJava = new Libro("Il nuovo Java", "Claudio De Sio Cesari");
tu.programma("Java");
tu.leggi(nuovoJava);
}
}
ovviamente è possibile implementare l’ereditarietà multipla in Java anche estendendo contemporaneamente una classe (astratta o non) e una o più interfacce.
Applicabilità dell’ereditarietà
Un’altra differenza meno conosciuta ma importante, riguarda l’applicabilità dell’ereditarietà. Infatti, solo una classe può estendere un’altra classe, non gli altri tipi di Java. Invece le interfacce possono estendere altre interfacce, ma non possono estendere classi (astratte o concrete). Le interfacce inoltre, possono essere implementate da classi, enumerazioni e record, che quindi possono usufruire anche dei metodi di default ereditati. In particolare i record (introdotti in Java 14 come feature preview), permettono di definire classi che rappresentano dati immutabili con una sintassi minimale. Essi non sono estendibili e non possono estendere classi. Questo perché in fase di compilazione i record vengono trasformati in classi dichiarate final che estendono la classe dal java.lang.Record, e quindi non possono estendere altre classi. Fortunatamente, possono implementare interfacce. Per esempio, considerata l’interfaccia Progettista:
public interface Progettista {
default void progetta(String strumento) {
System.out.println("Sto progettando software con " + strumento);
}
}
possiamo creare il record Dipendente nel seguente modo:
public record Dipendente (String nome, int matricola) implements Progettista { }
e usarlo con un codice come il seguente:
Dipendente giorgio = new Dipendente("Giorgio", 10);
giorgio.progetta("UML");
ottenendo l’output:
Sto progettando software con UML
Lo stesso discorso si ripete per le enumerazioni, che possono implementare interfacce, ma non estendere classi, dato che in fase di compilazione vengono trasformate in classi che estendono la classe java.lang.Enum.
Conclusioni
Con l’evoluzione delle interfacce a partire da Java 8, le differenze tecniche con le classi astratte si sono ridotte. Non è possibile istanziare né classi astratte né interfacce. Inoltre, il vantaggio comune che offrono sia le classi astratte sia le interfacce, risiede nel fatto che esse possono obbligare le sottoclassi ad implementare metodi astratti. Potremmo però riassumere le differenze principali semplicisticamente in questo modo:
- Le interfacce non possono dichiarare dati (a parte costanti statiche e pubbliche).
- Le classi astratte dovrebbero astrarre oggetti troppo generici per poter essere istanziati, mentre le interfacce dovrebbero astrarre comportamenti che oggetti diversi potrebbero implementare.
- È possibile estendere una sola classe alla volta, ma è possibile implementare più interfacce.
- Le classi possono essere estese solo da altre classi, mentre le interfacce possono essere implementate anche da enumerazioni e record.
Note dell’autore
Questo articolo è basto su alcuni paragrafi del mio libro “Il nuovo Java”, e del mio libro in inglese “Java for Aliens”.
