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

Fra*_*ank 37 java lambda filtering java-8

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

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

但是,如果我有一个集合和六个过滤标准,我想测试标准的组合怎么办?

例如,我有一组对象和以下标准:

<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
Run Code Online (Sandbox Code Playgroud)

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

<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>
Run Code Online (Sandbox Code Playgroud)

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

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

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}}
...
Run Code Online (Sandbox Code Playgroud)

但是,如何使用Java 8附带的简洁表达式中的过滤机制最有效地实现它?

Stu*_*rks 81

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

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

另一个问题是OP的一些"过滤器"是上下文敏感的,例如"按特定顺序排在前50%".这不能通过filter(predicate)流上的简单构造来完成.

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

假设正在操作的值是Widget的实例,这是一个有一些明显的getter的POJO:

class Widget {
    String name() { ... }
    int length() { ... }
    double weight() { ... }

    // constructors, fields, toString(), etc.
}
Run Code Online (Sandbox Code Playgroud)

让我们从第一个问题开始,并弄清楚如何使用可变数量的简单谓词进行操作.我们可以创建一个这样的谓词列表:

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

鉴于此列表,我们可以置换它们(可能没有用,因为它们是独立的顺序)或者选择我们想要的任何子集.假设我们只想应用所有这些.我们如何将可变数量的谓词应用于流?还有一个Predicate.and(),将采取两个谓词和使用逻辑相结合的方法他们,返回单个预测.因此,我们可以采取的第一个谓语,写一个循环,用连续的谓词结合它来建立一个单一的谓语,这是一个复合他们所有的:

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

这可行,但如果列表为空则失败,并且因为我们现在正在进行函数式编程,所以在循环中变量变量是declassé.但是,瞧!这是减少!我们可以通过运算符减少所有谓词获得单个复合谓词,如下所示:

Predicate<Widget> compositePredicate =
    allPredicates.stream()
                 .reduce(w -> true, Predicate::and);
Run Code Online (Sandbox Code Playgroud)

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

注意使用w -> true减少的身份值.(这也可以用作compositePredicate循环的初始值,这将修复零长度列表的情况.)

现在我们有了复合谓词,我们可以写出一个简短的管道,它只是将复合谓词应用到窗口小部件:

widgetList.stream()
          .filter(compositePredicate)
          .forEach(System.out::println);
Run Code Online (Sandbox Code Playgroud)

上下文敏感过滤器

现在让我们考虑一下我称之为"上下文敏感"的过滤器,它由例如"按特定顺序排在前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);
Run Code Online (Sandbox Code Playgroud)

这并不复杂,但它有点麻烦,因为我们必须将元素收集到列表中并将其分配给变量,以便在50%计算中使用列表的大小.

但这是限制性的,因为它是这种过滤的"静态"表示.我们如何将它链接到具有可变数量元素(其他过滤器或标准)的流中,就像我们对谓词所做的那样?

一个重要的观察是,该代码在流的消耗和流的发送之间进行实际工作.它恰好在中间有一个收集器,但是如果你将一条流链接到它的前端并从它的后端链起来的东西,没有人是明智的.事实上,标准的流管道操作就像map并且filter每个都将流作为输入并将流作为输出发出.所以我们可以自己编写一个这样的函数:

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));
}
Run Code Online (Sandbox Code Playgroud)

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

Stream<Widget> shortestThree(Stream<Widget> stream) {
    return stream.sorted(comparing(Widget::length))
                 .limit(3);
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以编写一些将这些有状态过滤器与普通流操作相结合的东西:

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

这是有效的,但有点糟糕,因为它读作"从里到外"和向后.流源是widgetList通过普通谓词流式传输和过滤的.现在,向后,应用前50%过滤器,然后应用最短三过滤器,最后forEach应用流操作.这有效但读起来很混乱.它仍然是静态的.我们真正想要的是有一种方法将这些新过滤器放在我们可以操作的数据结构中,例如,运行所有排列,就像在原始问题中一样.

在这一点上的一个关键见解是这些新类型的过滤器实际上只是函数,我们在Java中有函数接口类型,它们让我们将函数表示为对象,操作它们,将它们存储在数据结构中,组合它们等.功能接口类型,它接受某种类型的参数并返回相同类型的值UnaryOperator.在这种情况下,参数和返回类型是Stream<Widget>.如果我们采用诸如this::shortestThree或的方法引用this::top50PercentByWeight,则生成的对象的类型将是

UnaryOperator<Stream<Widget>>
Run Code Online (Sandbox Code Playgroud)

如果我们将这些放入列表中,那么列表的类型就是

List<UnaryOperator<Stream<Widget>>>
Run Code Online (Sandbox Code Playgroud)

啊! 嵌套泛型的三个层次对我来说太过分了.(但是Aleksey Shipilev曾经向我展示了一些使用了四级嵌套泛型的代码.)太多泛型的解决方案是定义我们自己的类型.让我们把我们的新事物之一称为标准.事实证明,通过使我们的新功能接口类型与之相关UnaryOperator,获得的价值很小,因此我们的定义可以简单地为:

@FunctionalInterface
public interface Criterion {
    Stream<Widget> apply(Stream<Widget> s);
}
Run Code Online (Sandbox Code Playgroud)

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

List<Criterion> criteria = Arrays.asList(
    this::shortestThree,
    this::lengthGreaterThan20
);
Run Code Online (Sandbox Code Playgroud)

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

这创建了一个标准,在给定比较器的情况下选择前N个小部件:

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

在给定比较器的情况下,这会创建一个选择最高百分比的小部件的标准:

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));
    };
}
Run Code Online (Sandbox Code Playgroud)

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

Criterion fromPredicate(Predicate<Widget> pred) {
    return stream -> stream.filter(pred);
}
Run Code Online (Sandbox Code Playgroud)

现在我们有一种非常灵活的方法来创建标准并将它们放入列表中,在这些列表中可以对它们进行子集化或置换或其他任何方式:

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%
);
Run Code Online (Sandbox Code Playgroud)

一旦我们有了Criterion对象列表,我们就需要找到一种方法来应用所有这些对象.再次,我们可以使用我们的朋友reduce将所有这些组合成一个Criterion对象:

Criterion allCriteria =
    criteria.stream()
            .reduce(c -> c, (c1, c2) -> (s -> c2.apply(c1.apply(s))));
Run Code Online (Sandbox Code Playgroud)

身份功能c -> c很明确,但第二个arg有点棘手.给定一个流,s我们首先应用Criterion c1,然后是Criterion c2,并将其包含在一个lambda中,该lambda接受两个Criterion对象c1和c2,并返回一个lambda,它将c1和c2的组合应用于流并返回结果流.

现在我们已经编写了所有条件,我们可以将它应用到如下所示的小部件流:

allCriteria.apply(widgetList.stream())
           .forEach(System.out::println);
Run Code Online (Sandbox Code Playgroud)

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

函数式编程大师可能会说"他刚刚彻底改造......!" 这可能是真的.我确信这可能已经在某处发明了,但它对Java来说是新的,因为在lambda之前,编写使用这些技术的Java代码是不可行的.

更新2014-04-07

我已经清理完并在一个要点中发布了完整的示例代码.

  • 完美的!现在任何对此主题感兴趣的人都可以轻松地测试和学习您的示例代码,谢谢!我希望有一天你发明的这些“轮子”[topPercentFromRange、topPercent、topN ...] 可以成为标准 Java 语言特性的一部分,并得到改进,所以我们不必在我们自己的库中携带它们. (2认同)
  • @skomi当然,过滤器可能会被重载以获取谓词的集合(或流),但这似乎是一个非常罕见的情况,不能保证包含在API中.如果谓词在编译时都是已知的,那么它们当然可以使用`&&`运算符重构为单个谓词.但是如果确切的谓词集在运行时才知道,那么它们需要在运行时组合,这就是`reduce()`派上用场的地方. (2认同)