Lezione 6 · Canale 1 · lunedì 5 ottobre 2026
Identità e uguaglianza, classe Object e schema UML dei dati
Progettazione del Software
Metodi scritti per essere chiamati da altri
Spesso un metodo lo chiama un'altra parte del programma, non il main, e si aspetta di trovarlo con un'intestazione precisa.
| Metodo | Chi lo chiama |
|---|---|
| costruttore | new, dopo aver creato l'oggetto |
toString | println, per stampare un oggetto |
equals | contains, per confrontare gli elementi |
Un punto del piano ha due coordinate, fissate dal costruttore: dopo la creazione non cambiano.
public class Punto {
private double x, y;
public Punto(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public String toString() {
return "(" + x + ", " + y + ")";
}
}
Con questo toString, println stampa (3.0, 2.0). Quello generato dall'ambiente di sviluppo scrive anche i nomi dei campi, Punto [x=3.0, y=2.0]: meno leggibile.
Provare includeOrigine
includeOrigine dice se una lista di punti contiene l'origine:
static boolean includeOrigine(LinkedList<Punto> l) {
for (Punto p : l)
if (p.getX() == 0 && p.getY() == 0)
return true;
return false;
}
Il ciclo si può lasciare alla libreria: return l.contains(new Punto(0, 0));. Per una collezione e un non nullo, contains è definito così:
Un metodo booleano si prova con almeno un caso true e uno false: con un solo caso, un metodo che risponde sempre allo stesso modo supera la prova. Si prova senza l'origine e poi con:
LinkedList<Punto> l = new LinkedList<Punto>();
l.add(new Punto(3, 2));
l.add(new Punto(3, 1));
System.out.println(includeOrigine(l)); // false
l.add(new Punto(0, 0));
System.out.println(includeOrigine(l)); // true
Con Punto così com'è, il ciclo dà false e true, mentre contains dà false e false. La prima prova non distingue le due versioni, la seconda sì.
Identità e uguaglianza
Ogni new crea un oggetto nuovo: due new Punto(0, 0) danno due oggetti distinti con le stesse coordinate.
Punto p = new Punto(0, 0);
Punto q = new Punto(0, 0);
Punto r = p;
// p == q: false p == r: true
Sono due nozioni diverse:
- identità (uguaglianza superficiale): due riferimenti denotano lo stesso oggetto, e
a == blo verifica. - uguaglianza (profonda): le informazioni che contano sono le stesse (quali contano lo stabilisce la classe), e
a.equals(b)lo verifica.
Il criterio predefinito è l'identità: l'equals di Object confronta i riferimenti. Perciò contains non trova l'origine: riceve un oggetto appena creato, che non può già stare nella lista. Nel diagramma della memoria un oggetto si identifica con il suo indirizzo.
Oggetti in memoria durante contains
Il criterio giusto dipende dal problema. Due punti con le stesse coordinate sono lo stesso punto geometrico. Due persone con lo stesso nome e cognome possono essere persone diverse: gli stessi dati non bastano a dirle uguali.
Object, equals e cast
Una classe che non dichiara extends è sottoclasse di Object: class Punto equivale a class Punto extends Object. Object è la radice: ogni classe ne eredita i metodi, fra cui toString, equals e hashCode.
Per far trovare l'origine a contains, Punto ridefinisce equals, con l'intestazione esatta public boolean equals(Object o), perché contains chiama quel metodo. Una variabile Object può riferire oggetti di qualsiasi classe, e il compilatore guarda il tipo dichiarato: o.x non compila, perché non tutti gli oggetti hanno x. Serve un cast:
public boolean equals(Object o) {
if (o != null && getClass().equals(o.getClass())) {
Punto p = (Punto) o;
return x == p.x && y == p.y;
}
return false;
}
Il cast non crea né converte l'oggetto: p e o riferiscono lo stesso oggetto, visto con il tipo Punto. Il controllo prima del cast serve perché equals può ricevere null o un oggetto di un'altra classe: senza il controllo, con null darebbe errore l'accesso a p.x, con un'altra classe il cast. In entrambi i casi la risposta è false.
Ora la seconda prova dà true: contains confronta con il nuovo equals il punto cercato e ogni elemento, e con le coordinate coincidono. Due punti sono uguali se coincidono entrambe:
Confrontare solo renderebbe uguali due punti con diversa.
equals(Punto p)sarebbe un altro metodo con lo stesso nome (overloading), non la ridefinizione:containschiama ancora l'equals(Object)diObjecte dàfalse.- Un
HashSetusa anchehashCode, che va ridefinito in modo coerente: oggetti uguali devono avere lo stessohashCode. Con il soloequals, il suocontainspuò darefalse. Per questo l'esempio usa unaLinkedList, che si affida al soloequals.
Dalla specifica allo schema UML dei dati
Le classi si scrivono guardando solo lo schema UML dei dati, senza tornare alla specifica: nei casi semplici lo schema la rappresenta per intero. Ciò che non rappresenta si annota a parte.
Il caso è un'applicazione per pazienti, medici, visite e reparti, le classi che la specifica elenca. In uno schema dei dati ogni rettangolo è una classe, cioè un insieme di oggetti, non un oggetto come nei diagrammi della memoria. Il nome sta sopra la linea, i campi sotto.
Medici e pazienti sono persone: c'è quindi la superclasse Persona, con nome, cognome e nascita. Paziente e Medico sono sue sottoclassi, cioè tipi particolari di persona: i tre campi valgono per tutte le persone e non si ripetono. Medico aggiunge la specializzazione. Paziente non aggiunge campi: si distingue dal medico perché prenota visite, cioè per un'associazione. Di Visita interessa il codice. Reparto è disegnato senza campi, ma non può restarne privo: i suoi vanno ancora definiti.
Il criterio è l'inclusione: extends vuol dire «sottoinsieme di», non «ha più componenti». Un punto del piano è un punto dello spazio con terza coordinata nulla: la classe dei punti del piano è sottoclasse di quella dei punti dello spazio, pur avendo meno coordinate. Il contrario non vale.
Molteplicità
Il numero accanto a una classe dice a quanti suoi oggetti è legato ogni oggetto dell'altra: si legge dal lato opposto. Leggerlo dal lato della propria classe è un errore frequente.
L'associazione prenota lega Paziente e Visita. Ogni visita è prenotata da un solo paziente: 1 accanto a Paziente. Un paziente ne prenota quante vuole, senza limite: 0..* accanto a Visita, che si può omettere.
L'associazione assegnato lega Medico e Visita. Una visita appena prenotata non ha un medico, e in seguito ne ha al massimo uno: vale 0..1 e non 1, perché la visita può essere non assegnata o assegnata. A un medico si assegnano al massimo ottanta visite e nessun minimo: 0..80. L'associazione in lega Visita e Reparto: ogni visita è sempre e soltanto in un reparto, quindi 1, non 0..1.
Schema dei dati di pazienti, medici e visite
Versi delle associazioni
Un'associazione si può percorrere in due versi, e lo schema non dice quali servono. Di solito interessano le visite di un paziente e il paziente di una visita: due versi.
A volte è un requisito a imporre un verso. Deve essere possibile implementare un metodo che disassegna tutte le visite di un medico: per disassegnarle bisogna sapere quali sono, quindi dal medico si deve risalire alle sue visite, e Medico contiene l'insieme delle visite assegnate.
La prima implementazione è semplificata: un collegamento è un campo, un riferimento o un insieme, e nulla garantisce che i due versi di un legame coincidano. Un paziente può prenotare per errore una visita già prenotata da un altro, o una visita può essere assegnata a un medico senza comparire fra le sue visite. Una versione più elaborata evita o rileva questi errori.
La classe Medico
Dallo schema si scrive una bozza di Medico: dati ereditati, specializzazione e insieme delle visite assegnate.
public class Persona {
private String nome, cognome;
private int nascita;
public Persona(String nome, String cognome, int nascita) {
this.nome = nome;
this.cognome = cognome;
this.nascita = nascita;
}
public String getNome() { return nome; }
public String getCognome() { return cognome; }
public int getNascita() { return nascita; }
}
import java.util.HashSet;
public class Medico extends Persona {
private String specializzazione;
private HashSet<Visita> assegnato;
public Medico(String nome, String cognome, int nascita,
String specializzazione) {
super(nome, cognome, nascita);
this.specializzazione = specializzazione;
this.assegnato = new HashSet<Visita>();
}
public String toString() {
return getNome() + " " + getCognome() + ", " + getNascita()
+ ", " + specializzazione;
}
}
Il codice generato dall'ambiente di sviluppo va controllato. Se Persona non ha costruttori, Java le dà quello senza parametri, e il costruttore generato per Medico chiama super() e riceve solo la specializzazione: nome, cognome e nascita non si possono passare e restano null e 0. Si scrive prima il costruttore di Persona, poi si genera quello di Medico, con i campi propri ed ereditati.
La stampa di un medico deve comprendere anche i dati ereditati: il toString generato, senza di essi, stampa solo la specializzazione. Va completato e reso più sintetico, senza i nomi dei campi.
Persona si dichiara abstract se ogni persona è un medico o un paziente: con public abstract class Persona, new Persona(...) diventa un errore. Altri tipi di persona, come infermieri o impiegati, sarebbero altre sottoclassi.
Formulario
Identità e uguaglianza
a == b: stesso oggettoa.equals(b): lo decide la classenewcrea sempre un oggetto nuovoObject:equalscome==
Collezioni
c.contains(o) è true se
- provare un caso
truee unofalse
equals
- intestazione
equals(Object o) nullo altra classe:false- cast: stesso oggetto, altro tipo
- confrontare i campi che decidono
HashSet: anchehashCode
Schema UML dei dati
- rettangolo: una classe
- triangolo: verso la superclasse
extends: sottoinsieme diabstract: nientenew
Molteplicità
- si legge dal lato opposto
1: esattamente uno0..1: nessuno o uno0..*: nessun limite, si omette
Versi delle associazioni
- due versi, se servono
- un requisito può imporli
- campo: riferimento o insieme