如何基于谓词过滤Java集合?

766

我希望基于谓词对java.util.Collection进行筛选。

30个回答

845

Java 8(2014)使用流和Lambda仅需一行代码即可解决此问题:

List<Person> beerDrinkers = persons.stream()
    .filter(p -> p.getAge() > 16).collect(Collectors.toList());

这是一个教程

使用Collection#removeIf直接修改集合。 (注意:在这种情况下,谓词将删除满足谓词条件的对象):

persons.removeIf(p -> p.getAge() <= 16);

lambdaj可以让您在不编写循环或内部类的情况下过滤集合:

List<Person> beerDrinkers = select(persons, having(on(Person.class).getAge(),
    greaterThan(16)));

你能想象出比这更易读的东西吗?

免责声明:我是lambdaj的贡献者。


38
不错,但静态导入使情况变得含糊不清。作为参考,ch.lambdaj.Lambda上的select/having/on是静态导入,而greaterThan是org.hamcrest.Matchers。 - MikePatel
11
LambdaJ非常吸引人,但值得注意的是它会带来显著的开销(平均为2.6):https://code.google.com/p/lambdaj/wiki/PerformanceAnalysis。 - Doc Davluz
7
似乎在Android上无效:https://groups.google.com/forum/#!msg/lambdaj/km7uFgvSd3k/grJhgl3ik5sJ - Moritz
9
喜欢这个LamdaJ的例子......类似于.NET内置的Lambda函数。16岁时一个人能在哪里喝酒?我们应该考虑添加本地化限制。 :P - MAbraham1
3
示例中的removeIf应该是 persons.removeIf(p -> p.getAge() <= 16); - vim
显示剩余13条评论

229
假设您正在使用Java 1.5,且无法添加Google Collections,我会采用与Google工程师类似的方法。 这是对Jon评论的轻微变化。
首先,将此接口添加到您的代码库中。
public interface IPredicate<T> { boolean apply(T type); }

实现者可以在特定类型满足某个谓词时进行回答。例如,如果 TUserAuthorizedUserPredicate<User> 实现了 IPredicate<T>,那么 AuthorizedUserPredicate#apply 返回传入的 User 是否被授权。

然后,在某个实用类中,您可以这样说:

public static <T> Collection<T> filter(Collection<T> target, IPredicate<T> predicate) {
    Collection<T> result = new ArrayList<T>();
    for (T element: target) {
        if (predicate.apply(element)) {
            result.add(element);
        }
    }
    return result;
}

假设您可以使用上述内容

Predicate<User> isAuthorized = new Predicate<User>() {
    public boolean apply(User user) {
        // binds a boolean method in User to a reference
        return user.isAuthorized();
    }
};
// allUsers is a Collection<User>
Collection<User> authorizedUsers = filter(allUsers, isAuthorized);

如果在线性检查方面表现很重要,那我可能需要一个拥有目标集合的领域对象。 拥有目标集合的领域对象将具有初始化、添加和设置目标集合的方法的过滤逻辑。
更新:
在实用类中(假设是Predicate),我添加了一个select方法,其中包含当谓词未返回预期值时使用默认值的选项,还添加了一个静态属性用于在新的IPredicate内部使用的参数。
public class Predicate {
    public static Object predicateParams;

    public static <T> Collection<T> filter(Collection<T> target, IPredicate<T> predicate) {
        Collection<T> result = new ArrayList<T>();
        for (T element : target) {
            if (predicate.apply(element)) {
                result.add(element);
            }
        }
        return result;
    }

    public static <T> T select(Collection<T> target, IPredicate<T> predicate) {
        T result = null;
        for (T element : target) {
            if (!predicate.apply(element))
                continue;
            result = element;
            break;
        }
        return result;
    }

    public static <T> T select(Collection<T> target, IPredicate<T> predicate, T defaultValue) {
        T result = defaultValue;
        for (T element : target) {
            if (!predicate.apply(element))
                continue;
            result = element;
            break;
        }
        return result;
    }
}

下面的示例查找集合之间缺失的对象:
List<MyTypeA> missingObjects = (List<MyTypeA>) Predicate.filter(myCollectionOfA,
    new IPredicate<MyTypeA>() {
        public boolean apply(MyTypeA objectOfA) {
            Predicate.predicateParams = objectOfA.getName();
            return Predicate.select(myCollectionB, new IPredicate<MyTypeB>() {
                public boolean apply(MyTypeB objectOfB) {
                    return objectOfB.getName().equals(Predicate.predicateParams.toString());
                }
            }) == null;
        }
    });

下面的示例在集合中查找实例,并在未找到实例时将集合的第一个元素作为默认值返回:
MyType myObject = Predicate.select(collectionOfMyType, new IPredicate<MyType>() {
public boolean apply(MyType objectOfMyType) {
    return objectOfMyType.isDefault();
}}, collectionOfMyType.get(0));

Java 8发布后更新:

距离我(Alan)第一次发布此答案已经过去了几年,我仍然无法相信我正在为此答案收集SO积分。无论如何,现在Java 8已经将闭包引入语言中,我的答案现在会更加简单明了。使用Java 8,不需要一个独立的静态实用程序类。因此,如果您想要找到与谓词匹配的第一个元素。

final UserService userService = ... // perhaps injected IoC
final Optional<UserModel> userOption = userCollection.stream().filter(u -> {
    boolean isAuthorized = userService.isAuthorized(u);
    return isAuthorized;
}).findFirst();

JDK 8针对可选项的API具有get()isPresent()orElse(defaultUser)orElseGet(userSupplier)orElseThrow(exceptionSupplier)的能力,以及其他“单子”功能,如mapflatMapfilter

如果您只想收集与谓词匹配的所有用户,则使用Collectors将流终止在所需的集合中。

final UserService userService = ... // perhaps injected IoC
final List<UserModel> userOption = userCollection.stream().filter(u -> {
    boolean isAuthorized = userService.isAuthorized(u);
    return isAuthorized;
}).collect(Collectors.toList());

请点击这里查看更多关于Java 8流的示例。


28
是的,但我不想再次重复地重新发明轮子。我宁愿找到一些可以满足我需求的实用程序库。 - Kevin Wong
2
如果您不想要新的集合,那么这并不是最好的方法。使用过滤器迭代器隐喻,它可以输入到一个新的集合中,或者它可能是您所需要的全部。 - Josh
@Nestor:在Scala的推导式中,过滤会更简单:val authorized = for (user <- users if user.isAuthorized) yield user - Alan
这个方法会修改原始集合还是创建一个全新的集合?我尝试使用了这个方法并记录了我的两个集合(原始集合和从方法返回的集合),它们是相同的。@Alan - Rohan
你知道这个方法是否意在修改原始集合吗?我尝试使用此方法,结果过滤了我的原始集合。 - Rohan
1
@Rohan,这并不意味着要改变原始集合。请注意,上面的结果集合是新构建的,而过滤方法仅在谓词适用时向结果集合添加内容。 - Alan

96

3
这样做可以,但它不是通用的,并且会直接修改集合(不太好)。 - Kevin Wong
3
在CollectionUtils中还有其他过滤方法,这些方法不会修改原始的集合。 - skaffman
44
特别是那种不会直接修改集合的方法是org.apache.commons.collections.CollectionUtils#select(Collection,Predicate)。 - Eero
5
在Commons Collections v4中,现在使用泛型。 - Justin Emery
1
这种方法应该谨慎使用,因为它依赖于iterator.remove()方法(至少在commons-collections-3.2.1的实现中),而该方法对集合来说是可选的。因此,如果你尝试过滤一个数组,可能会出现UnsupportedOperationException异常。 - user2417480

68
“最好”的方式太过宽泛了。是要“最短”?“最快”?还是“易读性强”? 是在原地筛选还是放入其他集合中?
最简单的方法(但不是最易读的)是迭代并使用Iterator.remove()方法:
Iterator<Foo> it = col.iterator();
while( it.hasNext() ) {
  Foo foo = it.next();
  if( !condition(foo) ) it.remove();
}

现在,为了使其更易读,您可以将其包装到一个实用方法中。然后发明一个IPredicate接口,创建该接口的匿名实现,并执行以下操作:

CollectionUtils.filterInPlace(col,
  new IPredicate<Foo>(){
    public boolean keepIt(Foo foo) {
      return foo.isBar();
    }
  });

在 filterInPlace() 中,迭代集合并调用 Predicate.keepIt() 方法来确定是否保留集合中的实例。

我真的不认为为了这个任务而引入第三方库是有道理的。


7
我支持这个方案:它可以正常工作,而且不需要使用外部库。我从没想到实例化一个Iterator比使用for-each语法更有用,或者你可以在不出现ConcurrentModificationException或类似问题的情况下从列表中删除项目。 :) - ZeroOne
1
我认为这是使用标准Java库的最佳方式,而无需复制。对于1.8版本,将会有“stream()”特性,但并不是每个人都可以玩最新的玩具:P - Populus
这会修改原始集合吗?@ZeroOne - Rohan
当然可以,@Rohan。如果你不相信的话,可以试一试。 ;) - ZeroOne
@Rohan,好的,你想要一个新列表?请参考gavenkoa的Java 8答案。 - ZeroOne
显示剩余2条评论

62
考虑使用Google Collections来更新支持泛型的集合框架。 更新: Google收藏库现已弃用。 您应该改用最新版本的Guava。 它仍然具有所有相同的集合框架扩展,包括基于谓词的过滤机制。

是的,我知道Google Collections库。我使用的版本没有Collections2。我在这个问题中添加了一个新答案,列出了具体的方法。 - Kevin Wong
7
Kevin,Iterables.filter() 和 Iterators.filter() 从一开始就存在,并且通常是您所需要的全部。 - Kevin Bourrillion

32

等待Java 8的到来:

List<Person> olderThan30 = 
  //Create a Stream from the personList
  personList.stream().
  //filter the element to select only those with age >= 30
  filter(p -> p.age >= 30).
  //put those filtered elements into a new List.
  collect(Collectors.toList());

21
啊...这太啰嗦了。为什么他们不能这样做:List<Person> result = personList.filter(p -> p.age > 30)? - Kevin Wong
8
要直接在集合(Collection)上使用过滤器(filter),需要使用removeIf方法调用:http://download.java.net/jdk8/docs/api/java/util/Collection.html#removeIf%28java.util.function.Predicate%29 - gavenkoa
7
“verbose” 这个词大概可以描述整个语言吧。至少它们保持了一致性? - Rogue
5
为什么最后一部分不使用Collectors.toList()? - Nestor Hernandez Loli
3
这是 gavenkoa 提供的一个链接,指向一个不会 404 的页面:HerepersonList.removeIf(p -> p.age < 30); 简洁些。另外,我听说开始实现接受和返回 Stream 而不是 Collection 的 API,因为 Stream 非常有用且快速,但是转换它们的速度很慢。 - Captain Man
显示剩余2条评论

11

自Java 8早期版本发布以来,您可以尝试类似以下的操作:

Collection<T> collection = ...;
Stream<T> stream = collection.stream().filter(...);
例如,如果您有一个整数列表,并且想要过滤掉大于10的数字,然后将这些数字打印到控制台上,您可以像这样做:
List<Integer> numbers = Arrays.asList(12, 74, 5, 8, 16);
numbers.stream().filter(n -> n > 10).forEach(System.out::println);

11

我会提出 RxJava,它也可在Android上使用。 如果您希望在集合中添加更多转换或处理过滤时出现的错误,RxJava可能并不总是最佳选择,但它会给您带来更多的灵活性。

Observable.from(Arrays.asList(1, 2, 3, 4, 5))
    .filter(new Func1<Integer, Boolean>() {
        public Boolean call(Integer i) {
            return i % 2 != 0;
        }
    })
    .subscribe(new Action1<Integer>() {
        public void call(Integer i) {
            System.out.println(i);
        }
    });

输出:

1
3
5

有关RxJava的filter的更多详细信息可以在此处找到。


7
自从Java 9开始,可以使用Collectors.filtering
public static <T, A, R>
    Collector<T, ?, R> filtering(Predicate<? super T> predicate,
                                 Collector<? super T, A, R> downstream)

因此,过滤应该是这样的:
collection.stream().collect(Collectors.filtering(predicate, collector))

例子:

List<Integer> oddNumbers = List.of(1, 19, 15, 10, -10).stream()
            .collect(Collectors.filtering(i -> i % 2 == 1, Collectors.toList()));

7

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