我知道在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的实例,它是一个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()
方法将采用两个谓词,并使用逻辑和将它们组合,并返回一个谓词。因此,我们可以采用第一个谓词并编写一个将其与后续谓词结合起来的循环,以建立一个由多个谓词组成的单个谓词:
Predicate<Widget> compositePredicate = allPredicates.get(0);
for (int i = 1; i < allPredicates.size(); i++) {
compositePredicate = compositePredicate.and(allPredicates.get(i));
}
这可以工作,但是如果列表为空,它将失败,并且由于我们现在正在执行函数式编程,因此在循环中对变量进行变异是declassé的。但是!这是减少!我们可以减少and运算符上的所有谓词,从而获得单个复合谓词,如下所示:
Predicate<Widget> compositePredicate =
allPredicates.stream()
.reduce(w -> true, Predicate::and);
(信贷:我从@venkat_s学到了这种技术。如果有机会,去见他在会议上讲话。他很好。)
注意使用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);
这行得通,但是有点烂,因为它读的是“ inside-out”和“ backs”。流源是widgetList
,它通过普通谓词进行流传输和过滤。现在,倒退,应用顶部50%的过滤器,然后应用最短三个过滤器,最后在最后应用流操作forEach
。这行得通,但阅读起来很混乱。而且仍然是静态的。我们真正想要的是要有一种方法,可以将这些新过滤器放入我们可以操纵的数据结构中,例如,运行所有置换,就像原始问题一样。
关于这一点的一个关键见解是,这些新型过滤器实际上只是函数,而我们在Java中具有函数接口类型,可以让我们将函数表示为对象,对其进行操作,将其存储在数据结构中,进行组合等。接受某种类型的参数并返回相同类型值的功能接口类型为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对象:
Criterion allCriteria =
criteria.stream()
.reduce(c -> c, (c1, c2) -> (s -> c2.apply(c1.apply(s))));
标识函数c -> c
很清楚,但是第二个arg有点棘手。给定一个流s
,我们首先应用Criterion c1,然后应用Criterion c2,并将其包装在一个lambda中,该lambda接受两个Criterion对象c1和c2,并返回一个lambda,该lambda将c1和c2的组成应用于流并返回结果流。
现在我们已经构成了所有条件,可以将其应用到小部件流中,如下所示:
allCriteria.apply(widgetList.stream())
.forEach(System.out::println);
这仍然是由内而外的,但控制得很好。最重要的是,它解决了最初的问题,即如何动态组合条件。一旦Criterion对象处于数据结构中,就可以对其进行选择,子集化,置换或其他必要的操作,并且可以将它们全部合并为一个标准,并使用上述技术将其应用于流。
功能编程专家可能会说:“他刚刚重塑了……!”这可能是真的。我敢肯定,这可能已经在某个地方发明了,但是它对于Java是新的,因为在使用lambda之前,编写使用这些技术的Java代码是不可行的。
更新2014-04-07
我已经整理并完整地发布了sample code。
休眠映射<键,设置<值>> - java我有以下表格:@Entity @Table(name = "events") Event --id --name @Entity @Table(name = "state") State --id --name @Entity @Table(name = "action") Action --id …
无法从ArrayList <String>转换为List <Comparable> - java当我写下面的代码时,编译器说 无法从ArrayList<String>转换为List<Comparable>private List<Comparable> get(){ return new ArrayList<String>(); } 但是当我用通配符编写返回类型时,代码会编译。private List&l…
合并List <T>和List <Optional <T >> - java鉴于: List<Integer> integers = new ArrayList<>(Arrays.asList( 10, 12 )); List<Optional<Integer>> optionalIntegers = Arrays.asList( Optional.of(5), Optional.em…
实例化类型<?>的泛型类 - java我正在为SCJP / OCPJP学习,并且遇到了一个对我来说很奇怪的示例问题。该示例代码实例化了两个通用集合:List<?> list = new ArrayList<?>(); List<? extends Object> list2 = new ArrayList<? extends Object>(); …
List <Dog>是List <Animal>的子类吗?为什么Java泛型不是隐式多态的? - java我对Java泛型如何处理继承/多态感到困惑。假设以下层次结构-动物(父母)狗-猫(儿童)因此,假设我有一个方法doSomething(List<Animal> animals)。根据继承和多态性的所有规则,我假设List<Dog>是List<Animal>,而List<Cat>是List<Animal&g…