2013-03-26 10 views
7

ho attualmente ottenuto un sovrascritto equals(Object) che assomiglia a questo:hashCode Override con eguali ignorate utilizzando equalsIgnoreCase per verificare l'uguaglianza

@Override 
public boolean equals(Object o) { 
    if (o == this) return true; 
    if (! (o instanceof Player)) return false; 
    Player p = (Player) o; 
    return getFirstName().equalsIgnoreCase(p.getFirstName()) && 
      getLastName().equalsIgnoreCase(p.getLastName()); 
} 

mio hashCode() attualmente si presenta così:

@Override 
public int hashCode() { 
    int result = 17; 
    result = 31 * result + getFirstName().toLowerCase().hashCode(); 
    result = 31 * result + getLastName().toLowerCase().hashCode(); 
    return result; 
} 

La mia domanda è riguardo il mio metodo hashCode() sovrascritto. So che ho bisogno di hashCode() per restituire lo stesso valore per due oggetti se sono considerati uguali dal metodo equals (Object). Il mio istinto mi dice che c'è del caso in cui questo hashCode() viola il contratto.

Esiste un modo accettabile di utilizzare il metodo equalsIgnoreCase (String) in un metodo equals (Object) sovrascritto e generare un hashcode che non violi il contratto?

+0

In hashCode() result = 31 ... dovrebbe essere risultato * = 31 ... in modo da non perdere il valore già in là. – Patashu

+1

Ha ottenuto l'equazione, 31 * risultato + (altri oggetti). Quindi non è perso. Solo i miei 2 centesimi, ma penso che tu stia andando nel modo giusto. Il tuo metodo per i pari vale per me. – Kyle

+0

Perché il tuo codice viola il contratto? Il tuo istinto deve essere nervoso, non ascoltarlo;) – ddmps

risposta

4
@Override 
public int hashCode() { 
    int result = 17; 
    result = 31 * result + characterwiseCaseNormalize(getFirstName()).hashCode(); 
    result = 31 * result + characterwiseCaseNormalize(getLastName()).hashCode(); 
    return result; 
} 

private static String characterwiseCaseNormalize(String s) { 
    StringBuilder sb = new StringBuilder(s); 
    for(int i = 0; i < sb.length(); i++) { 
     sb.setCharAt(i,Character.toLowerCase(Character.toUpperCase(sb.charAt(i)))); 
    } 
    return sb.toString(); 
} 

Questo hashCode sarà coerente con un equals definito utilizzando equalsIgnoreCase.In linea di principio, in base al contratto di equalsIgnoreCase, questo sembra fare affidamento su di esso è il caso che

Character.toLowerCase(Character.toUpperCase(c1))==Character.toLowerCase(Character.toUpperCase(c2)) 

ogni volta

Character.toLowerCase(c1)==Character.toLowerCase(c2). 

Non ho la prova che questo è vero, ma la realtà OpenJDK implementation of equalsIgnoreCase lo fa in modo coerente con questo metodo; controlla se i caratteri corrispondenti sono uguali, quindi se le loro versioni maiuscole sono uguali, allora se le versioni minuscole delle versioni maiuscole sono uguali.

+0

E 'String.compareToIgnoreCase' utilizza questo metodo in modo esplicito. –

+0

Farò +1 per un nuovo approccio, ma dovresti stare molto attento. I Javadoc ti avvertono anche: 'In generale, String.toLowerCase() dovrebbe essere usato per mappare caratteri in minuscolo. I metodi di mappatura delle maiuscole hanno numerosi vantaggi rispetto ai metodi di mappatura dei casi di caratteri. I metodi di mappatura delle maiuscole/minuscole possono eseguire mappature sensibili alle impostazioni locali, mappature sensibili al contesto e mappature dei caratteri 1: M, mentre i metodi di mappatura dei casi di carattere non riescono. Inoltre questo comportamento non sembra garantito dalle specifiche, quindi potrebbe cambiare da altro tu. Attenzione! –

+1

Giusto ... direi 'String.equalsIgnoreCase() '(e' String.compareToIgnoreCase() '), essendo basato sui metodi di mappatura dei casi' Character', dovrebbe venire con lo stesso avvertimento. Per quanto riguarda la scrittura di un 'hashCode()' coerente con 'equals()', si dovrebbe usare sia la mappatura dei casi basata su 'Character' in entrambi, sia la mappatura dei casi basata su' String' in entrambi. In effetti, l'interrogante originale potrebbe davvero voler mantenere il suo metodo 'hashCode()' e cambiare il suo metodo 'equals()' per usare 's1.toLowerCase(). Equals (s2.toLowerCase())' invece di 'equalsIgnoreCase () '. –

1

Hai ragione. Possiamo scorrere tutte le stringhe di un carattere e trovare coppie s1,s2 che s1.equalsIgnoreCase(s2) && !s1.toLowerCase().equals(s2.toLowerCase()). Ci sono alcune coppie. Per esempio

s1=0049 'LATIN CAPITAL LETTER I' 
s2=0131 'LATIN SMALL LETTER DOTLESS I' 

s1.lowercase = 0069 'LATIN SMALL LETTER I' 
s2.lowercase = 0131 itself 

Dipende anche locale: per S1, uso turco e azero U + 0131 per la minuscola (vedi http://www.fileformat.info/info/unicode/char/0049/index.htm)

1

Hai ragione di essere preoccupati. Read the contract for equalsIgnoreCase.

due caratteri c1 e c2 sono considerati lo stesso caso ignorando se almeno una delle seguenti condizioni:

  • I due caratteri sono uguali (rispetto dall'operatore ==)
  • Applicazione il metodo Character.toUpperCase (char) a ciascun carattere produce lo stesso risultato
  • Applicando il metodo Character.toLowerCase (char) a ciascun carattere produce lo stesso risultato

Quindi, se c'è un carattere che è uguale quando convertito in maiuscolo ma non viceversa, sarai nei guai.

Prendiamo l'esempio del carattere tedesco ß, che diventa two character sequence SS quando convertito in maiuscolo. Ciò significa che la stringa "ß" e "SS" sono "equalsIgnoreCase" ma non avrà la stessa rappresentazione quando convertita in minuscolo!

Quindi il tuo approccio qui è rotto. Sfortunatamente, non sono sicuro che sarai in grado di progettare un hashCode che esprima adeguatamente le tue necessità qui.

+0

Quindi, usando il carattere ß come esempio, se avessimo un giocatore con il nome/cognome "ßilly ßob", confrontarlo con un altro giocatore chiamato "SSilly SSob" li renderebbe uguali agli occhi di equalsIgnoreCase ma poi generare due diversi hashCode (il problema). Supponendo che questo sia "okay" per la mia applicazione, potremmo generare un hashCode che è uguale quando sono considerati uguali da equalsIgnoreCase usando toUpperCase dove stavo usando a LowerCase? – Jazzer

+0

Sono sicuro che potresti trovare un controesempio anche nell'altro modo. –

+0

@Jazzer: 'equalsIgnoreCase' definisce una relazione di equivalenza, cioè è impossibile avere tre stringhe x, y e z, tali che x.equalsIgnoreCase (y) e y.equalsIgnoreCase (z), ma non x.equalsIgnoreCase (z)? Di conseguenza, "ß" .equalsIgnoreCase ("SS") sarebbe vero, e "ss" .equalsIgnoreCase ("SS") sarebbe vero, ma "ß" .equalsIgnoreCase ("ss") sarebbe falso. L'override di 'equals' con una funzione che non implementa una relazione di equivalenza sarebbe interrotta, anche se' hashCode' restituiva sempre i valori corrispondenti per le stringhe corrispondenti. – supercat

1

In termini di scrittura di un hashCode() coerente con equals(), si dovrebbe usare sia Character basata su caso-mapping in entrambi, o String basata su caso-mapping in entrambi. Nella mia altra risposta ho mostrato come scrivere un hashCode() usando il case-mapping basato su Character; ma c'è un'altra soluzione, che è quella di cambiare equals() invece di utilizzare il case-mapping basato su String. (Si noti che String.equalsIgnoreCase() utilizza Character basata su caso-mapping.)

@Override 
public boolean equals(Object o) { 
    if (o == this) return true; 
    if (! (o instanceof Player)) return false; 
    Player p = (Player) o; 
    return getFirstName().toLowerCase().equals(p.getFirstName().toLowerCase()) && 
     getLastName().toLowerCase().equals(p.getLastName().toLowerCase()); 
} 
+0

In alcune circostanze, in realtà, si desidera davvero utilizzare un po 'di normale normalizzazione Unicode sulle stringhe e la piegatura del caso. Vedi http://userguide.icu-project.org/transforms/normalization. –