【问题标题】:ArrayList's contains() method always returns false with custom objectArrayList 的 contains() 方法总是使用自定义对象返回 false
【发布时间】:2019-02-18 10:04:20
【问题描述】:

我在编写代码时遇到了一些麻烦,我给你举个简单的例子(虽然它会稍微复杂一点,这个简单的代码也不能正常工作)。

class Sign {

  private String char;
  private Integer freq;

  public Sign(String c) {
  this.char = c; 
  }

  @Override
  public boolean equals(Object o) {

   String check = (String)o;
   return check.equals(this.char);
  }

  @Override
  public int hashCode() {

    int hash = 7;
    hash = 31 * hash + this.char.hashCode();
    return hash;
}

}

为了简单起见,我假设在 equals 方法中总会有一个字符串。还有一些 hashCode() 也可以确保 contains() 方法有效,这是测试本身:

    ArrayList<Sign> queueOfSigns = new ArrayList<>();

    Sign test = new Sign("C");
    String c = "C";
    queueOfSigns.add(test);

    if(queueOfSigns.contains("C"))
        System.out.println("I am here!");

无论如何,这个简单的测试代码在这种情况下总是返回 false - 所以“我在这里”消息永远不会出现。我一直在尝试一些不同的方法来处理我的代码,但这是因为这样做的想法是从字符串文本中获取单个字符并检查单个字符是否已经存在于 ArrayList 中。尽管如此 - 如果没有让这个简单的测试正常工作,我无法继续前进,所以我想问你 - 我错过了什么。这是我第一次真正使用 equals() 和 hashCode() 方法来让我自己的对象与 contains() 方法一起正常工作。

【问题讨论】:

  • 问题很可能是new Sign("C").equals("C") 没有返回与"C".equals(new Sign("C")) 相同的内容。要正常工作,equals 需要是对称的:a.equals(b) == b.equals(a) 应该始终为真。

标签: java arraylist contains


【解决方案1】:

您的equals 实现不正确。 equals 有特定合约;该代码试图违反该合同。来自文档:

equals 方法在非空对象引用上实现等价关系:

  • 它是自反的:对于任何非null 的引用值xx.equals(x) 应该返回true
  • 它是对称的:对于任何非null 参考值xyx.equals(y) 应该返回true 当且仅当y.equals(x) 返回true .
  • 它是传递的:对于任何非null 引用值xyz,如果x.equals(y) 返回truey.equals(z) 返回@ 987654343@,然后x.equals(z) 应该返回true
  • 一致:对于任何非null 引用值xyx.equals(y) 的多次调用始终返回true 或始终返回false,提供equals 对象比较中使用的信息没有被修改。
  • 对于任何非null 参考值xx.equals(null) 应返回false

无法将Signequals 的实例设为字符串。

【讨论】:

  • 当然可能让一个对象报告它等于另一个类型的对象。如果另一个对象也不使自己等于您的对象,它就不会正确工作。有几个地方你可以改变规则(例如,一些项目使用了专用的“查找对象”,对于这种情况,它们是“足够”的),但改变规则是一个非常棘手的提议。
  • @DanielPryden - 对,也是一个有用的观察。但是Stringequals 不会也不会支持OP 的Sign 类,所以... :-)
  • 为了完整性,在这些属性中,OP 的 equals() 实现仅表现出传递性。在它坏的地方,它真的坏了:它有一个讨厌的习惯,扔ClassCastExceptions和NullPointerExceptions。
  • @LowProfile - 我根本不会使用ArrayList。我会使用Map 并使用char 属性作为键存储Sign 对象。然后你用containsKey来判断map是否有object,或者get根据key获取object。
  • @T.J. Crowder - 最后我可能会使用地图 - 这是我的第一个想法。我只是想尝试一下arraylist和自己的equals()、hashCode()。
【解决方案2】:

您的equals 方法实现不正确。违反Object.equals的总合约:

  • 它不是自反的 - 因为当参数不是字符串时它会引发异常,x.equals(x) 其中xSign 将因异常而崩溃。
  • 它不是对称的 - 如果y 是字符串并且xSign,则x.equals(y) 不会返回与y.equals(x) 相同的值
  • 不一致 - 因为当参数不是字符串时它可能会抛出异常,而不仅仅是返回 true 或 false。

在低抽象层次上,这个问题的原因是contains的实现。根据文档:

如果此列表包含指定元素,则返回 true。更正式地说,当且仅当此列表包含至少一个元素 e 满足 (o==null ? e==null : o.equals(e)) 时,才返回 true。

ArrayList 实际上调用了o.equals(e)o 是你传入的字符串。所以它实际上调用了String 中的equals 方法。

如果contains 调用e.equals(o),那么您的程序会打印“我在这里”,但您的equals 仍然违反合同。

一个更好的equals 实现是这样的:

@Override
public boolean equals(Object o) {
    if (o == null) {
        return false;
    }

    if (o.getClass() == this.getClass()) {
        Sign other = (Sign)o;
        return other.$char.equals($char); // I have renamed 'char' to '$char' since the former is not a valid identifier
    } else {
        return false;
    }
}

还有你的客户端代码:

    ArrayList<Sign> queueOfSigns = new ArrayList<>();

    Sign test = new Sign("C");
    Sign c = new Sign("C");
    queueOfSigns.add(test);

    if(queueOfSigns.contains(c))
        System.out.println("I am here!");

编辑:

我想这就是你要找的:

arrayList.stream()
    .filter(x -> x.getChar().equals("C"))
    .findFirst().isPresent() // this returns true if a sign with C is found in the array list

【讨论】:

  • 即使在equals 中使用instanceof 也是不可靠的。如果我创建一个扩展Sign 的类NiftySign,则NiftySign 实例不等于Sign 实例,但Sign's equals(根据上述)会认为是。跨度>
  • 谢谢,我明白了这个问题,但代码不会做我真正想要的。事情是这样的——我需要将单个字母(例如)作为字符串或字符传递给 ArrayList 的 contains() 方法——它必须包含 Sign 对象(ArrayList)。问题仍然存在,因为我只想拥有自己的、被覆盖的 equals() 方法,该方法实际上会将来自 Sign 对象的 String 与给定的 String 进行比较,就像在示例中一样。我只是想避免“instanceof”,因为在我的事业中,没有办法传递任何其他东西——即使它在任何方面都不是一个好习惯——我知道。
  • 那么你不应该覆盖equals。您需要在数组列表中找到具有char 字段"C" 的元素。见编辑。 @LowProfile
猜你喜欢
  • 2021-01-28
  • 2012-11-01
  • 2020-04-19
  • 2012-01-09
  • 2019-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多