Classi Astratte in Java
Definisci implementazioni parziali in Java con classi e metodi astratti che le sottoclassi devono completare.
Una classe astratta è una classe che non può essere istanziata direttamente. Esiste per essere estesa. Può combinare metodi concreti (con corpo) e metodi astratti (senza corpo, che la sottoclasse deve implementare) — questa combinazione la distingue sia da una classe normale che da un'interfaccia.
Questa pagina illustra come dichiarare una classe astratta, come le sottoclassi la completano, perché le classi astratte possono contenere stato, il pattern template method e come scegliere tra una classe astratta e un'interfaccia.
Usa una classe astratta quando le sottoclassi concrete devono condividere stato e infrastruttura, non solo un contratto API. Se hai bisogno solo di un contratto, un'interfaccia è la scelta migliore. Le classi astratte si basano sull'ereditarietà e il polimorfismo, quindi è utile conoscere bene questi concetti prima.
Dichiarare una classe astratta
Aggiungi abstract all'intestazione della classe. Aggiungi abstract a qualsiasi metodo che non ha corpo:
public abstract class Shape {
protected final String name;
protected Shape(String name) { this.name = name; }
public abstract double area(); // no body — subclass must provide one
public String describe() { // concrete — inherited as-is
return name + " area=" + area();
}
}Da questo derivano alcune conseguenze:
new Shape("circle")è un errore di compilazione — le classi astratte non possono essere istanziate.- Una sottoclasse che non implementa tutti i metodi astratti ereditati deve essa stessa essere dichiarata
abstract. Possono esistere sottoclassi astratte di classi astratte. - Una classe astratta può avere un costruttore — le sottoclassi lo invocano con
super(...)proprio come un normale genitore.
Implementare i metodi astratti
Una sottoclasse concreta deve fornire un corpo per ogni metodo astratto ereditato:
public class Circle extends Shape {
private final double r;
public Circle(double r) {
super("circle");
this.r = r;
}
@Override
public double area() { return Math.PI * r * r; }
}Ora new Circle(2) funziona, e describe() (ereditato da Shape) chiama il metodo area() della sottoclasse tramite dispatch dinamico.
Le classi astratte possono contenere stato
Questo è il motivo principale per preferire una classe astratta a un'interfaccia. Il genitore può dichiarare campi, scrivere un costruttore che li inizializza e offrire metodi che operano su quello stato condiviso:
public abstract class HttpHandler {
private final String path;
protected HttpHandler(String path) { this.path = path; }
public final String path() { return path; }
public abstract Response handle(Request r); // subclass-specific behavior
}Ogni handler concreto ha un path; il genitore lo memorizza e lo espone; ogni sottoclasse scrive solo la logica specifica per la richiesta. Le interfacce non possono fare questo da sole (non hanno campi di istanza).
Combinare metodi astratti e concreti — il template method
Un pattern comune: la classe astratta implementa il flusso generale come metodo concreto e lascia astratti i punti di variazione. Le sottoclassi completano solo le parti che differiscono:
public abstract class Beverage {
// Template — the algorithm, written once.
public final void prepare() {
boilWater();
brew(); // varies
pourIntoCup();
addCondiments(); // varies
}
protected abstract void brew();
protected abstract void addCondiments();
private void boilWater() { System.out.println("boiling water"); }
private void pourIntoCup() { System.out.println("pouring into cup"); }
}
public class Tea extends Beverage {
protected void brew() { System.out.println("steeping tea"); }
protected void addCondiments() { System.out.println("adding lemon"); }
}Tea.prepare() esegue il template del genitore, che richiama brew e addCondiments di Tea tramite polimorfismo. Aggiungere una sottoclasse Coffee richiede solo i due metodi astratti.
Questo è il pattern template method ed è il motivo più comune per scegliere una classe astratta.
Astratto vs finale
abstract e final sono opposti e il compilatore lo impone:
abstract class— deve essere estesa.final class— non deve essere estesa.
Lo stesso vale per i metodi: i metodi abstract devono essere sovrascritti; i metodi final non possono esserlo. Scriverli entrambi insieme è un errore di compilazione.
Classe astratta vs interfaccia
| Classe astratta | Interfaccia | |
|---|---|---|
| Costruttori | Sì | No |
| Campi di istanza | Sì | No (solo costanti public static final) |
| Corpi dei metodi | Sì (qualsiasi numero) | Sì tramite default (usato con parsimonia) |
| Ereditarietà | Singola — un solo genitore | Multipla — più interfacce |
| Quando usare | Le sottoclassi condividono stato e infrastruttura | Le sottoclassi condividono solo un contratto API |
Una regola pratica: inizia con un'interfaccia. Passa a (o aggiungi) una classe astratta solo se riscontri l'accumulo di codice condiviso tra le implementazioni. I metodi default di Java 8+ sulle interfacce hanno eroso parte del territorio che un tempo apparteneva alle classi astratte, ma il caso d'uso dello "stato mutabile condiviso" rimane territorio delle classi astratte.
I metodi astratti non possono essere private o static
privaterenderebbe il metodo invisibile alle sottoclassi — non potrebbero sovrascriverlo, rendendo inapplicabile l'astrazione.- I metodi
staticnon vengono dispatchati dinamicamente — non possono essere sovrascritti, solo nascosti — quindi un metodo astratto statico sarebbe privo di senso.
Queste due combinazioni sono errori di compilazione.
Errori comuni
- Dimenticare
abstractsulla classe. Un metodo senza corpo all'interno di una classe non astratta non compila — il compilatore richiede che anche la classe contenitrice siaabstract. - Tentare di istanziarla.
new Shape("x")viene rifiutato in fase di compilazione. Istanzia invece una sottoclasse concreta. - Lasciare un metodo non implementato in una sottoclasse concreta. Se anche un solo metodo astratto ereditato non ha un corpo, la sottoclasse deve anch'essa essere dichiarata
abstract. - Aspettarsi l'ereditarietà multipla. Una classe può estendere un solo genitore (astratto o meno). Se devi combinare più contratti, usa le interfacce.
Un esempio pratico
Cosa c'è dopo
Hai ora visto la versione dell'astrazione che include stato e codice condiviso. La versione che elimina entrambi — puro contratto, nessuna implementazione — è l'interfaccia. Continua con le interfacce Java.