2011-01-17 19 views
17

avere la seguente classe:oggetti mutabili e hashCode

public class Member { 
private int x; 
private long y; 
private double d; 

public Member(int x, long y, double d) { 
    this.x = x; 
    this.y = y; 
    this.d = d; 
} 

@Override 
public int hashCode() { 
    final int prime = 31; 
    int result = 1; 
    result = prime * result + x; 
    result = (int) (prime * result + y); 
    result = (int) (prime * result + Double.doubleToLongBits(d)); 
    return result; 
} 

@Override 
public boolean equals(Object obj) { 
    if (this == obj) { 
     return true; 
    } 
    if (obj instanceof Member) { 
     Member other = (Member) obj; 
     return other.x == x && other.y == y 
       && Double.compare(d, other.d) == 0; 
    } 
    return false; 
} 

public static void main(String[] args) { 
    Set<Member> test = new HashSet<Member>(); 
    Member b = new Member(1, 2, 3); 
    test.add(b); 
    System.out.println(b.hashCode()); 
    b.x = 0; 
    System.out.println(b.hashCode()); 
    Member first = test.iterator().next(); 
    System.out.println(test.contains(first)); 
    System.out.println(b.equals(first)); 
      System.out.println(test.add(first)); 

} 

}

Produce i seguenti risultati:
30814 29853 false true true

Perché la hashCode dipende dello stato dell'oggetto non può più a lungo recuperato correttamente, quindi il controllo per il contenimento fallisce. HashSet non funziona più correttamente. Una soluzione sarebbe quella di rendere i membri immutabili, ma è l'unica soluzione? Tutte le classi aggiunte a HashSet dovrebbero essere immutabili? C'è un altro modo per gestire la situazione?

Saluti.

+0

Perché il compilatore non applica una regola per non consentire gli oggetti mutabili come chiavi nelle tabelle hash o negli oggetti nei set hash? Questo mi sembra orribile. – ncmathsadist

risposta

26

Oggetti in hashsets dovrebbero sia essere immutabili, o è necessario esercitare la disciplina nel non cambiare loro dopo che sono stati utilizzati in una hashset (o HashMap).

In pratica ho riscontrato raramente che questo è un problema - raramente mi ritrovo a dover utilizzare oggetti complessi in quanto le chiavi sono elementi impostati e, quando lo faccio, di solito non è un problema, basta non mutarle. Ovviamente se hai esposto i riferimenti ad altri codici entro questo periodo, può diventare più difficile.

+0

Per oggetti valore. Gli oggetti di riferimento possono essere mutevoli quanto vuoi. –

+0

@Tom Hawtin: Non sarebbe anche pericoloso modificare anche gli oggetti di riferimento. In particolare, parlo di mutazioni che cambieranno l'hashcode dell'oggetto o lo trasformeranno in un membro di una diversa classe di equivalenza. – Brian

+2

@Brian: Sospetto che Tom stia parlando di tipi che non sovrascrivono hashCode o uguale. –

7

Sì. Pur mantenendo la classe mutevole, è possibile calcolare la hashCode ed i metodi equals sulla base di valori immutabili della classe (forse un id generato) per aderire al contratto hashCode definito nella classe Object:

  • Ogni volta che viene invocato sullo stesso oggetto più di una volta durante un'esecuzione di un'applicazione Java, il metodo hashCode deve restituire costantemente lo stesso numero intero, a condizione che nessuna informazione utilizzata nei confronti degli uguali sull'oggetto sia modificata. Questo numero intero non deve rimanere coerente da un'esecuzione di un'applicazione a un'altra esecuzione della stessa applicazione.

  • Se due oggetti sono uguali in base al metodo equals (Object), chiamare il metodo hashCode su ciascuno dei due oggetti deve produrre lo stesso risultato intero.

  • Non è necessario che se due oggetti non sono uguali secondo il metodo equals (java.lang.Object), la chiamata del metodo hashCode su ciascuno dei due oggetti deve produrre risultati interi distinti. Tuttavia, il programmatore dovrebbe essere consapevole del fatto che produrre risultati interi distinti per oggetti non uguali può migliorare le prestazioni degli hashtables.

A seconda della situazione questo può essere più facile o meno.

class Member { 
    private static long id = 0; 

    private long id = Member.id++; 
    // other members here... 


    public int hashCode() { return this.id; } 
    public boolean equals(Object o) { 
     if(this == o) { return true; } 
     if(o instanceOf Member) { return this.id == ((Member)o).id; } 
     return false; 
    } 
    ... 
} 

Se avete bisogno di un attributo thread-safe, si può considerare l'uso: AtomicLong invece, ma ancora una volta, dipende da come hai intenzione di utilizzare l'oggetto.

+0

Il problema con questo approccio è che se inserisco due diverse istanze in un HashSet (ad esempio un nuovo membro (1,2, 3)) verranno inserite due volte. Anche il metodo equals non funzionerà come previsto. Ma grazie per aver sottolineato il contratto hashCode: "Ogni volta che viene invocato sullo stesso oggetto più di una volta durante l'esecuzione di un'applicazione Java, il metodo hashCode deve restituire costantemente lo stesso numero intero, a condizione che nessuna informazione utilizzata nei confronti uguali sull'oggetto sia modificata." Non l'ho letto abbastanza attentamente. – robert

+0

Beh, questo era solo un esempio, è possibile crearlo in questo modo: 'Membro pubblico (int a, int b, int) {this.id = a * 31 + b * 31 + 31 *; } 'Se crei un altro' nuovo membro (1,2,3) 'calcolerà lo stesso hash, ** ma ** se ne modifichi uno, dovrai sincronizzarlo. È possibile utilizzare un metodo di registro/factory e creare oggetti come: 'Member a = Member.newInstance (1,2,3); Membro b = Member.newInstance (1,2,3); 'e crea l'istanza solo una volta. – OscarRyz

0

Teoricamente (e più spesso di quanto non in pratica troppo) la classe sia:

  1. ha un'identità immutabile naturale che può essere dedotta da un sottoinsieme dei suoi campi, nel qual caso è possibile utilizzare questi campi per generare il hashCode da.
  2. non ha identità naturale, nel qual caso l'uso di un Set per archiviarli non è necessario, si potrebbe anche usare uno List.
3

Jon Skeet ha elencato tutte le alternative. Per quanto riguarda il motivo per cui le chiavi in ​​una mappa o un set non devono cambiare:

Il contratto di un set implica che, in qualsiasi momento, non ci sono due oggetti O1 e O2 in modo tale che

o1 != o2 && set.contains(o1) && set.contains(o2) && o1.equals(o2) 

Perché che è necessaria è particolarmente chiaro per una mappa. Dal contratto di Map.get():

Più formalmente, se questa mappa contiene una mappatura da una chiave k ad un valore tale che v(key==null ? k==null : key.equals(k)), allora questo metodo restituisce v, altrimenti restituisce null. (Può esserci al massimo una tale mappatura.)

Ora, se si modifica una chiave inserita in una mappa, è possibile renderla uguale a qualche altra chiave già inserita. Inoltre, la mappa non può sapere di averlo fatto. Quindi, che cosa dovrebbe fare la mappa se esegui map.get(key), dove key è uguale a più chiavi nella mappa? Non esiste un modo intuitivo per definire cosa significherebbe - principalmente perché la nostra intuizione per questi tipi di dati è l'ideale matematico di insiemi e mappature, che non devono occuparsi di cambiare le chiavi, poiché le loro chiavi sono oggetti matematici e quindi immutabili.

-1

Non cambiare mai 'campo hashable" dopo aver messo in un contenitore a base di hash.

Come se voi (Stati) registrato il tuo numero di telefono (Member.x) a pagina gialla (contenitore in base hash), ma hai cambiato numero di , allora nessuno può trovare nella pagina di colore giallo più

2

Come già accennato, si può accettare i seguenti tre soluzioni:.

  1. utilizzare oggetti immutabili; anche quando la classe è mutevole, è possibile utilizzare identità immutabili sul tuo esercizio hashcode entazione e controllo equals, ad es. un valore di tipo ID.
  2. Analogamente a quanto sopra, attuare add/remove per ottenere un clone dell'oggetto inserito, non il riferimento effettivo. HashSet non offre una funzione get (ad esempio per consentire di modificare l'oggetto in un secondo momento); quindi, sei sicuro che non esisteranno duplicati.
  3. Esercizio disciplina non cambiare loro dopo che sono stati utilizzati, come @Jon Skeet suggerisce

Ma, se per qualche motivo avete veramente bisogno di modificare gli oggetti dopo essere stato inserito per un HashSet, è necessario trovare un modo di "informare" la tua Collezione con le nuove modifiche.Per ottenere questa funzionalità:

  1. È possibile utilizzare il modello di progettazione Observer ed estendere HashSet per implementare l'interfaccia Observer. Gli oggetti Member devono essere Observable e update il HashSet su qualsiasi setter o altro metodo che influisce su hashcode e/o equals.

Nota 1: Estensione 3, utilizzando 4: possiamo accettare alterazioni, ma quelli che non creare un oggetto già esistente (ad esempio, ho aggiornato ID di un utente, assegnando un nuovo ID, non impostandola su uno esistente uno). Altrimenti, devi considerare lo scenario in cui un oggetto viene trasformato in modo tale che ora è uguale a un altro oggetto già esistente nello Set. Se accetti questa limitazione, il 4 ° suggerimento funzionerà correttamente, altrimenti devi essere proattivo e definire una politica per tali casi.

Nota 2: è necessario fornire entrambi gli stati precedenti e attuali dell'oggetto alterato sul update implementazione, perché è necessario rimuovere inizialmente l'elemento più vecchio (ad esempio utilizzare getClone() prima di impostare nuovi valori), quindi aggiungere l'oggetto con il nuovo stato. Il seguente frammento di codice è solo un'implementazione di esempio, necessita di modifiche in base alla tua politica di aggiunta di un duplicato.

@Override 
public void update(Observable newItem, Object oldItem) { 
    remove(oldItem); 
    if (add(newItem)) 
     newItem.addObserver(this); 
} 

ho usato tecniche simili su progetti, dove sono necessari più indici su una classe, così posso guardare in alto con la O (1) per insiemi di oggetti che condividono una comune identità; immaginalo come una MultiKeymap di HashSet (questo è veramente utile, dato che puoi poi intersecare/union union e lavorare in modo simile alla ricerca di tipo SQL). In tali casi, annoto metodi (di solito setter) che devono aggiornare fireChange ciascuno degli indici quando si verifica un cambiamento significativo, quindi gli indici vengono sempre aggiornati con gli stati più recenti.

Problemi correlati