Java List.contains(Object with field value equal to x) Java列表.contains(字段值等于x的对象)

291

我想检查一个List是否包含一个具有特定值字段的对象。现在,我可以使用循环来遍历并检查,但我很好奇是否有更高效的代码方式。

类似于:

if(list.contains(new Object().setName("John"))){
    //Do some stuff
}

我知道上面的代码没有任何作用,只是为了粗略地演示我所想要实现的内容。

另外,为了澄清,我不想使用简单循环的原因是,这段代码目前将进入一个嵌套循环中,该嵌套循环又位于另一个嵌套循环内部。为了可读性,我不想在这些循环中添加循环。因此,我想知道是否有任何简单的替代方法。


6
由于这是自定义相等性,您将需要编写自定义代码。 - Sotirios Delimanolis
你所说的目标和代码示例似乎不匹配。你只想根据一个字段值比较对象吗? - Duncan Jones
1
你想覆盖自定义对象的 equals(Object) 方法吗? - Josh M
1
for(Person p:list) if (p.getName().equals("John") return true; return false;很抱歉,在Java中您不会找到更简洁的方法。 - MikeFHay
@Rajdeep 抱歉,我不理解你的问题。p.equals(p) 应该总是为真,所以我不明白你想要实现什么。希望如果你提出一个新问题,你可以得到更好的帮助。 - MikeFHay
14个回答

394

数据流

如果您正在使用Java 8,您可以尝试以下方式:

public boolean containsName(final List<MyObject> list, final String name){
    return list.stream().filter(o -> o.getName().equals(name)).findFirst().isPresent();
}

或者,您可以尝试像这样的东西:

public boolean containsName(final List<MyObject> list, final String name){
    return list.stream().map(MyObject::getName).filter(name::equals).findFirst().isPresent();
}

如果List<MyObject>包含名称为nameMyObject,则此方法将返回true。如果您想对每个满足getName().equals(name)条件的MyObject执行操作,则可以尝试以下示例:

public void perform(final List<MyObject> list, final String name){
    list.stream().filter(o -> o.getName().equals(name)).forEach(
            o -> {
                //...
            }
    );
}

其中o代表一个MyObject实例。

另外,如评论所示(感谢MK10),您可以使用Stream#anyMatch 方法:

public boolean containsName(final List<MyObject> list, final String name){
    return list.stream().anyMatch(o -> name.equals(o.getName()));
}

2
然后,第二个例子应该是 public boolean。此外,如果 o.getName() 可能为 null,您可能希望使用 Objects.equals() - Eric Jablow
1
我只是一个多疑的程序员。我处理过那些人对null毫不在意的项目,因此我倾向于采取防御性措施。确保项目从来不会出现null,这样会更好。 - Eric Jablow
5
也许你应该评论所有没有执行“null”检查的答案,而不仅仅是一个(我的)。 - Josh M
79
return list.stream().anyMatch(o -> o.getName().equals(name)); 翻译:返回一个布尔值,表示列表中是否存在名称与给定名称相同的对象。 - MK10
4
我同意 @MK10 的观点,但我会使用 java.util.Objects 来进行安全比较。代码如下:return list.stream().anyMatch(o -> Objects.equals(o.getName(), name)); 如果你想要在流中检查空对象,可以在 anyMatch 之前添加 .filter(Objects::nonNull) - tobijdc
显示剩余7条评论

85

你有两个选择。

1. 第一个选择是覆盖你的Object类中的`equals()`方法,这是更可取的选择。

例如,假设你有这个Object类:

public class MyObject {
    private String name;
    private String location;
    //getters and setters
}

现在假设你只关心MyObject的名称,并且希望确保它是唯一的,因此如果两个`MyObject`具有相同的名称,则应将它们视为相等。在这种情况下,您需要重写`equals()`方法(以及`hashcode()`方法),以便比较名称以确定相等性。

完成后,您可以通过以下方式检查集合是否包含名称为“foo”的MyObject:

MyObject object = new MyObject();
object.setName("foo");
collection.contains(object);

然而,如果:

  • 您同时使用名称和位置来检查相等性,但只想检查集合中是否有某个特定位置的`MyObject`。在这种情况下,您已经覆盖了`equals()`方法。
  • `MyObject`是您无法更改的API的一部分。

如果两者中有任何一种情况,您需要选择第二个选项:

2. 编写自己的实用程序方法:

public static boolean containsLocation(Collection<MyObject> c, String location) {
    for(MyObject o : c) {
        if(o != null && o.getLocation.equals(location)) {
            return true;
        }
    }
    return false;
}

另一种方法是扩展ArrayList(或其他集合),然后添加自己的方法:

public boolean containsLocation(String location) {
    for(MyObject o : this) {
        if(o != null && o.getLocation.equals(location)) {
                return true;
            }
        }
        return false;
    }

不幸的是,没有更好的方法来避免这个问题。


我认为你在if语句中的getter方法忘记加括号了,应该是if(o != null && o.getLocation().equals(location))。 - op_

50

这是使用Java 8+的方法:

boolean isJohnAlive = list.stream().anyMatch(o -> "John".equals(o.getName());

4
尝试了Josh提供的基于过滤器的答案,但Intellij还建议使用您的anyMatch答案。太好了! - Thyag
4
请将字符串相等性检查翻转,以避免潜在的空指针异常。o.getName().equals("John") - B Thuy
在那个相等性检查中,哪一个应该先出现? - undefined

26

Google Guava

如果您正在使用Guava,您可以采取一种功能性的方法,然后执行以下操作。

FluentIterable.from(list).find(new Predicate<MyObject>() {
   public boolean apply(MyObject input) {
      return "John".equals(input.getName());
   }
}).Any();

看起来有点啰嗦。然而,谓词是一个对象,你可以为不同的搜索提供不同的变体。请注意,该库本身将集合的迭代和你想要应用的函数分开。你不必为特定行为覆盖 equals()

如下所述,Java 8及更高版本内置的java.util.Stream框架提供了类似的功能。


1
或者你可以使用 Iterables.any(list, predicate),它的实际效果相同,但你可能会根据风格喜好而选择使用它。 - MikeFHay
@EricJablow 是的。 :) 你可以看看我的解决方案。 - Josh M

22

Collection.contains() 是通过对每个对象调用 equals() 直到返回 true 来实现的。

因此,一种实现方法是重写 equals(),但显然你只能拥有一个 equals 方法。

Guava 这样的框架因此使用 predicates。使用 Iterables.find(list, predicate),您可以通过将测试放入 predicate 中来搜索任意字段。

其他基于 VM 构建的语言已经内置了此功能。例如,在 Groovy 中,您只需要编写:

def result = list.find{ it.name == 'John' }

Java 8 使我们所有人的生活更加轻松:

List<Foo> result = list.stream()
    .filter(it -> "John".equals(it.getName())
    .collect(Collectors.toList());

如果你关心这些事情,我建议阅读《超越Java》这本书。它提供了许多关于Java的不足之处以及其他语言如何更好地处理问题的例子。


那么,如果我覆盖了添加到列表中的自定义对象的equals方法...如果某些类变量相同,我能让它只返回true吗? - Rudi Kershaw
1
不,equals() 的想法非常有限。它的意思是检查“对象标识” - 对于某些类的对象可能意味着什么。如果您添加了一个标志来包含哪些字段,那可能会引起各种麻烦。我强烈反对这样做。 - Aaron Digulla
喜欢 Groovy,所以“新”的 Java 8 Lambda 还没有给我们带来这种酷炫的感觉吗?def result = list.find{ it.name == 'John' } Oracle/JCP 应该因对程序员的战争罪行而受到起诉。 - ken

19

二分查找

您可以使用Collections.binarySearch在列表中查找元素(假设该列表已排序):

Collections.binarySearch(list, new YourObject("a1", "b",
                "c"), new Comparator<YourObject>() {

            @Override
            public int compare(YourObject o1, YourObject o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });

如果对象不在集合中,它将返回一个负数,否则它将返回对象的index。使用它,您可以使用不同的搜索策略搜索对象。


这是一个适用于小型项目的绝佳解决方案,不需要使用guava。但有一个问题,binarySearch文档中指出:“在调用此方法之前,必须根据指定的比较器将列表按升序排序。如果未排序,则结果是未定义的”。但是在某些情况下,根据比较的内容,似乎并非如此? - chrismarx
负数表示如果您添加并重新排序,则对象将插入的位置。 - fiorentinoing

8

Map

您可以创建一个Hashmap<String, Object>,使用其中一个值作为键,然后查看yourHashMap.keySet().contains(yourValue)是否返回true来判断是否包含该值。


虽然在一开始将细节放入地图中会增加一些开销,但随后您可以获得常数时间查找。我很惊讶直到现在还没有人提出这个建议。 - Rudi Kershaw

6

Eclipse Collections

如果您使用的是Eclipse Collections,您可以使用anySatisfy()方法。如果可能的话,请将您的List适配为ListAdapter或将您的List更改为ListIterable

ListIterable<MyObject> list = ...;

boolean result =
    list.anySatisfy(myObject -> myObject.getName().equals("John"));

如果您经常进行此类操作,最好提取一个方法来回答该类型是否具有属性。
public class MyObject
{
    private final String name;

    public MyObject(String name)
    {
        this.name = name;
    }

    public boolean named(String name)
    {
        return Objects.equals(this.name, name);
    }
}

您可以使用替代形式anySatisfyWith()与方法引用一起使用。
boolean result = list.anySatisfyWith(MyObject::named, "John");

如果您不能将您的List转换为ListIterable,那么您可以使用ListAdapter
boolean result = 
    ListAdapter.adapt(list).anySatisfyWith(MyObject::named, "John");

注意:我是Eclipse集合的提交者。

5

Predicate

Predicate是一个Java函数式接口,用于定义一个可以被应用于某个对象的布尔型测试。如果该测试返回true,则Predicate表示匹配成功;否则,表示匹配失败。

如果您不使用Java 8或类库来处理集合,可以实现一些比您的解决方案更可重用的功能。

interface Predicate<T>{
        boolean contains(T item);
    }

    static class CollectionUtil{

        public static <T> T find(final Collection<T> collection,final  Predicate<T> predicate){
            for (T item : collection){
                if (predicate.contains(item)){
                    return item;
                }
            }
            return null;
        }
    // and many more methods to deal with collection    
    }

我正在使用类似的方法,我有谓词接口,并将其实现传递给我的工具类。
这样做的好处是什么?您只需编写一个方法即可处理任何类型的集合中的搜索。如果要按不同字段搜索,则无需创建单独的方法。您所需要做的就是提供不同的谓词,它可以在不再有用时被销毁。
如果要使用它,您只需要调用该方法并定义自己的谓词即可。
CollectionUtil.find(list, new Predicate<MyObject>{
    public boolean contains(T item){
        return "John".equals(item.getName());
     }
});

4

下面是一种使用Guava的解决方案。

private boolean checkUserListContainName(List<User> userList, final String targetName){

    return FluentIterable.from(userList).anyMatch(new Predicate<User>() {
        @Override
        public boolean apply(@Nullable User input) {
            return input.getName().equals(targetName);
        }
    });
}

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