Lezione 9 · Canale 2 · martedì 13 ottobre 2026
Classi astratte, interfacce, Object ed equals
Progettazione del Software
Classi astratte
Una classe astratta rappresenta un concetto generale che nel programma si manifesta attraverso casi concreti. Si dichiara con abstract, non si istanzia con new e serve da base per le sottoclassi.
Può avere metodi astratti: hanno la segnatura, finiscono con ; e non hanno corpo. Ogni sottoclasse concreta deve ridefinirli con l'overriding.
Di ogni figura interessano colore, perimetro e area. Il colore è comune e si scrive una volta; perimetro e area esistono per ogni figura ma si calcolano in modo diverso, quindi restano astratti:
// figure/Figura.java
package figure;
abstract public class Figura {
private String colore;
public Figura(String c) { colore = c; }
public Figura() { this("bianco"); }
public String getColore() { return colore; }
abstract public double getPerimetro();
abstract public double getArea();
}
Una classe astratta e le sue figure
Ogni sottoclasse aggiunge i propri dati e definisce i due metodi. Il costruttore senza super(...) esplicito chiama quello senza argomenti di Figura, quindi la figura è bianca:
// figure/Cerchio.java
package figure;
public class Cerchio extends Figura {
private double raggio;
public Cerchio(String c, double r) {
super(c);
raggio = r;
}
public Cerchio(double r) {
raggio = r; // super() implicito: colore bianco
}
public double getPerimetro() { return 2 * Math.PI * raggio; }
public double getArea() { return Math.PI * raggio * raggio; }
}
Rettangolo ha base, altezza e gli stessi due costruttori, con e senza colore. Un Quadrato può derivare da Rettangolo, perché è un rettangolo con base uguale all'altezza, e ne eredita perimetro e area.
Triangolo ha i tre lati l1, l2, l3. L'area usa la formula di Erone:
// figure/Triangolo.java
package figure;
public class Triangolo extends Figura {
private double l1, l2, l3;
public Triangolo(String c, double l1, double l2, double l3) {
super(c);
this.l1 = l1;
this.l2 = l2;
this.l3 = l3;
}
public Triangolo(double l1, double l2, double l3) {
this.l1 = l1;
this.l2 = l2;
this.l3 = l3;
}
public double getPerimetro() { return l1 + l2 + l3; }
public double getArea() {
double sp = getPerimetro() / 2;
return Math.sqrt(sp * (sp - l1) * (sp - l2) * (sp - l3));
}
}
Il codice non controlla che i tre lati formino davvero un triangolo: il controllo si può aggiungere.
Funzioni polimorfe
Una funzione cliente riceve un array di figure e restituisce la somma dei perimetri. Usa solo i metodi di Figura e non nomina nessuna sottoclasse:
// servizifigure/ServiziFigure.java
package servizifigure;
import figure.*;
public class ServiziFigure {
public static double perimetroComplessivo(Figura[] a) {
double ris = 0;
for (int i = 0; i < a.length; i++)
ris = ris + a[i].getPerimetro();
return ris;
}
}
// Main.java
Figura r = new Rettangolo(10, 10);
Figura t = new Triangolo(10, 10, 10);
Figura c = new Cerchio(10);
Figura[] a = new Figura[3];
a[0] = r;
a[1] = t;
a[2] = c;
System.out.println(ServiziFigure.perimetroComplessivo(a));
La chiamata a[i].getPerimetro() è ammessa perché il tipo statico Figura ha quel metodo; il corpo si sceglie a runtime in base al tipo dinamico dell'oggetto. La stampa è la somma , circa .
La funzione è polimorfa: la stessa chiamata si comporta in modo diverso secondo l'oggetto concreto, grazie all'overriding e al late binding. Un esagono regolare si aggiunge scrivendo la sua classe, senza toccare la funzione. Senza polimorfismo servirebbe una catena di if sul tipo, da aggiornare a ogni nuova figura. Così il codice è più leggibile, riusabile ed estendibile.
Interfacce
Un'interfaccia è l'astrazione di un insieme di funzioni pubbliche, di cui si dà la segnatura e non il corpo. Una classe la implementa definendo tutte le funzioni previste. Nella forma base non ha campi dati (tranne costanti statiche) e tutte le sue funzioni sono astratte e implicitamente pubbliche.
public interface Confrontabile {
boolean maggiore(Confrontabile c);
boolean paritetico(Confrontabile c);
}
maggiore dice se l'oggetto di invocazione è maggiore dell'altro, paritetico se i due sono equivalenti. Un Edificio li implementa confrontando le altezze:
public class Edificio implements Confrontabile {
protected int altezza;
public Edificio(int a) { altezza = a; }
public boolean maggiore(Confrontabile e) {
if (e != null && e instanceof Edificio)
return altezza > ((Edificio) e).altezza;
else return false;
}
public boolean paritetico(Confrontabile e) {
if (e != null && e instanceof Edificio)
return altezza == ((Edificio) e).altezza;
else return false;
}
}
Le funzioni dell'interfaccia sono pubbliche, quindi la classe le scrive public: un overriding può solo allargare l'accesso. Il parametro resta Confrontabile perché la segnatura deve essere quella dell'interfaccia, e per leggere altezza serve il cast a Edificio. Prima del cast, instanceof ne verifica il tipo.
Persona ha la stessa struttura: confronta l'età eta, che espone con getEta().
Un'interfaccia si usa come tipo di un parametro, di una variabile o di un array. Non si creano oggetti dell'interfaccia, ma riferimenti a oggetti delle classi che la implementano. Quindi una funzione polimorfa può lavorare con classi che ancora non esistono:
public class ServiziConfrontabile {
static public Confrontabile MaggioreTraTre(Confrontabile c1,
Confrontabile c2,
Confrontabile c3) {
if ((c1.maggiore(c2) || c1.paritetico(c2)) &&
(c1.maggiore(c3) || c1.paritetico(c3)))
return c1;
else if ((c2.maggiore(c1) || c2.paritetico(c1)) &&
(c2.maggiore(c3) || c2.paritetico(c3)))
return c2;
else return c3;
}
}
// Main.java
Persona p1 = new Persona(30);
Persona p2 = new Persona(35);
Persona p3 = new Persona(32);
Edificio e1 = new Edificio(12);
Edificio e2 = new Edificio(5);
Edificio e3 = new Edificio(100);
Persona pp = (Persona) ServiziConfrontabile.MaggioreTraTre(p1, p2, p3);
Edificio ee = (Edificio) ServiziConfrontabile.MaggioreTraTre(e1, e2, e3);
Il risultato ha tipo statico Confrontabile, quindi il cliente lo converte nel tipo che gli serve. Qui pp è la persona di età 35 ed ee l'edificio di altezza 100.
Classe astratta o interfaccia. Una classe astratta astrae un insieme di oggetti simili per natura, quelli delle sue sottoclassi: risponde a «che cosa è?». Un'interfaccia astrae un insieme di funzionalità: risponde a «che cosa sa fare?». Pensare Confrontabile come l'astrazione di persone ed edifici è una forzatura.
Una classe ha una sola base, ma può implementare più interfacce, elencate dopo implements e separate da virgole: le interfacce si prestano alle capacità trasversali. La classe definisce le funzioni di tutte. Un'interfaccia può estenderne una o più: chi la implementa definisce anche le funzioni delle interfacce estese.
public interface I { void g(); }
public interface J { void h(); }
public interface M extends I, J { void k(); }
public class C implements M {
public void g() { }
public void h() { }
public void k() { }
}
Per le interfacce l'ereditarietà multipla è ammessa, per le classi no. Un'interfaccia nella forma base dà solo segnature, senza corpi: fra più versioni non c'è nulla da scegliere, quindi nessuna ambiguità.
La classe Object
Tutte le classi Java derivano implicitamente da Object, direttamente o attraverso la propria classe base. Ogni oggetto è quindi un'istanza di Object e ne eredita le funzioni: toString(), getClass(), equals(Object o), clone(), hashCode().
toString() associa all'oggetto una stringa stampabile, e println lo chiama. Senza overriding stampa il nome della classe, @ e un codice esadecimale che può cambiare fra le esecuzioni (per esempio B@601bb1). Con l'overriding stampa una descrizione utile. Una derivata riusa quella della base, anche se i è privato e D non lo raggiunge:
public class B {
private int i;
public B(int x) { i = x; }
public String toString() { return "i: " + i; }
}
public class D extends B {
private int j;
public D(int x, int y) {
super(x);
j = y;
}
public String toString() {
return super.toString() + " j: " + j;
}
}
System.out.println(new B(5)) stampa i: 5, e System.out.println(new D(5, 10)) stampa i: 5 j: 10. Se la descrizione di B cambia, D la segue.
La classe Class
Per ogni classe o interfaccia del programma la JVM ha un oggetto della classe Class, che la rappresenta a runtime. Si indica con il letterale C.class o con un riferimento Class c. Gli oggetti Class li genera la JVM: Class non ha costruttori accessibili.
boolean isInstance(Object o) è vero se o è un'istanza della classe o implementa l'interfaccia rappresentata. Un oggetto della derivata è anche istanza della base, non il contrario:
// D extends B
B b1 = new B();
D d1 = new D();
B.class.isInstance(b1); // true
B.class.isInstance(d1); // true
D.class.isInstance(d1); // true
D.class.isInstance(b1); // false
Il controllo riguarda l'oggetto, non il riferimento. Con D implements I, un oggetto D referenziato da una variabile di tipo I è comunque istanza di I e di D:
I i1 = new D();
I.class.isInstance(i1); // true
D.class.isInstance(i1); // true
L'operatore c instanceof C equivale a C.class.isInstance(c), ma usa il nome della classe, che deve essere noto in compilazione. Se la classe si sceglie a runtime, come in b.isInstance(c) con b di tipo Class, instanceof non basta.
getClass(), ereditato da Object, restituisce l'oggetto Class della classe più specifica dell'oggetto, qualunque sia il riferimento. instanceof accetta anche i sottotipi, mentre getClass() permette di confrontare la classe esatta.
Uguaglianza fra oggetti
Due banconote da 10 euro hanno lo stesso valore, ma sono due pezzi di carta distinti. Allo stesso modo due oggetti si confrontano in due sensi:
- uguaglianza superficiale: due riferimenti denotano lo stesso oggetto;
- uguaglianza profonda: le informazioni rilevanti contenute negli oggetti coincidono.
Con C c1 = new C(4, 5); C c2 = new C(4, 5);, dove C ha i campi interi x e y, i due oggetti non sono uguali superficialmente ma lo sono profondamente. equals, ereditato da Object, se non ridefinito si comporta come ==, quindi c1.equals(c2) dà false. Per l'uguaglianza profonda si ridefinisce equals secondo uno schema:
public class B {
private int x, y;
public boolean equals(Object o) {
if (o != null && getClass().equals(o.getClass())) {
B b = (B) o;
return (x == b.x) && (y == b.y);
}
else return false;
}
}
- Il parametro è
Object: così si ridefinisce l'equalsdiObject. - Se
oènullnon è un oggetto, e l'uguaglianza è falsa. - Se le classi sono diverse, gli oggetti sono diversi. Superato il controllo, il cast a
Bè sicuro e si leggono i campi. - I campi primitivi si confrontano con
==, quelli che sono oggetti conequals.
Il controllo usa getClass() e non instanceof perché equals deve essere simmetrico: se a.equals(b) allora b.equals(a). Con instanceof un oggetto della base può risultare uguale a uno della derivata, ma non viceversa. Con getClass() oggetti di classi diverse non sono mai uguali.
Overloading accidentale. Dichiarare equals(B b) crea un overloading, non un overriding: B ha due metodi, il nuovo e quello di Object. Quale dei due parte lo decide il compilatore dai tipi statici, e i risultati sono controintuitivi:
public class B {
private int x, y;
public B(int a, int b) { x = a; y = b; }
public boolean equals(B b) { // overloading, non overriding
return b != null && b.x == x && b.y == y;
}
}
static void stampaUguali(Object o1, Object o2) {
if (o1.equals(o2)) System.out.println("UGUALI");
else System.out.println("DIVERSI");
}
// main
B b1 = new B(10, 20);
B b2 = new B(10, 20);
if (b1.equals(b2)) System.out.println("UGUALI"); // stampa UGUALI
else System.out.println("DIVERSI");
stampaUguali(b1, b2); // stampa DIVERSI
b1.equals(b2) sceglie equals(B) e confronta i campi. In stampaUguali i tipi statici sono Object, quindi parte l'equals di Object, che B non ridefinisce e che confronta i riferimenti.
Formulario
Classe astratta
abstract: nientenew- metodo astratto: senza corpo
- le concrete lo ridefiniscono
Interfaccia
- funzioni pubbliche e astratte
implements I, Jextends I, Jfra interfacce
Object
toString(): testogetClass(): classe esattaC.class: oggettoClass
Tipo di un oggetto
c instanceof C- uguale a
C.class.isInstance(c)
equals
- ereditato: come
== - parametro
Object getClass()uguale, poi i campi- mai overloading