我是否应该覆盖 child 类 中的 equals 和 hashCode,即使它没有添加任何内容?

Should I be overriding equals and hashCode in child classes even if it's not adding anything?

我有抽象 class 覆盖 equals()hashCode()。在这些方法中,我使用抽象方法 getDescription() 来检查相等性并生成 hashCode。现在,当我扩展 class 并添加一个仅在 getDescription() 方法中使用的字段时,我遇到了一个 sonarLint 问题 "Extends a class that overrides equals and add fields"。只是 Sonar 不够复杂,无法理解正在发生的事情,还是我这样做不是 java 方式,而是 better/more 优雅的方式?

Parent class:

public abstract String getDescription();   

@Override
public int hashCode()
{
    return new HashCodeBuilder(19, 71).
            append(mViolation).
            append(getDescription()).
            append(mProperties).
            toHashCode();
}

@Override
public boolean equals(
    final Object obj)
{
    boolean equal = false;
    if (this == obj)
    {
        equal = true;
    }
    else if (obj instanceof parent)
    {
        AbstractStructuredDataIssue rhs = (parent) obj;
        equal = new EqualsBuilder().
                append(mViolation, rhs.mViolation).
                append(getDescription(), rhs.getDescription()).
                append(mProperties, rhs.mProperties).
                isEquals();
    }
    return equal;
}

Child class:

public class Child extends Parent {
    private final String mMessage;

    public Child(final String message, final int number) {
        super(number);
        mMessage = message;
    }

    @Override
    public String getDescription()
    {
        return String.format(
                DESCRIPTION_FORMAT,
                mMessage);
    }
}

这个有点复杂;我必须解释一些关于 equals 和 hashCode 如何工作的事情来解释可行的解决方案。

有一个'contract'。编译器无法强制执行,但如果你不遵守这个约定,就会发生奇怪的事情。具体来说:当您的对象用作散列映射中的键时,您的对象只会做错事,并且在使用第三方库时可能会出现其他此类问题。为了正确遵守合约,任何给定的 class 要么需要完全退出 equals/hashCode,要么整个链(因此,class 及其所有子 class es) 需要正确地覆盖 hashCode 和 equals,除了,你真的不能这样做,除非父级被正确地检测到这样做。

合同规定这必须始终正确:

  • a.equals(b) -> b.equals(a).
  • a.equals(b) 和 b.equals(c) -> a.equals(c).
  • a.equals(一).
  • a.equals(b) -> a.hashCode() == b.hashCode()。 (注意,反过来不一定是真的;相等的哈希码并不意味着对象是相等的)。

面对class层级,合约真的很难保证!想象一下,我们将现有的 java.util.ArrayList 和 subclass 与 'color' 的概念相结合。所以现在我们可以有一个蓝色的 ColoredArrayList,或者一个红色的 ColoredArrayList。说一个蓝色的 ColoredArrayList 绝对不应该等于一个红色的 ColoredArrayList 是完全有道理的,除了.. ArrayList 本身的 equals impl(你不能改变),有效地定义你不能简单地使用这样的属性扩展 ArrayList at all:如果你调用 a.equals(b),其中 a 是一个空数组列表,b 是一些空列表(比如,一个空的红色 ColoredArrayList),它只会检查每个的相等性其中的成员,考虑到它们都是空的,这是微不足道的。因此,空的普通数组列表等于空的红色和空的蓝色 ColoredArrayList,因此合同规定空的红色 ColoredArrayList 必须等于空的蓝色 ColoredArrayList。从这个意义上说,声纳在这里被打破了。有问题,而且无法修复。 java写ColoredArrayList的概念是不可能的。

然而,有一个解决方案,但前提是层次结构中的每个 class 都在船上。这是 canEqual 方法。摆脱上述彩色困境的方法是区分 'I am extending, and adding new properties' 和 'I am extending, but, these things are still semantically speaking the exact same thing with no new properties' 的概念。 ColoredArrayList 是前一种情况:它是一个添加新属性的扩展。 canEqual 的想法是您创建一个单独的方法来指示这一点,这让 ArrayList 明白:我不能等于任何 ColoredArrayList 实例,即使所有元素都相同。然后我们可以再次遵守合同。 ArrayList 没有这个系统,因此,鉴于您无法更改 ArrayList 的源代码,您陷入困境:它不可修复。但是如果你写自己的class层级,你可以添加它。

Lombok 项目负责为您添加 equals 和 hashCode。即使您不想使用它,您也可以查看它生成的内容并将其复制到您自己的代码中。这也将删除声纳发出的警告。请参阅 https://projectlombok.org/features/EqualsAndHashCode – 这也向您展示了如何使用 canEqual 概念来避免 ColoredArrayList 困境。

在这里你 subclass 没有添加新的属性,所以,实际上没有替换 hashCode 和 equals 的需要。但是sonar不知道。

我们来看看规则RSPEC-2160:

Extend a class that overrides equals and add fields without overriding equals in the subclass, and you run the risk of non-equivalent instances of your subclass being seen as equal, because only the superclass fields will be considered in the equality test.

Sonar 指出的是您获得 不相等 对象被视为 相等 的风险,因为当您调用 equals 在您的子 class 中,如果没有正确的实现,将仅评估父 class 字段。

不合规代码示例(来自文档)

public class Fruit {
  private Season ripe;

  public boolean equals(Object obj) {
    if (obj == this) {
      return true;
    }
    if (this.class != obj.class) {
      return false;
    }
    Fruit fobj = (Fruit) obj;
    if (ripe.equals(fobj.getRipe()) {
      return true;
    }
    return false;
  }
}

public class Raspberry extends Fruit {  // Noncompliant; instances will use Fruit's equals method
  private Color ripeColor;
}

合规解决方案(也来自文档)

public class Fruit {
  private Season ripe;

  public boolean equals(Object obj) {
    if (obj == this) {
      return true;
    }
    if (this.class != obj.class) {
      return false;
    }
    Fruit fobj = (Fruit) obj;
    if (ripe.equals(fobj.getRipe()) {
      return true;
    }
    return false;
  }
}

public class Raspberry extends Fruit {
  private Color ripeColor;

  public boolean equals(Object obj) {
    if (! super.equals(obj)) {
      return false;
    }
    Raspberry fobj = (Raspberry) obj;
    if (ripeColor.equals(fobj.getRipeColor()) {  // added fields are tested
      return true;
    }
    return false;
  }
}

我同意你的观点,即 Sonar 可能不够复杂,无法查看运行时发生的情况,因此它指出了 Code Smell。

你需要担心不破坏equalshashCode契约,你的方法是动态的,Sonar可能没有考虑到这一点。

  • 覆盖childclass中的equals()和hashcode()方法会生效 考虑 child class 成员(变量),它也有帮助 使用 sub-types 的 Collection 框架和 Map 实例来查找 收集框架操作期间的右内存space(桶)(例如: save/retrieve).

    这里继承自super class可能会漏掉child class个成员 有效生成 hashcode/equals 方法功能。

通过您的实现,您可以有两个 Parent 引用,它们 相等 ,但指向两个不同 类 的对象,这样一来被投射到 Child 而另一个 - 不是。

这是非常出乎意料的,可能会导致未来出现问题 - 指出这一点是 Sonar 的工作。如果您认为它对您的用例来说是合理的,只需将其与 Sonar 的警告一起记录下来(这就是我会做的)。