【问题标题】:Overriding hashCode with overridden equals using equalsIgnoreCase to check equality使用 equalsIgnoreCase 用覆盖的等于覆盖 hashCode 来检查相等性
【发布时间】:2013-03-15 18:15:24
【问题描述】:

我目前有一个被覆盖的equals(Object),看起来像这样:

@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());
}

我的hashCode() 目前看起来像这样:

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

我的问题是关于我重写的 hashCode() 方法。我知道如果 equals(Object) 方法认为它们相等,我需要 hashCode() 为两个对象返回相同的值。我的直觉告诉我有 some 这种 hashCode() 会违反合同的情况。

是否有一种可接受的方式在被覆盖的 equals(Object) 方法中使用 equalsIgnoreCase(String) 方法并生成不违反合同的哈希码?

【问题讨论】:

  • In hashCode() result = 31... 应该是 result *= 31... 这样你就不会丢失已经存在的值。
  • 他在等式中有结果,31 * 结果+(其他)。所以不会丢失。只是我的 2 美分,但我认为你的做法是正确的。你的 equals 方法对我来说看起来不错。
  • 为什么你的代码会违反合约?你的直觉一定很紧张,不要听它;)
  • 我可能只是有点过于谨慎,但我不完全确定 equalsIgnoreCase() 和 toLowerCase() 方法如何处理特殊字符和不同的语言环境。我认为这不适用于此应用程序,但我正在尝试做一些尽可能防弹的事情来养成这种习惯。
  • 传统的智慧是你永远不应该依赖默认的Locale,而应该始终使用String.toLowerCase(Locale)和明确的Locale。否则你会遇到“臭名昭著的土耳其语言环境错误”。

标签: java overriding equals hashcode


【解决方案1】:
@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();
}

hashCode 将与使用equalsIgnoreCase 定义的equals 一致。原则上,根据equalsIgnoreCase的合同,这似乎依赖于它是这样的情况

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

无论何时

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

我没有证据证明这是真的,但OpenJDK implementation of equalsIgnoreCase 实际上与这种方法一致;它检查对应的字符是否相等,然后它们的大写版本是否相等,然后大写版本的小写版本是否相等

【讨论】:

  • String.compareToIgnoreCase 明确使用此方法。
  • 我会为一种新方法 +1,但你应该非常小心。 Javadocs 甚至警告您:In general, String.toLowerCase() should be used to map characters to lowercase. String case mapping methods have several benefits over Character case mapping methods. String case mapping methods can perform locale-sensitive mappings, context-sensitive mappings, and 1:M character mappings, whereas the Character case mapping methods cannot. 另外,规范似乎无法保证这种行为,因此它可能会与其他人不同。小心!
  • 对...我想说String.equalsIgnoreCase()(和String.compareToIgnoreCase()),基于Character case-mapping 方法,应该有同样的警告。在编写与equals() 一致的hashCode() 方面,您应该在两者中使用基于Character 的大小写映射,或者在两者中使用基于String 的大小写映射。事实上,原来的提问者可能真的很想保留他的hashCode() 方法并将他的equals() 方法更改为使用s1.toLowerCase().equals(s2.toLowerCase()) 而不是equalsIgnoreCase()
【解决方案2】:

你是对的。我们可以遍历所有单字符字符串,并找到对 s1,s2s1.equalsIgnoreCase(s2) &amp;&amp; !s1.toLowerCase().equals(s2.toLowerCase())。有相当多的对。例如

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

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

它还取决于语言环境:对于 s1,土耳其语和阿塞拜疆语使用 U+0131 表示小写(参见 http://www.fileformat.info/info/unicode/char/0049/index.htm

【讨论】:

    【解决方案3】:

    你的担心是对的。 Read the contract for equalsIgnoreCase.

    如果以下至少一项为真,则两个字符 c1 和 c2 被认为是相同的忽略大小写:

    • 这两个字符相同(通过 == 运算符比较)
    • 对每个字符应用 Character.toUpperCase(char) 方法会产生相同的结果
    • 对每个字符应用 Character.toLowerCase(char) 方法会产生相同的结果

    所以,如果有一个字符在转换为 大写 时是相等的,而反之则不然,那你就麻烦了。

    我们以德语字符ß 为例,它在转换为大写时会变成two character sequence SS。这意味着字符串 "ß" 和 "SS" 是 "equalsIgnoreCase" 但在转换为小写时不会有相同的表示!

    所以你的方法在这里被打破了。不幸的是,我不确定您是否能够在这里设计一个充分表达您需要的 hashCode。

    【讨论】:

    • 因此以字符 ß 为例,如果我们有一个名字/姓氏为“ßilly ßob”的玩家,将他与另一个名为“SSilly SSob”的玩家进行比较将使他们在equalsIgnoreCase 的眼睛,但随后生成两个不同的 hashCodes(问题)。假设这对我的应用程序来说是“好的”,我们是否可以在我使用 toLowerCase 的地方使用 toUpperCase 生成一个当它们被 equalsIgnoreCase 认为相等时相等的 hashCode?​​span>
    • 我相信你也能找到反例。
    • @Jazzer:equalsIgnoreCase是否定义了等价关系,即不可能有三个字符串x、y和z,使得x.equalsIgnoreCase(y)和y.equalsIgnoreCase(z) ,但不是 x.equalsIgnoreCase(z)?听上去,"ß".equalsIgnoreCase("SS") 为真,"ss".equalsIgnoreCase("SS") 为真,但 "ß".equalsIgnoreCase("ss") 为假。使用不实现等价关系的函数覆盖 equals 将被破坏,即使 hashCode 总是返回匹配字符串的匹配值。
    • "ß".equalsIgnoreCase("SS") 为假,因为equalsIgnoreCase 使用Character.toUpperCaseCharacter.toLowerCase 而不是String.toUpperCaseString.toLowerCase。这为与equalsIgnoreCase 一致的hashCode 带来了希望;看我的回答。
    【解决方案4】:

    就编写与equals() 一致的hashCode() 而言,您应该在两者中使用基于Character 的大小写映射,或者在两者中使用基于String 的大小写映射。在我的另一个答案中,我展示了如何使用基于Character 的案例映射编写hashCode();但还有另一种解决方案,即更改equals(),而不是使用基于String 的案例映射。 (注意String.equalsIgnoreCase() 使用基于Character 的大小写映射。)

    @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());
    }
    

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-06
    相关资源
    最近更新 更多