有趣的问题。这里发生了几件事。毫无疑问,这可以在不到半页的Haskell或Lisp中解决,但这是Java,所以现在我们开始吧。
一个问题是,我们有可变数量的过滤器,而已经显示的大多数示例都说明了固定的管道。
另一个问题是,OP的一些“过滤器”是上下文相关的,例如“按一定顺序排名前50%”。这不能通过流上的简单构造来完成。filter(predicate)
关键是要认识到,虽然lambda允许将函数作为参数传递(效果良好),但这也意味着它们可以存储在数据结构中,并且可以对它们执行计算。最常见的计算是获取多个函数并组合它们。
假设正在操作的值是 Widget 的实例,Widget 是一个 POJO,它有一些明显的 getter:
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()
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那里学到了这种技术。如果你有机会,去看他在会议上发言。他很好。
请注意用作归约的标识值。(这也可以用作 for 循环的初始值,这将修复零长度列表大小写。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%的计算中使用列表的大小。
但是,这是有限制的,因为它是这种过滤的“静态”表示。我们如何将其链接到具有可变数量元素(其他筛选器或条件)的流中,就像我们对谓词所做的那样?
一个重要的观察结果是,此代码在流消耗和流发出之间执行其实际工作。它碰巧在中间有一个收集器,但是如果你将一条流链接到它的前面,并从它的后端链接东西,没有人比它更聪明。实际上,标准流管道操作(如 和)将流作为输入,并发出流作为输出。因此,我们可以自己编写一个这样的函数: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));
}
一个类似的例子可能是找到最短的三个小部件:
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);
这有效,但有点糟糕,因为它读起来“由内而外”和向后。流源是通过普通谓词进行流式处理和筛选的源。现在,向后,应用前 50% 筛选器,然后应用最短的三个筛选器,最后在最后应用流操作。这有效,但读起来很混乱。而且它仍然是静态的。我们真正想要的是有一种方法将这些新过滤器放在我们可以操作的数据结构中,例如,运行所有排列,就像原始问题一样。widgetList
forEach
在这一点上,一个关键的见解是,这些新型的过滤器实际上只是函数,我们在Java中具有函数接口类型,它们允许我们将函数表示为对象,操作它们,将它们存储在数据结构中,组合它们等。采用某种类型的参数并返回相同类型的值的功能接口类型是 。在本例中,参数和返回类型为 。如果我们要采用诸如 or 之类的方法引用,则生成的对象的类型将为UnaryOperator
Stream<Widget>
this::shortestThree
this::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 allCriteria =
criteria.stream()
.reduce(c -> c, (c1, c2) -> (s -> c2.apply(c1.apply(s))));
恒等函数很清楚,但第二个参数有点棘手。给定一个流,我们首先应用标准 c1,然后应用标准 c2,这被包装在一个 lambda 中,该 lambda 采用两个标准对象 c1 和 c2,并返回一个 lambda,该 lambda 将 c1 和 c2 的组合应用于流并返回生成的流。c -> c
s
现在我们已经编写了所有标准,我们可以将其应用于小部件流,如下所示:
allCriteria.apply(widgetList.stream())
.forEach(System.out::println);
这仍然有点内向外,但它得到了很好的控制。最重要的是,它解决了最初的问题,即如何动态组合标准。一旦 Criterion 对象位于数据结构中,就可以根据需要选择、子集化、置换或任何内容,并且它们可以全部组合到单个标准中,并使用上述技术应用于流。
函数式编程大师可能会说“他刚刚重新发明了......”,这可能是真的。我敢肯定,这可能已经在某个地方发明了,但它对Java来说是新的,因为在lambda之前,编写使用这些技术的Java代码是不可行的。
更新 2014-04-07
我已经清理并发布了完整的示例代码在要点中。