Java HashSet contiene duplicati se l’elemento contenuto viene modificato

Supponiamo che tu abbia una class e tu crei un HashSet che può memorizzare questa istanza di questa class. Se si tenta di aggiungere istanze uguali, nella raccolta viene mantenuta una sola istanza e ciò va bene.

Tuttavia, se hai due istanze diverse in HashSet e ne prendi una e ne fai una copia esatta dell’altro (copiando i campi), HashSet conterrà quindi due istanze duplicate.

Ecco il codice che lo dimostra:

public static void main(String[] args) { HashSet set = new HashSet(); GraphEdge edge1 = new GraphEdge(1, "a"); GraphEdge edge2 = new GraphEdge(2, "b"); GraphEdge edge3 = new GraphEdge(3, "c"); set.add(edge1); set.add(edge2); set.add(edge3); edge2.setId(1); edge2.setName("a"); for(GraphEdge edge: set) { System.out.println(edge.toString()); } if(edge2.equals(edge1)) { System.out.println("Equals"); } else { System.out.println("Not Equals"); } } public class GraphEdge { private int id; private String name; //Constructor ... //Getters & Setters... public int hashCode() { int hash = 7; hash = 47 * hash + this.id; hash = 47 * hash + Objects.hashCode(this.name); return hash; } public boolean equals(Object o) { if(o == this) { return true; } if(o instanceof GraphEdge) { GraphEdge anotherGraphEdge = (GraphEdge) o; if(anotherGraphEdge.getId() == this.id && anotherGraphEdge.getName().equals(this.name)) { return true; } } return false; } } 

L’output dal codice precedente:

 1 a 1 a 3 c Equals 

Esiste un modo per forzare HashSet a convalidarne il contenuto in modo che vengano rimosse possibili voci duplicate create come nello scenario precedente?

Una ansible soluzione potrebbe essere quella di creare un nuovo HashSet e copiare il contenuto da un hashset a un altro in modo che il nuovo hashset non contenga duplicati, tuttavia non mi piace questa soluzione.

La situazione che descrivi non è valida. Vedere Javadoc : “Il comportamento di un set non viene specificato se il valore di un object viene modificato in un modo che influisce su confronti uguali mentre l’object è un elemento nel set.”

Per aggiungere alla risposta di @ EJP, ciò che accadrà nella pratica se metti oggetti in un HashSet per renderli duplicati (nel senso del contratto equals / hashcode ) è che la struttura dei dati della tabella hash si interromperà.

  • A seconda dei dettagli esatti della mutazione e dello stato della tabella hash, una o entrambe le istanze diventeranno invisibili alla ricerca (ad esempio contains e altre operazioni). O si trova sulla catena hash sbagliata o perché l’altra istanza appare prima nella catena hash. Ed è difficile prevedere quale istanza sarà visibile … e se rimarrà visibile.

  • Se si itera il set, entrambe le istanze saranno ancora presenti … in violazione del contratto Set .

Naturalmente, questo è molto rotto dal punto di vista dell’applicazione.


Puoi evitare questo problema:

  • usando un tipo immutabile per i tuoi elementi impostati,
  • fare una copia degli oggetti mentre li metti nel set e / o tirarli fuori dal set,
  • scrivendo il tuo codice in modo che “sappia” di non cambiare gli oggetti per la durata …

Dal punto di vista della correttezza e della robustezza, la prima opzione è chiaramente la migliore.


Per inciso, sarebbe davvero difficile “aggiustarlo” in modo generale. Non c’è alcun meccanismo pervasivo in Java per sapere … o essere avvisati … che qualche elemento è cambiato. È ansible implementare un tale meccanismo su una class per class, ma deve essere codificato esplicitamente (e non sarà economico). Anche se tu avessi un tale meccanismo, cosa faresti? Chiaramente uno degli oggetti dovrebbe ora essere rimosso dal set … ma quale?

Hai ragione e non penso che ci sia un modo per proteggersi dal caso che discuti. Tutte le raccolte che utilizzano hashing e uguali sono soggette a questo problema. La raccolta non ha notificato che l’object è cambiato da quando è stato aggiunto alla raccolta. Penso che la soluzione che hai delineato sia buona.

Se sei così interessato a questo problema, forse hai bisogno di ripensare le tue strutture dati. Potresti usare oggetti immutabili, per esempio. Con oggetti immutabili non avresti questo problema.

HashSet non è a conoscenza delle proprietà dei suoi membri che cambiano dopo che l’object è stato aggiunto. Se questo è un problema per te, allora potresti prendere in considerazione l’ GraphEdge rendere GraphEdge immutabile. Per esempio:

 GraphEdge edge4 = edge2.changeName("new_name"); 

Nel caso in cui GraphEdge sia immutabile, la modifica di un risultato del valore restituisce una nuova istanza piuttosto che la modifica dell’istanza esistente.

Objects.hashCode è pensato per essere utilizzato per generare un hascode utilizzando oggetti parametro. Lo stai utilizzando come parte del calcolo hascode.

Prova a sostituire l’implementazione di hashCode con quanto segue:

 public int hashCode() { return Objects.hashCode(this.id, this.name); } 

Dovrai eseguire il rilevamento univoco al momento della iterazione dell’elenco. Creare un nuovo HashSet potrebbe non sembrare la strada giusta da percorrere, ma perché non provare questo … E forse non usare un HashSet per iniziare con …

 public class TestIterator { public static void main(String[] args) { List list = new ArrayList(); list.add("1"); list.add("1"); list.add("2"); list.add("3"); for (String s : new UniqueIterator(list)) { System.out.println(s); } } } public class UniqueIterator implements Iterable { private Set hashSet = new HashSet(); public UniqueIterator(Iterable iterable) { for (T t : iterable) { hashSet.add(t); } } public Iterator iterator() { return hashSet.iterator(); } }