1. Classi e metodi astratti
A volte nella vita (e nella programmazione) viene da dire: «Non so esattamente come si fa, ma so per certo che deve esserci!» Per esempio, tutti gli animali devono saper emettere un suono, ma quale — dipende dal singolo animale. Per questi casi in Java sono stati pensati le classi astratte e i metodi astratti.
Classe astratta — è una classe che non può essere creata direttamente (non si può scrivere new Animal() se Animal è astratta), ma da cui si può ereditare. Una classe del genere può contenere sia metodi normali (implementati) sia metodi astratti — cioè dichiarati ma non implementati.
Metodo astratto — è un metodo senza corpo. Si dichiara con la parola chiave abstract e deve essere obbligatoriamente implementato nelle sottoclassi (a meno che la sottoclasse non sia a sua volta astratta).
Esempio concreto
Supponiamo di avere un’app per uno zoo. Vogliamo che tutti gli animali abbiano il metodo makeSound(), ma non sappiamo quale suono emettano. Allora creiamo una classe astratta:
public abstract class Animal {
public abstract void makeSound(); // Metodo astratto
}
E gli animali concreti implementano questo metodo a modo loro:
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Bau bau!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Miao!");
}
}
Ora, se qualcuno prova a creare new Animal(), il compilatore dirà subito: «Mi dispiace, ma gli animali astratti non esistono in natura!» Questo è utile: garantite che nel programma esistano solo animali concreti con comportamenti concreti.
2. Polimorfismo tramite astrazione
Astrazione — è l’individuazione di un’interfaccia comune per un gruppo di oggetti. La classe astratta definisce proprio questa interfaccia comune: specifica quali metodi devono essere necessariamente implementati da tutti gli eredi.
Polimorfismo e astrazione lavorano in coppia: la classe astratta garantisce che tutti gli eredi abbiano i metodi necessari, e il polimorfismo permette di chiamarli tramite un riferimento al tipo base.
Esempio: creiamo uno zoo
Mettiamo insieme un piccolo zoo. Abbiamo la classe astratta Animal e alcuni suoi eredi:
public abstract class Animal {
public abstract void makeSound();
}
public class Cow extends Animal {
@Override
public void makeSound() {
System.out.println("Muu!");
}
}
public class Duck extends Animal {
@Override
public void makeSound() {
System.out.println("Qua qua!");
}
}
Ora possiamo creare un array di animali:
Animal[] zoo = {
new Dog(),
new Cat(),
new Cow(),
new Duck()
};
for (Animal animal : zoo) {
animal.makeSound(); // Per ogni animale verrà chiamato il metodo "giusto"
}
Ogni oggetto nell’array è un animale concreto, ma per il codice è semplicemente un Animal. Grazie a polimorfismo e astrazione possiamo essere certi che ogni oggetto abbia il metodo makeSound() e che esso funzioni correttamente.
3. Uso delle classi astratte per il polimorfismo
Vediamo un esempio più pratico. Immaginiamo di sviluppare un’app per la gestione dei dipendenti di un’azienda. Abbiamo diversi tipi di dipendenti: manager, sviluppatori, tester. Tutti hanno un metodo comune work(), ma lo eseguono in modo diverso.
Classe astratta Employee
public abstract class Employee {
protected String name;
public Employee(String name) {
this.name = name;
}
public abstract void work();
}
Sottoclassi concrete
public class Manager extends Employee {
public Manager(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " coordina il team.");
}
}
public class Developer extends Employee {
public Developer(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " scrive codice.");
}
}
public class Tester extends Employee {
public Tester(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " testa l'applicazione.");
}
}
Uso del polimorfismo
Ora possiamo creare un array di dipendenti e chiamare per ciascuno il metodo work():
Employee[] employees = {
new Manager("Anna"),
new Developer("Ivan"),
new Tester("Maria")
};
for (Employee e : employees) {
e.work();
}
Risultato:
Anna coordina il team.
Ivan scrive codice.
Maria testa l'applicazione.
Nota: nel ciclo non sappiamo (e non vogliamo sapere!) quale sia esattamente il tipo di dipendente. Chiamiamo semplicemente work() e ogni oggetto fa il suo lavoro.
4. Dettagli utili
Garanzia di implementazione dei metodi
Una classe astratta obbliga tutti gli eredi a implementare i metodi richiesti. Se dimenticate di implementare un metodo astratto in una sottoclasse, il compilatore vi rimprovererà subito: «Devi farlo!»
Interfaccia universale
Il codice che lavora con un array o una lista di tipo astratto (Employee[], List<Animal>) può essere del tutto generico. Potete aggiungere nuove sottoclassi — e il codice principale non dovrà essere modificato.
Protezione da oggetti «non conformi»
Poiché una classe astratta non può essere creata direttamente, nessuno potrà accidentalmente creare un oggetto di tipo «indefinito» che non implementa i metodi necessari.
Teoria e sintassi: come dichiarare una classe e un metodo astratti
- Una classe astratta si dichiara con la parola chiave abstract davanti a class.
- Un metodo astratto si dichiara con la parola chiave abstract e non ha corpo (solo il punto e virgola).
- Una classe con almeno un metodo astratto deve essere astratta.
- Una classe che estende una classe astratta deve implementarne tutti i metodi astratti, oppure essere essa stessa astratta.
Schema
public abstract class Animal {
public abstract void makeSound();
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Miao!");
}
}
5. Errori tipici nell’uso delle classi astratte
Errore n. 1: tentativo di creare un oggetto di una classe astratta.
Codice come new Animal() non verrà compilato. Le classi astratte — sono come istruzioni di montaggio senza i pezzi: finché non compare una sottoclasse concreta, l’oggetto non si può assemblare.
Errore n. 2: dimenticare di implementare un metodo astratto nella sottoclasse.
Se avete dichiarato un metodo astratto ma non lo avete implementato nell’erede (e non avete reso astratta anche la classe), il compilatore si lamenterà e mostrerà un errore.
Errore n. 3: dimenticare i modificatori di accesso.
Un metodo ridefinito non può avere un modificatore di accesso più restrittivo di quello nella classe base. Per esempio, se il metodo astratto era public, anche l’implementazione deve essere public (e non protected o private).
Errore n. 4: tentativo di usare un metodo astratto con un corpo.
Un metodo contrassegnato abstract non può avere un corpo, altrimenti il compilatore alzerà gli occhi al cielo e dirà: «Deciditi — o astrazione, o implementazione!»
Errore n. 5: il polimorfismo non funziona per i metodi statici.
Il polimorfismo funziona solo per i metodi non statici. I metodi statici non si sovrascrivono — si nascondono; quindi il comportamento alla chiamata dipende dal tipo della variabile, non dall’oggetto effettivo.
GO TO FULL VERSION