即使没有增加任何东西,我是否应该在子类中覆盖equals和hashCode方法?

3
我有一个抽象类,覆盖了equals()hashCode()方法。在这些方法中,我使用了抽象方法getDescription()来检查相等性并生成哈希码。现在当我扩展这个类并添加一个仅在getDescription()方法中使用的字段时,我会得到一个SonarLint问题“继承了覆盖了equals并添加了字段的类”。这只是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);
    }
}
4个回答

1
这有点复杂;我必须解释一下 equals 和 hashCode 的工作原理,以便说明可行的解决方案。
有一个“合同”。编译器无法强制执行它,但如果您不遵守此合同,将会发生奇怪的事情。具体来说:当您的对象用作哈希图中的键时,您的对象将执行错误操作,并且在使用第三方库时可能会出现其他问题。为了正确遵守合同,任何给定的类都需要完全退出 equals/hashCode,或者,整个链(因此,类及其所有子类)需要正确地覆盖 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并将其子类化为“颜色”的概念。因此,现在我们可以拥有蓝色的ColoredArrayList或红色的ColoredArrayList。毫无疑问,说蓝色的ColoredArrayList绝对不应该等于红色的ColoredArrayList是有道理的,除了... ArrayList本身的equals实现(你无法更改)有效地定义了你根本不能像这样扩展ArrayList的属性:如果你调用a.equals(b),其中a是一个空arraylist,b是某个空List(比如,一个空的红色ColoredArrayList),它只会检查其中每个成员的相等性,在这种情况下,它们都是空的,显然是真的。因此,空的普通ArrayList等于空的红色和空的蓝色ColoredArrayList,因此合约规定空的红色ColoredArrayList必须等于空的蓝色ColoredArrayList。从这个意义上说,sonar在这里只是出了问题。存在一个问题,而且它是无法修复的。在Java中编写ColoredArrayList的概念是不可能的

然而,有一个解决方案,但前提是层级中的每个类都必须同意。这就是“canEqual”方法。摆脱上述的困境的方法是区分“我正在扩展并添加新属性”和“我正在扩展,但从语义上讲,这些东西仍然是完全相同的,没有新属性”。ColoredArrayList是前一种情况:它是一种扩展,添加了新属性。canEqual的想法是你创建一个单独的方法来表明这一点,这让ArrayList找出:即使所有元素相同,我也不能等于任何ColoredArrayList实例。然后我们可以再次遵守契约。ArrayList没有这个系统,因此,鉴于您无法更改ArrayList的源代码,您会陷入困境:它是不可修复的。但如果你编写自己的类层次结构,你可以添加它。

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

在这里,您可以子类化而不添加新属性,因此实际上不需要替换hashCode和equals。但是sonar不知道这一点。


0
在子类中覆盖equals()和hashcode()方法将会更有效,考虑到子类成员(变量),并且还有助于在使用集合框架的子类型和Map实例时,找到正确的内存空间(桶)以进行集合框架操作(例如:保存/检索)。 从父类继承可能会错过子类成员,无法有效地生成hashcode/equals方法功能。

0

使用您的实现方式,您可以拥有两个Parent引用是相等的,但指向两个不同类的对象,这样一个可以转换为Child,而另一个则不能。

这种情况非常出乎意料,并可能导致以后的问题 - 而这就是Sonar的工作。如果您认为这对您的用例很有必要,只需在Sonar的警告下进行实时文档记录(这就是我会做的)。


0
让我们来看一下规则 RSPEC-2160
如果您扩展一个重写了equals方法的类,并在子类中添加字段而不重写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可能不够复杂,无法看到运行时发生的情况,因此它会指出代码异味。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接