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

标签 java lambda filtering java-8

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

最佳答案

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

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

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

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

假设被操作的值是 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));
}

这有效,但如果列表为空,它会失败,并且由于我们现在正在进行函数式编程,因此在循环中改变变量是 declassé。但是!这是 Markdown !我们可以减少和操作符上的所有谓词得到一个复合谓词,像这样:
Predicate<Widget> compositePredicate =
    allPredicates.stream()
                 .reduce(w -> true, Predicate::and);

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

注意使用 w -> 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 中有函数式接口(interface)类型,它允许我们将函数表示为对象、操作它们、将它们存储在数据结构中、组合它们等。接受某种类型的参数并返回相同类型值的功能接口(interface)类型是 UnaryOperator 。这种情况下的参数和返回类型是 Stream<Widget> 。如果我们采用诸如 this::shortestThreethis::top50PercentByWeight 之类的方法引用,则结果对象的类型将是
UnaryOperator<Stream<Widget>>

如果我们将这些放入一个列表中,该列表的类型将是
List<UnaryOperator<Stream<Widget>>>

啊!三层嵌套泛型对我来说太多了。 (但是 Aleksey Shipilev 确实曾经向我展示了一些使用四级嵌套泛型的代码。)泛型过多的解决方案是定义我们自己的类型。让我们将我们的一项新事物称为标准。事实证明,让我们的新功能接口(interface)类型与 UnaryOperator 相关几乎没有什么值(value),所以我们的定义可以简单地为:
@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 对象的列表,我们就需要找出一种方法来应用所有这些对象。再一次,我们可以使用我们的 friend reduce 将所有这些组合成一个 Criterion 对象:
Criterion allCriteria =
    criteria.stream()
            .reduce(c -> c, (c1, c2) -> (s -> c2.apply(c1.apply(s))));

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

现在我们已经编写了所有标准,我们可以将它应用到一个小部件流,如下所示:
allCriteria.apply(widgetList.stream())
           .forEach(System.out::println);

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

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

更新 2014-04-07

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

关于java - 如何在 Java 8 中动态进行过滤?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22845574/

相关文章:

java - 如何根据用户登录隐藏某些功能?

arrays - 在angular2中过滤数组

java - 检查正在运行的进程 - java

java - 有没有统一的方法来检测 linux 上安装的 java?

c++ - 将捕获的 lambda 作为函数指针传递

c# - 如何在 lambda 表达式中组合多个语句

JavaScript:为什么我不能用 .push() 链接 Array.prototype.filter?

java - 在 Java 中删除数组中重复项的最佳方法是什么?

java - 如何将 jpanel 与油漆一起使用(或重新油漆)

java - Java SerializedLambda 的 captureClass 和 implClass 有什么区别?