我是否应该在子类中重写equals和hashCode,即使它没有添加任何东西?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我是否应该在子类中重写equals和hashCode,即使它没有添加任何东西?相关的知识,希望对你有一定的参考价值。

我有抽象类来覆盖equals()hashCode()。在那些方法中,我使用抽象方法getDescription()来检查相等性并生成hashCode。现在当我扩展类并添加一个仅在getDescription()方法中使用的字段时,我得到一个sonarLint问题“扩展一个覆盖等于并添加字段的类”。仅仅是Sonar不够精明,无法理解发生了什么,或者我是不是以java方式做到这一点而且有更好/更优雅的方式?

家长班:

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;
}

儿童班:

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如何解释可行解决方案的事情。

有一个'合同'。编译器无法强制执行,但如果您不遵守此合同,则会发生奇怪的事情。具体来说:当用作哈希映射中的键时,您的对象只会做错事,并且在使用第三方库时可能会出现其他类似问题。为了正确遵守契约,任何给定的类都需要完全选择退出equals / hashCode,OR,整个链(所以,类及其所有子类)需要正确覆盖hashCode和equals,除此之外,你真的可以'除非父母有适当的仪器,否则不要这样做。

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

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

面对阶级层级,合同真的很难保证!想象一下,我们采用现有的java.util.ArrayList并将其子类化为'color'的概念。所以现在我们可以有一个蓝色的ColoredArrayList,或一个红色的ColoredArrayList。说明蓝色ColoredArrayList绝对不应该等于红色ColoredArrayList,除了...等于ArrayList本身的impl(你无法改变),这是非常有意义的,有效地定义你根本无法使用这样的属性扩展ArrayList:如果你调用a.equals(b)其中a是一个空的arraylist而b是一个空的List(比如一个空的红色ColoredArrayList),它只会检查其中每个成员的相等性,因为它们都是空的,很简单。因此,空正常arraylist等于空红色和空蓝色ColoredArrayList,因此合同规定空红色ColoredArrayList必须等于空蓝色ColoredArrayList。从这个意义上讲,声纳只是在这里打破了。有一个问题,它是不可修复的。在java中编写ColoredArrayList的概念是不可能的。

然而,有一个解决方案,但只有当层次结构中的每个类都在船上时。这是canEqual方法。如上所述,摆脱色彩困境的方法是区分“我正在扩展,添加新属性”和“我正在扩展”这一概念,但是,这些东西在语义上仍然完全相同,没有新属性。 ColoredArrayList是前一种情况:它是一个添加新属性的扩展。 canEqual的想法是你创建一个单独的方法来指示这个,这让ArrayList弄清楚:我不能等于任何ColoredArrayList实例,即使所有元素都是相同的。然后我们可以再次遵守合同。 ArrayList没有这个系统,因此,如果你不能更改ArrayList的源代码,你就会陷入困境:它无法修复。但是,如果您编写自己的类层次结构,则可以添加它。

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

这里你是子类而不添加新属性,因此,实际上不需要替换hashCode和equals。但声纳不知道这一点。

另一答案

让我们来看看规则RSPEC-2160

扩展一个覆盖等于的类并添加字段而不覆盖子类中的equals,并且您冒着子类的非等效实例被视为相等的风险,因为在相等测试中只考虑超类字段。

Sonar指出的是你将不相等的对象视为相等的风险,因为当你在你的子类中调用equals时,没有正确的实现,只会评估父类字段。

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

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所考虑的。

另一答案
  • 考虑子类成员(变量),覆盖子类中的equals()和hashcode()方法将是有效的,并且它还有助于在集合框架操作期间使用Collection框架和Map实例的子类型来查找正确的内存空间(存储区)(例如:保存/检索)。 在这里继承超类可能会错过子类成员来有效地生成hashcode / equals方法功能。
另一答案

使用你的实现,你可以有两个相同的Parent引用,但指向两个不同类的对象,这样一个可以转换为Child而另一个 - 不是。

这是非常意外的,可能会导致问题 - 而Sonar的工作就是指出它。如果你认为你的用例是合理的,那就用Sonar的警告记录下来(这就是我要做的)。

以上是关于我是否应该在子类中重写equals和hashCode,即使它没有添加任何东西?的主要内容,如果未能解决你的问题,请参考以下文章

重写了equals方法为何需要重写 hashCode

为啥要重写toString方法和hashcode方法

java:放入Set中的对象一定要重写hashCode()和equals()吗?

重写equals()和hashCode()

重写equals()方法和 hashCode()方法

重写HashCode和equals规范