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

Posted

技术标签:

【中文标题】如何在 Java 8 中动态进行过滤?【英文标题】:How to dynamically do filtering in Java 8? 【发布时间】:2014-05-15 18:00:35 【问题描述】:

我知道在 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 附带的简洁表达式中的过滤机制最有效地实现它呢?

【问题讨论】:

你能解释一下过滤顺序会如何改变结果吗? 创建所有组合的“电源集”并流式传输/过滤每个组合。 也许您可以简单地组合 de 谓词并使用单个过滤器,即 Predicate&lt;T&gt; p = p1.and(p2).and(p3).and(p4) @skiwi 大多数简单的过滤器(例如,u.age() > 30)是上下文不敏感的,因此过滤器排序无关紧要。不过,OP 还有其他上下文相关的“过滤器”,例如“某些标准的前 50%”。这些取决于输入集,因此取决于过滤器的排序。不幸的是,如何使用标准流filter() 操作来表达这些标准并不明显。 @Edwin Dalorzo:这就是我的想法,但不知道如何将其放入 Java 表达式中,任何示例代码可以告诉我如何做到这一点?尤其是前 20% 的部分。 【参考方案1】:

有趣的问题。这里发生了几件事。毫无疑问,这可以在不到半页的 Haskell 或 Lisp 中解决,但这是 Java,所以我们开始......

一个问题是我们有可变数量的过滤器,而显示的大多数示例都说明了固定的管道。

另一个问题是某些 OP 的“过滤器”是上下文相关的,例如“按特定顺序排列的前 50%”。这不能通过流上的简单 filter(predicate) 构造来完成。

关键是要意识到,虽然 lambdas 允许函数作为参数传递(效果很好),但这也意味着它们可以存储在数据结构中并且可以对它们执行计算。最常见的计算是获取多个函数并组合它们。

假设被操作的值是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 组合它们,返回一个谓词。所以我们可以取第一个谓词并编写一个循环,将它与连续的谓词组合起来,构建一个单一的谓词,它是它们的复合

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

这可行,但如果列表为空,它会失败,并且由于我们现在正在执行函数式编程,因此在循环中改变变量是不合格的。但是看!这是减法!我们可以通过 and 运算符对所有谓词进行归约,得到一个复合谓词,如下所示:

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

(信用:我从@venkat_s 学到了这个技巧。如果你有机会,去看看他在会议上的演讲。他很好。)

注意使用w -&gt; true 作为归约的标识值。 (这也可以用作循环的 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&lt;Widget&gt;。如果我们采用this::shortestThreethis::top50PercentByWeight 等方法引用,则结果对象的类型将是

UnaryOperator<Stream<Widget>>

如果我们将它们放入一个列表中,该列表的类型将是

List<UnaryOperator<Stream<Widget>>>

啊!三层嵌套泛型对我来说太多了。 (但是Aleksey Shipilev 确实曾经给我展示了一些使用四级嵌套泛型的代码。)泛型过多的解决方案是定义我们自己的类型。让我们将其中一项新事物称为标准。事实证明,使我们的新函数式接口类型与UnaryOperator 相关并没有什么价值,所以我们的定义可以简单地为:

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

现在我们可以像这样创建一个标准列表:

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

(我们将在下面弄清楚如何使用此列表。)这是向前迈出的一步,因为我们现在可以动态地操作列表,但它仍然有些限制。首先,它不能与普通谓词组合。其次,这里有很多硬编码的值,比如最短的三个:两个或四个怎么样?与长度不同的标准怎么样?我们真正想要的是一个为我们创建这些 Criterion 对象的函数。使用 lambdas 很容易。

给定一个比较器,这将创建一个选择前 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 对象的列表,我们就需要找出一种方法来应用所有这些对象。再一次,我们可以使用我们的朋友 reduce 将所有这些组合成一个 Criterion 对象:

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

标识函数c -&gt; c 很清楚,但是第二个参数有点棘手。给定一个流s,我们首先应用 Criterion c1,然后是 Criterion c2,这被包装在一个 lambda 中,该 lambda 接受两个 Criterion 对象 c1 和 c2 并返回一个 lambda,它将 c1 和 c2 的组合应用于流并返回结果流。

现在我们已经组成了所有的条件,我们可以将它应用到一个小部件流中,如下所示:

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

这仍然有点由内而外,但控制得相当好。最重要的是,它解决了最初的问题,即如何动态组合标准。一旦 Criterion 对象位于数据结构中,就可以根据需要对其进行选择、子集化、置换或任何操作,并且可以将它们全部组合成一个标准并使用上述技术应用于流。

函数式编程大师可能会说“他刚刚重新发明了......!”这可能是真的。我确信这可能已经在某个地方发明了,但它对 Java 来说是新的,因为在 lambda 之前,编写使用这些技术的 Java 代码是不可行的。

2014-04-07 更新

我已经清理并发布了完整的sample code 要点。

【讨论】:

哇!!!这就是我要说的。做得很棒!我认为 Java 8 可以做到这一点,但不太知道如何将其放入代码中,它非常强大!谢谢。 @Frank 谢谢!我已经发布了带有示例代码的要点。我看到您还有其他问题,但您似乎已将其删除,所以我认为您已经解决了这些问题。 @Jakub 谢谢!是的,我已经修正了“t1.size()”的拼写错误。在您编写 Criterion 时,您可能没有原始列表来调用 size() 。您所拥有的只是作为参数传递的 Stream,并且在源和您之间可能存在上游过滤器。如果您在该流上调用count(),它会耗尽它,因此您无法获得正确的元素集作为输出流发出。将元素收集到一个临时列表中似乎是在这里进行的唯一方法。 完美!现在任何对此主题感兴趣的人都可以轻松地测试和学习您的示例代码,谢谢!我希望有一天你发明的这些“***”[topPercentFromRange, topPercent, topN ...] 可以成为标准 Java 语言功能的一部分,并得到改进,这样我们就不必在我们自己的库中携带它们了. @skomi 当然,过滤器可能会被重载以获取谓词的集合(或流),但这似乎是一种非常罕见的情况,不保证包含在 API 中。如果谓词在编译时都是已知的,那么它们当然可以使用&amp;&amp; 运算符重构为单个谓词。但是,如果直到运行时才知道确切的谓词集,则需要在运行时组合它们,这就是 reduce() 派上用场的地方。【参考方案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)).

会变成:

            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();

    

【讨论】:

以上是关于如何在 Java 8 中动态进行过滤?的主要内容,如果未能解决你的问题,请参考以下文章

如何允许在 cassandra 中使用 java datastax 进行过滤

如何在(Jasper Studio)的 mongo DB 查询中制作动态/可选过滤器(参数)

java 动态操作数据库

如何在 Java 中动态构造谓词

如何基于选择列表过滤交互式网格?

在网格中为动态列创建过滤器