如何在Java 8中动态进行过滤?

50

我知道在Java 8中,我可以这样过滤:

List<User> olderUsers = users.stream().filter(u -> u.age > 30).collect(Collectors.toList());

但是如果我有一个集合和六个过滤条件,而我想测试这些条件的组合呢?

例如,我有一个对象集合,并有以下几个过滤条件:

<1> Size
<2> Weight
<3> Length
<4> Top 50% by a certain order
<5> Top 20% by a another certain ratio
<6> True or false by yet another criteria

我希望测试上述标准的组合,例如:

<1> -> <2> -> <3> -> <4> -> <5>
<1> -> <2> -> <3> -> <5> -> <4>
<1> -> <2> -> <5> -> <4> -> <3>
...
<1> -> <5> -> <3> -> <4> -> <2>
<3> -> <2> -> <1> -> <4> -> <5>
...
<5> -> <4> -> <3> -> <3> -> <1>

如果每个测试订单可能给我不同的结果,如何编写循环以自动筛选所有组合?

我想到的方法是使用另一种生成测试订单的方法,如下所示:

int[][] getTestOrder(int criteriaCount)
{
 ...
}

So if the criteriaCount is 2, it will return : {{1,2},{2,1}}
If the criteriaCount is 3, it will return : {{1,2,3},{1,3,2},{2,1,3},{2,3,1},{3,1,2},{3,2,1}}
...

那么如何在Java 8带有的简明表达式过滤机制中最有效地实现它呢?


4
过滤器的顺序如何改变结果? - skiwi
创建一个“幂集”,包含所有组合,并对每个组合进行流/过滤。 - assylias
也许您可以简单地组合谓词并使用单个过滤器,即 Predicate<T> p = p1.and(p2).and(p3).and(p4) - Edwin Dalorzo
4
大多数简单的过滤器(例如 u.age() > 30)是与上下文无关的,所以过滤器的顺序并不重要。然而,OP有其他的“过滤器”是与上下文相关的,比如“某一标准下的前50%”。这些过滤器依赖于输入集合和过滤器的顺序。不幸的是,使用标准的流filter()操作表达这种条件并不明显。 - Stuart Marks
@ Edwin Dalorzo: 我的想法跟你一样,但不知道如何用Java表达式来实现,有没有示例代码可以展示给我看如何做呢?特别是当涉及到前20%的部分时。 - Frank
2个回答

112
有趣的问题。这里有几个问题。毫无疑问,使用Haskell或Lisp不到半页纸就可以解决,但这是Java,所以我们开始吧...。
一个问题是,我们有可变数量的过滤器,而大多数已经展示的示例都是固定的管道。
另一个问题是,一些OP的“过滤器”具有上下文敏感性,例如“按某种顺序排名前50%”。在流的简单filter(predicate)构造中无法完成此操作。
关键是要认识到,虽然lambda允许将函数作为参数传递(效果很好),但这也意味着它们可以存储在数据结构中,并且可以对它们执行计算。最常见的计算是取多个函数并将它们组合起来。
假设被操作的值是Widget的实例,Widget是一个具有一些明显的getter方法的POJO:
class Widget {
    String name() { ... }
    int length() { ... }
    double weight() { ... }

    // constructors, fields, toString(), etc.
}

让我们从第一个问题开始,找出如何处理可变数量的简单谓词。我们可以像这样创建谓词列表:

List<Predicate<Widget>> allPredicates = Arrays.asList(
    w -> w.length() >= 10,
    w -> w.weight() > 40.0,
    w -> w.name().compareTo("c") > 0);

给定这个列表,我们可以对它们进行排列组合(可能没有用,因为它们是无序的),或者选择任意子集。假设我们只想应用所有谓词,如何将变量数量的谓词应用于流?有一个 Predicate.and() 方法,它将接受两个谓词并使用逻辑 and 进行组合,返回一个单独的谓词。因此,我们可以取第一个谓词,并编写一个循环来将其与后续谓词相结合,以建立一个由它们全部组成的复合and谓词:

Predicate<Widget> compositePredicate = allPredicates.get(0);
for (int i = 1; i < allPredicates.size(); i++) {
    compositePredicate = compositePredicate.and(allPredicates.get(i));
}

这个方法确实有效,但如果列表为空会失败,而且由于我们现在正在进行的是函数式编程,所以在循环中更改变量是过时的。但是!这是一个约简操作!我们可以将所有谓词缩减为运算符得到一个复合谓词,如下所示:

Predicate<Widget> compositePredicate =
    allPredicates.stream()
                 .reduce(w -> true, Predicate::and);

(信用:我从@venkat_s那里学到了这个技巧。如果你有机会的话,去听他在会议上的演讲吧,他很厉害。)

请注意将w -> true作为reduction的身份值的用法。(这也可以用作循环的compositePredicate的初始值,这样就可以解决零长度列表的情况。)

现在我们有了复合谓词,我们可以编写一个简短的管道,仅将复合谓词应用于小部件:

widgetList.stream()
          .filter(compositePredicate)
          .forEach(System.out::println);

上下文敏感过滤器

现在我们考虑所谓的“上下文敏感”过滤器,例如表示为“按某种顺序的前50%”,例如重量最高的50%部件。虽然这不是最好的术语,但它与流中到此为止的元素数量有关。

如何使用流实现这样的过滤器呢?除非有人想出了什么聪明的方法,否则我认为我们必须先将元素收集到某个地方(比如列表)才能将第一个元素发射到输出中。 这有点像管道中的sorted() ,在排序所有输入元素之前无法确定要输出的第一个元素。

使用流找到按重量排名前50%的部件的简单方法如下:

List<Widget> temp =
    list.stream()
        .sorted(comparing(Widget::weight).reversed())
        .collect(toList());
temp.stream()
    .limit((long)(temp.size() * 0.5))
    .forEach(System.out::println);

这并不复杂,但有点繁琐,因为我们必须将元素收集到列表中并分配给变量,才能在50%计算中使用列表的大小。

然而,这种过滤的“静态”表示有限。如果我们想像谓词一样将其链接成具有可变数量元素(其他过滤器或条件)的流,该怎么办呢?

一个重要的观察是,这段代码实际上是在消耗流和发射流之间进行工作。它碰巧在中间有一个收集器,但如果您将一个流链接到其前面并从其后端链接一些内容,那就没人知道了。事实上,标准的流管道操作,如mapfilter,每个都将流作为输入并发出流作为输出。所以我们可以自己编写类似这样的函数:

Stream<Widget> top50PercentByWeight(Stream<Widget> stream) {
    List<Widget> temp =
        stream.sorted(comparing(Widget::weight).reversed())
              .collect(toList());
    return temp.stream()
               .limit((long)(temp.size() * 0.5));
}

一个类似的例子可能是寻找最短的三个小部件:

Stream<Widget> shortestThree(Stream<Widget> stream) {
    return stream.sorted(comparing(Widget::length))
                 .limit(3);
}

现在我们可以编写结合这些有状态过滤器和普通流操作的内容:

shortestThree(
    top50PercentByWeight(
        widgetList.stream()
                  .filter(w -> w.length() >= 10)))
.forEach(System.out::println);

这样做是有效的,但读起来有点糟糕,因为它是“从里到外”并且是倒序的。流源是widgetList,通过普通谓词进行流式过滤。现在,往回走,应用了前50%的过滤器,然后应用了最短的三个过滤器,最后在末尾应用了流操作forEach。这是有效的,但阅读起来相当令人困惑。而且它还是静态的。我们真正想要的是有一种方法将这些新过滤器放入我们可以操纵的数据结构中,以便例如运行所有排列,就像原始问题中一样。

此时的关键洞察力是,这些新类型的过滤器实际上只是函数,而我们在Java中有函数接口类型,可以将函数表示为对象,以便操纵它们,将它们存储在数据结构中,组合它们等等。获取一个参数类型为某个类型并返回相同类型值的函数接口类型是UnaryOperator。在这种情况下,参数和返回类型是Stream<Widget>。如果我们采用方法引用,例如this::shortestThreethis::top50PercentByWeight,那么结果对象的类型将是

UnaryOperator<Stream<Widget>>
如果我们将它们列入列表中,该列表的类型将为:
List<UnaryOperator<Stream<Widget>>>

呃!嵌套三层的泛型对我来说太多了。(但是Aleksey Shipilev曾经向我展示过一个使用四个嵌套泛型的代码.) 太多泛型的解决方案是定义我们自己的类型。让我们称之为Criterion中的一种新功能。结果发现,将我们的新功能接口类型与UnaryOperator相关联没有太多价值,因此我们的定义可以简单地如下:

@FunctionalInterface
public interface Criterion {
    Stream<Widget> apply(Stream<Widget> s);
}

现在我们可以创建如下的条件列表:

List<Criterion> criteria = Arrays.asList(
    this::shortestThree,
    this::lengthGreaterThan20
);

(我们将在下面解释如何使用此列表。)这是一个进步,因为现在我们可以动态地操作列表,但它仍然有些限制。首先,它不能与普通谓词结合使用。其次,这里有很多硬编码的值,比如最短的三个:两个或四个怎么办?除了长度之外,还有其他标准吗?我们真正想要的是一个为我们创建这些Criterion对象的函数。这很容易使用lambda实现。

这将创建一个根据比较器选择前N个小部件的标准:

Criterion topN(Comparator<Widget> cmp, long n) {
    return stream -> stream.sorted(cmp).limit(n);
}

使用比较器创建一个标准,以便选择前p个百分比的小部件:

Criterion topPercent(Comparator<Widget> cmp, double pct) {
    return stream -> {
        List<Widget> temp =
            stream.sorted(cmp).collect(toList());
        return temp.stream()
                   .limit((long)(temp.size() * pct));
    };
}

这将从普通谓词创建一个标准:

Criterion fromPredicate(Predicate<Widget> pred) {
    return stream -> stream.filter(pred);
}

现在我们有了一种非常灵活的方法来创建条件并将它们放入列表中,可以对它们进行子集或排列等操作:

List<Criterion> criteria = Arrays.asList(
    fromPredicate(w -> w.length() > 10),                    // longer than 10
    topN(comparing(Widget::length), 4L),                    // longest 4
    topPercent(comparing(Widget::weight).reversed(), 0.50)  // heaviest 50%
);

一旦我们有了Criterion对象列表,我们需要找出一种方法来应用所有的Criterion。再次,我们可以使用我们的好朋友reduce将它们全部组合成一个单独的Criterion对象:

Criterion allCriteria =
    criteria.stream()
            .reduce(c -> c, (c1, c2) -> (s -> c2.apply(c1.apply(s))));

身份函数c -> c非常明确,但第二个参数有点棘手。给定流s,我们首先应用标准 c1 ,然后应用标准 c2 ,并将其包装在一个lambda中,该lambda接受两个Criterion对象 c1 和 c2 ,并返回应用c1和c2的组合到流的lambda,并返回结果流。

现在我们已经组合了所有标准,我们可以将其应用于窗口小部件流,如下所示:

allCriteria.apply(widgetList.stream())
           .forEach(System.out::println);

这还有点里外颠倒,但它相当可控。最重要的是,它回答了最初的问题,即如何动态组合条件。一旦标准对象在数据结构中,它们可以被选择、子集化、排列或任何必要的方式进行操作,并且它们都可以被组合成单个标准并使用上述技术应用于流。

函数式编程大师们可能会说:“他刚刚重新发明了……!”这可能是真的。我敢肯定,这已经在某个地方被发明了,但在Java之前,由于lambda,不可能编写使用这些技术的Java代码。

更新2014-04-07

我清理了并发布了完整的示例代码


1
哇!!!这就是我要说的。做得太棒了!这是我认为可以用Java 8实现的东西,但我不太知道如何编写代码,它非常强大!谢谢。 - Frank
1
@Frank 谢谢!我已经发布了一个gist,里面有示例代码。我看到你还有一些其他的问题,但是看起来你已经删除了它们,所以我假设你已经解决了它们。 - Stuart Marks
1
@Jakub 谢谢!是的,我已经修复了“t1.size()”的拼写错误。在编写 Criterion 的时候,您可能没有原始列表可供调用 size()。您所拥有的只是作为参数传递给您的 Stream,并且源和您之间可能会有上游过滤器。如果在该流上调用 count(),它将耗尽该流,因此您无法获得要作为输出流发出的正确元素集。将元素收集到临时列表中似乎是继续进行的唯一方法。 - Stuart Marks
2
太好了!现在任何对这个主题感兴趣的人都可以轻松地测试和学习你的示例代码,谢谢!我希望有一天你发明的这些“轮子”[topPercentFromRange、topPercent、topN...]可以成为标准Java语言功能的一部分,并得到改进,这样我们就不必在自己的库中携带它们了。 - Frank
2
@skomi 当然,过滤器可以重载以接受谓词的集合(或流),但这似乎是一个罕见的情况,不值得包含在API中。如果谓词在编译时都已知,则可以使用&&运算符将它们重构为单个谓词。但是,如果直到运行时才知道确切的谓词集合,则需要在运行时将它们组合起来,这就是reduce()派上用场的地方。 - Stuart Marks
显示剩余8条评论

2
我们可以使用一个映射表来添加计数器,以便在过滤后知道有多少元素。我创建了一个帮助类,其中包含一个计数并返回传递的相同对象的方法:
class DoNothingButCount<T> {
    AtomicInteger i;
    public DoNothingButCount() {
        i = new AtomicInteger(0);
    }
    public T pass(T p) {
        i.incrementAndGet();
        return p;
    }
}

public void runDemo() {
    List<Person>persons = create(100);
    DoNothingButCount<Person> counter = new DoNothingButCount<>();

    persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
            map((p) -> counter.pass(p)).
            sorted((p1, p2) -> p1.age - p2.age).
            collect(Collectors.toList()).stream().
            limit((int) (counter.i.intValue() * 0.5)).
            sorted((p1, p2) -> p2.length - p1.length).
            limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
}

我不得不在中间将流转换为列表,然后再转回流,因为限制会否则使用初始计数。这有点“hackish”,但这是我能想到的全部。

我可以使用映射类的函数稍微不同地完成它:

class DoNothingButCount<T > implements Function<T, T> {
    AtomicInteger i;
    public DoNothingButCount() {
        i = new AtomicInteger(0);
    }
    public T apply(T p) {
        i.incrementAndGet();
        return p;
    }
}

流中唯一会改变的是:
            map((p) -> counter.pass(p)).

will become:

            map(counter).

我的完整测试类,包括两个示例:

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Demo2 {
    Random r = new Random();
    class Person {
        public int size, weitght,length, age;
        public Person(int s, int w, int l, int a){
            this.size = s;
            this.weitght = w;
            this.length = l;
            this.age = a;
        }
        public String toString() {
            return "P: "+this.size+", "+this.weitght+", "+this.length+", "+this.age+".";
        }
    }

    public List<Person>create(int size) {
        List<Person>persons = new ArrayList<>();
        while(persons.size()<size) {
            persons.add(new Person(r.nextInt(10)+10, r.nextInt(10)+10, r.nextInt(10)+10,r.nextInt(20)+14));
        }
        return persons;
    }

    class DoNothingButCount<T> {
        AtomicInteger i;
        public DoNothingButCount() {
            i = new AtomicInteger(0);
        }
        public T pass(T p) {
            i.incrementAndGet();
            return p;
        }
    }

    class PDoNothingButCount<T > implements Function<T, T> {
        AtomicInteger i;
        public PDoNothingButCount() {
            i = new AtomicInteger(0);
        }
        public T apply(T p) {
            i.incrementAndGet();
            return p;
        }
    }

    public void runDemo() {
        List<Person>persons = create(100);
        PDoNothingButCount<Person> counter = new PDoNothingButCount<>();

        persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
                map(counter).
                sorted((p1, p2) -> p1.age - p2.age).
                collect(Collectors.toList()).stream().
                limit((int) (counter.i.intValue() * 0.5)).
                sorted((p1, p2) -> p2.length - p1.length).
                limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
    }

    public void runDemo2() {
        List<Person>persons = create(100);
        DoNothingButCount<Person> counter = new DoNothingButCount<>();

        persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
                map((p) -> counter.pass(p)).
                sorted((p1, p2) -> p1.age - p2.age).
                collect(Collectors.toList()).stream().
                limit((int) (counter.i.intValue() * 0.5)).
                sorted((p1, p2) -> p2.length - p1.length).
                limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
    }

    public static void main(String str[]) {
        Demo2 demo = new Demo2();
        System.out.println("Demo 2:");
        demo.runDemo2();
        System.out.println("Demo 1:");
        demo.runDemo();

    }
}

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