HashSet è un Set realizzato internamente come tabella hash.
Utilizza il metodo hashCode della classe Object per selezionare il bucket in cui posizionare un elemento.
Per ulteriori dettagli sulle tabelle hash, si consulti un testo di algoritmi e strutture dati.
I principali metodi di HashSet hanno la seguente complessità media:
size O(1)
isEmpty O(1)
add O(1)
contains O(1)
remove O(1)
Tuttavia, le prestazioni reali dipendono dalla bontà della funzione hash utilizzata.
Come previsto dall’interfaccia Set, HashSet utilizza il metodo equals per identificare gli elementi.
Quindi, equals ed hashCode devo rispettare la seguente regola di coerenza.
Per ogni coppia di elementi x ed y
se x.equals(y) è vero allora x.hashCode() == y.hashCode()
Si noti che le versioni di equals ed hashCode presenti in Object rispettano tale regola.
Infatti, se x.equals(y) è vero, allora x ed y puntano al medesimo oggetto, e quindi x ed y hanno lo stesso hashCode.
Esaminiamo il caso di una classe che non rispetta la regola di coerenza tra equals ed hashCode.
Supponiamo che una classe Employee ridefinisca equals in modo che confronti il nome degli impiegati, ma non ridefinisca hashCode di conseguenza.
Dati due oggetti Employee x ed y con lo stesso nome, essi risulteranno uguali secondo equals, ma avranno codici hash diversi.
Se inseriamo l’oggetto x in un HashSet:
Set s = new HashSet<Employee>();
s.add(x);
una successiva chiamata a s.contains(y) potrebbe restituire “false”!
Infatti, il codice hash di y potrebbe indurre la struttura dati a cercare l’oggetto in questione in un bucket diverso da quello in cui è stato inserito x.
Perché HashSet funzioni in maniera efficiente, ed in particolare perché le operazioni principali abbiano complessità costante, è necessario che la classe componente (cioè, la classe degli elementi contenuti) disponga di un opportuno metodo hashCode.
Senza entrare nel dettaglio della teoria delle funzioni di hash, è importante che il metodo hashCode assegni ad ogni oggetto un numero intero il più possibile uniformemente distribuito.
Ad esempio, supponiamo che la classe Employee disponga dei campi name (String) e salary (int), e che siano considerati uguali gli impiegati che hanno questi due campi uguali.
Il metodo hashCode dovrebbe restituire un intero derivato dai valori dei due campi.
Per i tipi non numerici, come le stringhe, conviene partire dal loro codice hash.
Poi, si devono combinare gli interi ottenuti dai vari campi in un unico numero, che verrà restituito.
Un modo comune di combinare due interi, senza correre il rischio di andare in overflow, è rappresentato dall’or esclusivo (XOR) bit a bit, eseguito in Java dall’operatore “^”.
Quindi, per l’esempio in questione, si potrebbe procedere come segue:
public int hashCode
()
{
return name.hashCode() ^ salary;
}
Un altro problema che affligge sia TreeSet che HashSet riguarda la mutabilità degli elementi.
Gli elementi vengono inseriti in queste strutture dati in base al valore dei loro campi:
Se un elemento viene modificato dopo essere stato inserito in una di queste strutture dati, il posizionamento di quell’elemento non corrisponderà più al valore dei suoi campi.
In questo caso, la struttura dati si comporterà in modo inatteso.
Ad esempio, supponiamo che la classe Employee ridefinisca sia equals sia hashCode, in modo che operino sulla stringa che rappresenta il nome dell’impiegato.
Inseriamo l’impiegato x in un HashSet:
Set s = new HashSet<
Employee>
();
Employee x = new Employee("Mario");
s.add(x);
se il nome di x viene modificato:
x.setName("Luigi");
una successiva chiamata a s.contains(x) potrebbe restituire “false”!
Si implementi la classe Container, che rappresenta un contenitore per liquidi di dimensione fissata.
Ad un contenitore, inizialmente vuoto, si può aggiungere acqua con il metodo addWater, che prende come argomento il numero di litri.
Il metodo getAmount restituisce la quantità d’acqua presente nel contenitore.
Il metodo connect prende come argomento un altro contenitore, e lo collega a questo con un tubo. Dopo il collegamento, la quantità d’acqua nei due contenitori (e in tutti quelli ad essi collegati) sarà la stessa.
Esempio d’uso (l’output è sulla slide successiva):
Container a=new Container(), b=new Container(), c=new Container(), d=new Container();
a.addWater(12);
d.addWater(8);
a.connect(b);
System.out.println(a.getAmount()+" "+b.getAmount()+" "+c.getAmount()+" "+d.getAmount());
b.connect(c);
System.out.println(a.getAmount()+" "+b.getAmount()+" "+c.getAmount()+" "+d.getAmount());
c.connect(d);
System.out.println(a.getAmount()+" "+b.getAmount()+" "+c.getAmount()+" "+d.getAmount());
Output dell’esempio d’uso:
6.0 6.0 0.0 8.0
4.0 4.0 4.0 8.0
5.0 5.0 5.0 5.0
List rappresenta una sequenza di elementi, ovvero un vettore.
La figura a destra rappresenta l’interfaccia List e le sue implementazioni.
A differenza di Set, l’interfaccia List aggiunge alcuni metodi a Collection.
Qui, presentiamo solo i due metodi responsabili per l’accesso posizionale:
Analizzeremo le due principali classi che implementano List: LinkedList ed ArrayList.
La versione grezza di LinkedList è stata già presentata nella lezione dedicata agli iteratori.
Ricordiamo solamente che essa offre i seguenti metodi, in aggiunta a quelli di List, qui presentati nella loro versione parametrica (vedi figura).
Questi metodi permettono di utilizzare una LinkedList sia come stack sia come coda.
Per ottenere il comportamento di uno stack (detto LIFO: last in first out), inseriremo ed estrarremo gli elementi dalla stessa estremità della lista:
Per ottenere, invece, il comportamento di una coda (FIFO: first in first out), inseriremo ed estrarremo gli elementi da due estremità opposte.
ArrayList è un’implementazione di List, realizzata internamente con un array di dimensione dinamica.
Ovvero, quando l’array sottostante è pieno, esso viene riallocato con una dimensione maggiore, e i vecchi dati vengono copiati nel nuovo array:
Il ridimensionamento avviene in modo che l’operazione di inserimento (add) abbia complessità ammortizzata costante:
L’accesso posizionale (metodi get e set) si comporta in maniera molto diversa in LinkedList rispetto ad ArrayList.
In LinkedList, ciascuna operazione di accesso posizionale può richiedere un tempo proporzionale alla lunghezza della lista:
In ArrayList, ogni operazione di accesso posizionale richiede tempo costante.
Pertanto, è fortemente sconsigliato utilizzare l’accesso posizionale su LinkedList.
Se l’applicazione richiede l’accesso posizionale, è opportuno utilizzare un semplice array, oppure la classe ArrayList.
Il fatto che l’accesso posizionale sia indicato per ArrayList e sconsigliato per LinkedList è anche segnalato dal fatto che, delle due, solo ArrayList implementa l’interfaccia RandomAccess.
RandomAccess è una interfaccia “di tag”, come Cloneable
4. Risoluzione dell'overloading e dell'overriding
5. Controllo di uguaglianza tra oggetti
6. Classi interne, locali ed anonime
7. Iteratori, teoria e pratica
8. Clonazione di oggetti. Confronto tra oggetti.
9. Elementi di programmazione di interfacce grafiche
10. Il paradigma Model-View-Controller. Il pattern Strategy
11. I pattern Composite e Decorator
12. I pattern Template Method e Factory Method
13. Classi e metodi parametrici
14. La libreria Java Collection Framework: le interfacce Iterable, ...
15. La libreria Java Collection Framework: la classe HashSet e le l...
16. Parametri di tipo con limiti
17. L'implementazione della programmazione generica: la cancellazio...
18. La riflessione
19. Introduzione al multi-threading
22. Classi enumerate