1. Che cos’è una gerarchia di classi?
Nella programmazione (e non solo) una gerarchia è una struttura ad albero, in cui abbiamo entità «generiche» in cima all’albero (classi base) e, più in basso, entità più specifiche (sottoclassi). In Java le gerarchie di classi si costruiscono tramite l’ereditarietà: ogni sottoclasse può a sua volta essere genitore di altre sottoclassi, e così via.
Analogia:
- Un animale (Animal) è una classe generale.
- Un mammifero (Mammal) è un caso particolare di animale.
- Un cane (Dog) è un caso particolare di mammifero.
- E un «Sharik» concreto è già un oggetto della classe Dog.
Nel codice appare così:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
Una gerarchia di classi consente di descrivere le proprietà e i comportamenti comuni «in alto», e i dettagli «in basso». Questo rende il codice più logico ed evita duplicazioni.
Schema della gerarchia
Animal
├── Mammal
│ ├── Dog
│ └── Cat
└── Bird
└── Sparrow
2. Come costruire una gerarchia: logica e pratica
Definire il generale e lo specifico
La regola principale: la classe base deve contenere ciò che è caratteristico di tutti i suoi discendenti. Tutto ciò che è unico va invece nelle sottoclassi.
Esempio:
- Tutti gli animali possono respirare e mangiare — quindi i metodi breathe() ed eat() devono stare nella classe Animal.
- Solo gli uccelli sanno volare — quindi il metodo fly() sarà nella classe Bird, non in Animal.
- Solo i cani sanno abbaiare — il metodo bark() sarà nella classe Dog.
Esempio: Animali
// Classe base
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + " mangia.");
}
void makeSound() {
System.out.println(name + " emette un suono.");
}
}
// Sottoclasse: Mammifero
class Mammal extends Animal {
Mammal(String name) {
super(name);
}
void feedMilk() {
System.out.println(name + " allatta i cuccioli con il latte.");
}
}
// Sottoclasse: Cane
class Dog extends Mammal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " abbaia: bau bau!");
}
void wagTail() {
System.out.println(name + " scodinzola.");
}
}
// Sottoclasse: Gatto
class Cat extends Mammal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " miagola: miao!");
}
void purr() {
System.out.println(name + " fa le fusa.");
}
}
// Sottoclasse: Uccello
class Bird extends Animal {
Bird(String name) {
super(name);
}
void fly() {
System.out.println(name + " vola.");
}
@Override
void makeSound() {
System.out.println(name + " cinguetta: cip cip!");
}
}
Chiamata in main:
public class ZooDemo {
public static void main(String[] args) {
Dog sharik = new Dog("Sharik");
Cat murka = new Cat("Murka");
Bird sparrow = new Bird("Passero");
sharik.eat(); // Sharik mangia.
sharik.makeSound(); // Sharik abbaia: bau bau!
sharik.feedMilk(); // Sharik allatta i cuccioli con il latte.
sharik.wagTail(); // Sharik scodinzola.
murka.eat(); // Murka mangia.
murka.makeSound(); // Murka miagola: miao!
murka.feedMilk(); // Murka allatta i cuccioli con il latte.
murka.purr(); // Murka fa le fusa.
sparrow.eat(); // Passero mangia.
sparrow.makeSound(); // Passero cinguetta: cip cip!
sparrow.fly(); // Passero vola.
}
}
Visualizzazione: albero delle classi
| Classe | Genitore | Caratteristiche |
|---|---|---|
|
|
name, eat(), makeSound() |
|
|
feedMilk() |
|
|
makeSound(), wagTail() |
|
|
makeSound(), purr() |
|
|
fly(), makeSound() |
3. Altri esempi dal mondo reale
Figure geometriche
Le gerarchie di classi si prestano benissimo a modellare la geometria.
// Classe base
class Shape {
void draw() {
System.out.println("Disegniamo una figura.");
}
}
// Cerchio
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void draw() {
System.out.println("Disegniamo un cerchio di raggio " + radius);
}
}
// Rettangolo
class Rectangle extends Shape {
double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
void draw() {
System.out.println("Disegniamo un rettangolo " + width + "x" + height);
}
}
Uso:
public class ShapeDemo {
public static void main(String[] args) {
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.draw(); // Disegniamo un cerchio di raggio 5.0
s2.draw(); // Disegniamo un rettangolo 3.0x4.0
}
}
Trasporti
class Vehicle {
void move() {
System.out.println("Il veicolo si muove.");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("L'auto viaggia su strada.");
}
}
class Bicycle extends Vehicle {
@Override
void move() {
System.out.println("Il ciclista pedala.");
}
}
main:
Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();
v1.move(); // L'auto viaggia su strada.
v2.move(); // Il ciclista pedala.
Utenti
class User {
String username;
User(String username) { this.username = username; }
void login() { System.out.println(username + " ha effettuato l'accesso al sistema."); }
}
class Admin extends User {
Admin(String username) { super(username); }
void deleteUser(String user) {
System.out.println(username + " ha eliminato l'utente " + user);
}
}
class Customer extends User {
Customer(String username) { super(username); }
void buy() { System.out.println(username + " ha effettuato un acquisto."); }
}
4. Sfumature utili
Non abusate dell’ereditarietà. L’ereditarietà è uno strumento per relazioni «is-a». Se volete dire «Il gatto è un Animale», usate l’ereditarietà. Se invece «Il gatto contiene una Coda», usate la composizione (has-a).
Male:
class Engine { /* ... */ }
class Car extends Engine { /* Un'auto è un motore? No, questa è composizione! */ }
Bene:
class Car {
Engine engine; // L'auto contiene un motore
}
Non create gerarchie troppo profonde. Più livelli ci sono — più è complesso mantenere e comprendere il codice. Di solito 2–3 livelli sono il massimo per la maggior parte dei casi.
Non create gerarchie troppo «piatte». Se avete 20 classi che ereditano tutte direttamente da una sola base, forse conviene rivedere l’architettura.
Ereditarietà «per comodità». A volte viene voglia di «prendere in prestito» metodi o campi senza pensare alla logica. È la via verso il caos! Se class A e class B non sono logicamente correlate, non fate class B extends A solo per un paio di metodi.
Violazione del principio di sostituzione di Liskov. Se una sottoclasse non può essere usata al posto del genitore senza sorprese — la gerarchia è stata costruita male.
Duplicazione del codice. Se vi accorgete che «questo metodo viene copiato da una classe all’altra», forse è il momento di portarlo nella classe base.
5. Errori tipici nella costruzione di gerarchie di classi
Errore n. 1: Ereditarietà senza la logica «is-a».
Se usate l’ereditarietà solo per accedere a metodi o campi, e non perché la sottoclasse «è davvero» il genitore, il vostro codice diventerà presto un pasticcio. Ad esempio, «L’auto eredita da Motore» — non è is-a, ma has-a.
Errore n. 2: Ignorare le caratteristiche uniche della sottoclasse.
Se tutte le vostre sottoclassi sembrano uguali e non aggiungono nulla di nuovo, probabilmente la gerarchia non serve — usate una sola classe.
Errore n. 3: Duplicare codice nelle sottoclassi.
Se copiate la stessa implementazione in più sottoclassi, conviene spostarla nella classe genitore.
Errore n. 4: Gerarchia troppo complessa o profonda.
Gerarchie con molti livelli sono difficili da mantenere e da testare. Più semplice è — meglio è!
Errore n. 5: Ridefinire metodi senza l’annotazione @Override.
Senza annotazione è facile sbagliare la firma del metodo, e allora il metodo non verrà ridefinito ma considerato nuovo. Usate sempre @Override!
GO TO FULL VERSION