ArrayList没有使用重写的equals方法

10

我在使用重写的equals方法时,ArrayList无法正确运行。我的问题是,我想只测试单个关键字段,使用ArrayList.contains()来测试是否存在具有正确字段的对象。下面是一个例子:

public class TestClass  {
    private static class InnerClass{    
    private final String testKey;
    //data and such

    InnerClass(String testKey, int dataStuff) {
        this.testKey =testKey;
        //etc
    }
    @Override
    public boolean equals (Object in) {
        System.out.println("reached here");
        if(in == null) {
        return false;
        }else if( in instanceof String) {
        String inString = (String) in;
        return testKey == null ? false : testKey.equals(inString);
        }else {
        return false;
        }       
    }       
    }

    public static void main(String[] args) {    
    ArrayList<InnerClass> objectList = new ArrayList<InnerClass>();
    //add some entries
    objectList.add(new InnerClass("UNIQUE ID1", 42));
    System.out.println( objectList.contains("UNIQUE ID1")); 
    }    
}

让我担心的是,输出不仅是错误的,而且也没有得到“到这里了”的输出。

有人知道为什么这个覆盖被完全忽略了吗?是否有一些关于覆盖和内部类的微妙之处我不知道的?

编辑: 由于网站出现问题,我无法标记答案。 感谢您的快速回复:是我的疏忽,调用的是String .equals而不是我的自定义方法。我想现在先使用老式检查方式。

11个回答

16
如果您检查 ArrayList 的源代码,您会发现它调用了其他对象的 equals。在您的情况下,它将调用 String "UNIQUE ID1"equals 方法,该方法将检查其他对象是否不是 String 类型,并返回 false
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    ...     
    for (int i = 0; i < size; i++)
    if (o.equals(elementData[i]))
        return i;
    ...
    return -1;
}

对于您的情况,调用具有仅包含 idInnerClass 参数的 contains 方法:

objectList.contains(new InnerClass("UNIQUE ID1"))

别忘了为InnerClass实现equals方法,只比较id字段。


谢谢。新的InnerClass()建议也非常有帮助。 :) - Sufian
1
在我看来,indexOf的实现应该是相反的:elementData[i].equals(o)... - marcolopes

7
根据 List.contains(o) 的JavaDoc,它被定义为仅当列表包含至少一个元素e满足(o==null ? e==null : o.equals(e))时返回true
请注意,此定义调用的是equals方法的参数o,而不是在List中的元素。
因此,将调用String.equals()而不是InnerClass.equals()
还要注意,Object.equals()的规范声明:
对于任何非空引用值xy,如果且仅如果y.equals(x)返回true,则x.equals(y)应该返回true
但是,由于new TestClass("foo", 1).equals("foo")返回true,但"foo".equals(new TestClass("foo", 1))将始终返回false,因此您违反了这个约束条件。
不幸的是,这意味着您的使用情况(自定义类可以等于另一个标准类)无法完全符合规范。
如果您仍然想要执行此类操作,您需要非常仔细地阅读所有集合类的规范(有时还需要阅读实现)并检查此类陷阱。

工作时间太长了。当你开始忘记基础知识或错误地阅读API规范时,这绝不是一个好兆头。我发现通过创建一个新的InnerClass(String testKey)构造函数来制作测试对象的解决方法,但实际数据为null / 0s。 - K.Barad

3

您正在使用一个 String 而不是一个 InnerClass 作为参数来调用 contains 方法:

System.out.println( objectList.contains("UNIQUE ID1"))

在我的JDK中:

public class ArrayList {

    public boolean contains(Object o) {
    return indexOf(o) >= 0;
    }

    public int indexOf(Object o) {
    if (o == null) {
        // omitted for brevity - aix
    } else {
        for (int i = 0; i < size; i++)
        if (o.equals(elementData[i])) // <<<<<<<<<<<<<<<<<<<<<<
            return i;
    }
    return -1;
    }
}

请注意indexOf是如何调用o.equals()的。在您的情况下,o是一个String,因此您的objectList.contains将使用String.equals而不是InnerClass.equals

2
通常情况下,您需要重写hashCode()方法,但这不是主要问题。您的equals(..)方法存在不对称性。文档明确指出它应该是对称的:

它是对称的:对于任何非空引用值x和y,如果且仅当y.equals(x)返回true时,x.equals(y)应该返回true。

您观察到的是由于破坏契约而导致的意外行为。
创建一个实用程序方法,迭代所有项目并使用equals(..)方法验证字符串。
public static boolean containsString(List<InnerClass> items, String str) {
    for (InnerClass item : items) {
        if (item.getTestKey().equals(str)) {
           return true;
        }
    }
    return false;
} 

你可以使用guava的 Iterables.any(..) 方法来做类似的事情:

final String str = "Foo";
boolean contains = Iterables.any(items, new Predicate<InnerClass>() {
   @Override
   public boolean apply(InnerClass input){ 
       return input.getTestKey().equals(str);
   }
}

1

你的等于实现是错误的。你的输入参数不应该是一个String,而应该是一个InnerClass

public boolean equals(Object o) {
  if (this == o) return true;
  if (!(o instanceof InnerClass) return false;
  InnerClass that = (InnerClass)o;
  // check for null keys if you need to
  return this.testKey.equals(that.testKey);
}

(注意,instanceof null返回false,因此您无需先检查null)。

然后,您将使用以下内容测试列表中一个等效对象的存在:

objectList.contains(new InnerClass("UNIQUE ID1"));

但是,如果你真的想通过字符串键检查 InnerClass,为什么不使用 Map<String,InnerClass> 呢?


坦白地说,我已经有6年没有使用Java了,虽然我有程序员的思维方式,但我缺乏经验。由于我最初学习的是C语言,因此我还不太熟悉Maps或Java哈希。现在使用Java是因为对于小型应用程序/工具开发来说速度更快,并且我需要良好的国际化和平台独立性。我添加了一个构造函数(以及一些其他实现抽象父类的类的姐妹类),以便只使用测试密钥创建测试对象。 - K.Barad

0

你的代码存在一些问题。如果你对它不熟悉,我的建议是完全避免覆盖equals方法,而是像这样扩展一个新的实现...

class MyCustomArrayList extends ArrayList<InnerClass>{

    public boolean containsString(String value){
        for(InnerClass item : this){
            if (item.getString().equals(value){
                return true;
            }
        }
        return false;
    }

}

然后你可以做类似这样的事情

List myList = new MyCustomArrayList()
myList.containsString("some string");

我建议这样做,因为如果你重写了equals方法,也应该重写hashCode方法,而且似乎你在这个领域缺乏一些知识 - 所以我会建议你避免这种情况。

此外,contains方法调用了equals方法,这就是为什么你看到了"reached here"的原因。如果你不理解调用流程,我还是建议你避免使用它。


我理解了调用流程,但错过了它使用的是String.equals而不是InnerClass.equals。由于ArrayList不是基于哈希的,因此我认为没有必要在片段中包含hashCode。当然,我会包括一个hashCode覆盖,即使在这里它所需的只是返回testKey.hashCode()。 - K.Barad

0

这篇文章最初是在Java 8发布之前写的,但现在已经到了2017年,你可以使用新的Java 8方式来代替List.contains(...)方法,像这样:

System.out.println(objectList.stream().filter(obj -> obj.getTestKey().equals("UNIQUE ID1")).findAny().isPresent());

并为您的 TestClass 添加一个 testKey 字段的 getter 方法:

public String getTestKey() {

   return testKey;
}

这种方法的好处是您不需要修改 equals 或 hash 方法,而且在同行中看起来像个老板!


0

你的代码中有两个错误。

第一个错误: 在“objectList”对象上调用的“contains”方法应该传递一个新的InnerClass对象作为参数。

第二个错误: equals方法(应该将参数作为Object接受,并且是正确的)应该根据收到的对象适当地处理代码。 像这样:

@Override
    public boolean equals (Object in) {
        System.out.println("reached here");
        if(in == null) {
        return false;
        }else if( in instanceof InnerClass) {
        String inString = ((InnerClass)in).testKey;
        return testKey == null ? false : testKey.equals(inString);
        }else {
        return false;
        }       
    }  

0
在另一种情况下,如果您按照以下方式更改代码,则会调用您的等于方法。希望这能清楚地阐明概念。
package com.test;

import java.util.ArrayList;    
import java.util.List;

public class TestClass  {
    private static class InnerClass{    
        private final String testKey;
        //data and such

        InnerClass(String testKey, int dataStuff) {
            this.testKey =testKey;
            //etc
        }

        @Override
        public boolean equals (Object in1) {
            System.out.println("reached here");
            if(in1 == null) {
                return false;
            }else if( in1 instanceof InnerClass) {
                return ((InnerClass) this).testKey == null ? false : ((InnerClass) this).testKey.equals(((InnerClass) in1).testKey);
            }else {
                return false;
            }       
        }       
    }

    public static void main(String[] args) {    
        ArrayList<InnerClass> objectList = new ArrayList<InnerClass>();
        InnerClass in1 = new InnerClass("UNIQUE ID1", 42);
        InnerClass in2 = new InnerClass("UNIQUE ID1", 42);

        //add some entries
        objectList.add(in1);
        System.out.println( objectList.contains(in2)); 
    }    
}

0

正如许多帖子所说,问题在于list.indexOf(obj)函数调用对象的“equals”函数,而不是列表上的项。

我遇到了同样的问题,“contains()”不能满足我的需求,因为我需要知道元素在哪里!我的方法是创建一个只有要比较的参数的空元素,然后调用indexOf。

实现一个像这样的函数:

public static InnerClass empty(String testKey) {
    InnerClass in = new InnerClass();
    in.testKey =testKey;
    return in;
}

然后,像这样调用indexOf:

ind position = list.indexOf(InnerClass.empty(key));

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