Lezione 2 · Canale 1 · venerdì 25 settembre 2026

Associazioni e collezioni in Java

Progettazione del Software

Collegare persone e automobili

Di una persona interessano nome, cognome e anno di nascita; di un'automobile targa e numero di telaio. Come si ottengono tutte le automobili di una persona?

Si potrebbero scorrere tutte le automobili create e controllare il proprietario di ciascuna, ma con migliaia di automobili è scomodo. Conviene che Persona conservi la collezione delle sue automobili e che ogni Automobile conservi il riferimento al proprietario.

Lo schema UML dei dati

Prima di scrivere le classi si descrive quali dati servono e come sono collegati. Sono i dati richiesti dall'applicazione, non tutti quelli che persone e automobili hanno nella realtà (cilindrata, stipendio, stato civile).

Personanome: stringacognome: stringanascita: interoAutomobiletarga: stringatelaio: stringapossiede1..10..*
Gli attributi sono i dati semplici; il legame fra persone e automobili è l'associazione «possiede», con una molteplicità a ciascun estremo.
  • Ogni classe è un rettangolo: in alto il nome, sotto gli attributi, cioè i dati semplici.
  • Le automobili di una persona e il proprietario di un'automobile non sono attributi. Li rappresenta l'associazione, la linea fra i rettangoli: ripeterli negli attributi sarebbe ridondante.
  • La molteplicità sta agli estremi della linea. Il numero accanto a una classe dice quante istanze di quella classe sono legate a ogni istanza dell'altra.

Nello schema 1..1 accanto a Persona significa che ogni automobile ha esattamente un proprietario; 0..* accanto a Automobile che una persona possiede da zero a un numero qualsiasi di automobili.

  • * è 0..*, nessun vincolo: si può omettere.
  • 1 è 1..1, esattamente uno.
  • 0..1 è al più uno: il proprietario può mancare. È il caso del codice sotto, dove l'automobile viene creata senza proprietario.
  • In generale x..y: almeno x, al più y.

Lo schema non riguarda Java: si può disegnare prima del programma. Fa capire subito a chi non conosce il problema quali dati ci sono e come si legano.

Le classi Java

Gli attributi diventano campi. L'associazione diventa una collezione in Persona e un riferimento in Automobile. Non è una gerarchia fra classi: è un legame fra oggetti.

import java.util.*;

class Persona {
    String nome;
    String cognome;
    int nascita;
    HashSet<Automobile> auto;

    Persona(String n, String c, int a) {
        nome = n;
        cognome = c;
        nascita = a;
        auto = new HashSet<Automobile>();
    }
}

class Automobile {
    String targa;
    String telaio;
    Persona proprietario;

    Automobile(String t, String f) {
        targa = t;
        telaio = f;
    }
}
  • HashSet sta nella libreria java.util: serve l'import.
  • Dichiarare auto crea solo la variabile. L'insieme, vuoto, lo crea new.
  • Il costruttore è il metodo con lo stesso nome della classe. Quello di Automobile non riceve il proprietario, che si assegna dopo:
Persona p1 = new Persona("nome", "cognome", 2000);
Automobile a = new Automobile("targa", "telaio");
p1.auto.add(a);
a.proprietario = p1;

Le ultime due istruzioni dicono un solo fatto, nei due versi.

Riferimenti e oggetti in memoria

  • Una variabile di tipo classe contiene un riferimento a un oggetto, non l'oggetto. In C la differenza fra struct e struct * si scrive; in Java vale sempre. Solo i tipi primitivi, come int, contengono il valore.
  • new crea l'oggetto, come malloc in C, e il suo riferimento va nella variabile: nell'assegnazione si esegue prima la parte destra. Il programma non maneggia l'indirizzo.
  • null è il riferimento a nessun oggetto. Un campo di tipo classe parte da null; una variabile locale non ha valore finché non si assegna, e il compilatore ne rifiuta la lettura.
  • Anche un HashSet è un oggetto: auto è un riferimento, e l'insieme contiene riferimenti alle automobili, non copie.
  • Più variabili possono riferire lo stesso oggetto: p1 e a.proprietario.
  • Non esiste free: un oggetto che nessuno riferisce più lo recupera il garbage collector.
p1 : Personanome = "nome"cognome = "cognome"nascita = 2000p1.auto : HashSeta : Automobiletarga = "targa"telaio = "telaio"proprietario
Ogni collegamento è un riferimento: l'insieme contiene un riferimento all'automobile, non una copia, e p1 e a.proprietario riferiscono lo stesso oggetto Persona.

Dopo le istruzioni sopra c'è un solo oggetto Persona e un solo oggetto Automobile.

Coerenza dei due versi

Quando l'associazione è memorizzata nei due versi, i collegamenti devono dire la stessa cosa: se a.proprietario è p1, allora a sta in p1.auto.

Se un'automobile passa da p1 a p2 servono tre passi: cambiare il proprietario di a, toglierla dall'insieme di p1, aggiungerla a quello di p2.

a.proprietario = p2;
p1.auto.remove(a);
p2.auto.add(a);

Se ne manca uno i dati si contraddicono:

  • a ha come proprietario p2 ma non è fra le automobili di p2;
  • a è ancora fra le automobili di p1 ma non gli appartiene.

Due strategie:

  • fare gli aggiornamenti dei due collegamenti solo insieme, nello stesso metodo;
  • lasciare gli aggiornamenti liberi e controllare la coerenza quando si interrogano i dati.

Senza private e public, che arrivano più avanti, nulla impedisce di cambiare un solo campo: per ora i due collegamenti si aggiornano sempre insieme.

I due versi servono solo se l'applicazione deve percorrere l'associazione in entrambe le direzioni. Se da una persona basta risalire alla nazione e non serve elencare gli abitanti di una nazione, il collegamento è in un verso solo. Non c'è allora nulla da tenere coerente, e non si memorizzano milioni di collegamenti per nazione.

Insiemi e liste

  • HashSet: l'ordine degli elementi non è garantito. La stampa può sembrare rispettare l'inserimento, ma non ci si può contare. Non esiste un «secondo elemento» e non c'è accesso per indice.
  • Lista: gli elementi si aggiungono in coda e hanno indice da 0. l.get(1) è il secondo elemento; l[1] non compila, perché l è un oggetto e non un array.
  • Una lista permette di inserire in posizione intermedia, cosa che un array non fa: l.add(1, "nuovo") mette "nuovo" all'indice 1 e sposta di uno gli elementi che seguivano.
  • Per scorrere un insieme si usa un ciclo sugli elementi, uno alla volta; lo stesso ciclo vale per le liste.
List<String> l = new LinkedList<String>();
l.add("x");
l.add("y");               // x, y
l.add(1, "nuovo");        // x, nuovo, y
System.out.println(l.get(1));   // nuovo
for (String s : l)
    System.out.println(s.length());

Esercizio: la stringa più lunga

Scrivere un metodo che, data una collezione di stringhe (insieme o lista), restituisce la stringa più lunga, non la sua lunghezza.

static String piuLunga(Collection<String> c) {
    String max = "";
    for (String s : c)
        if (s.length() > max.length())
            max = s;
    return max;
}
  • Si conserva la stringa più lunga trovata finora: la lunghezza non serve salvarla, è max.length().
  • max parte da "" perché una variabile locale deve avere un valore prima dell'uso. La stringa vuota non è null.
  • Collection<String> accetta sia un HashSet sia una lista.

Tipi primitivi nelle collezioni

Una collezione contiene riferimenti a oggetti, un int è un valore. Per questo List<int> non compila: si usa la classe wrapper del tipo, Integer.

List<Integer> l = new LinkedList<Integer>();
l.add(3);
l.add(8);
l.add(2);

Per ogni add Java mette il numero in un oggetto Integer e la lista conserva il suo riferimento.

Ogni tipo primitivo ha il suo wrapper:

  • int: Integer;
  • char: Character;
  • boolean: Boolean;
  • double: Double.

Formulario

Molteplicità

  • il numero accanto a una classe: quante sue istanze per ognuna dell'altra
  • * è 0..*
  • 1 è 1..1
  • 0..1: al più uno
  • x..y: da x a y

Riferimenti

  • variabile di classe: riferimento
  • int: valore
  • null: nessun oggetto
  • new crea l'oggetto
  • niente free: lo fa il garbage collector

Due versi

  • se a.proprietario è p1, allora a sta in p1.auto
  • si aggiornano insieme, nello stesso metodo
p.auto.add(a);
a.proprietario = p;

Collezioni

  • HashSet: niente ordine, niente indice
  • lista: indici da 0
  • l.get(i)
  • l.add(i, x)
for (T x : c)
    ...

Wrapper

  • List<int>: errore
  • List<Integer>: corretto
  • int: Integer
  • char: Character
  • boolean: Boolean