197 组合函数、谓词和比较器
组合(或链接)函数、谓词和比较器允许我们编写应该统一应用的复合标准。
组合谓词
假设我们有以下Melon
类和Melon
的List
:
public class Melon { private final String type; private final int weight; // constructors, getters, setters, equals(), // hashCode(), toString() omitted for brevity } List<Melon> melons = Arrays.asList(new Melon("Gac", 2000), new Melon("Horned", 1600), new Melon("Apollo", 3000), new Melon("Gac", 3000), new Melon("Hemi", 1600));
Predicate
接口提供了三种方法,它们接受一个Predicate
,并使用它来获得一个丰富的Predicate
。这些方法是and()
、or()
和negate()
。
例如,假设我们要过滤重量超过 2000 克的西瓜。为此,我们可以写一个Predicate
,如下所示:
Predicate<Melon> p2000 = m -> m.getWeight() > 2000;
现在,让我们假设我们想要丰富这个Predicate
,只过滤符合p2000
的瓜,并且是Gac
或Apollo
类型的瓜。为此,我们可以使用and()
和or()
方法,如下所示:
Predicate<Melon> p2000GacApollo = p2000.and(m -> m.getType().equals("Gac")) .or(m -> m.getType().equals("Apollo"));
这从左到右被解释为a && (b || c)
,其中我们有以下内容:
a
是m -> m.getWeight() > 2000
b
是m -> m.getType().equals("Gac")
c
是m -> m.getType().equals("Apollo")
显然,我们可以以同样的方式添加更多的标准。
我们把这个Predicate
传给filter()
:
// Apollo(3000g), Gac(3000g) List<Melon> result = melons.stream() .filter(p2000GacApollo) .collect(Collectors.toList());
现在,假设我们的问题要求我们得到上述复合谓词的否定。将这个谓词重写为!a && !b && !c
或任何其他对应的表达式是很麻烦的。更好的解决方案是调用negate()
方法,如下所示:
Predicate<Melon> restOf = p2000GacApollo.negate();
我们把它传给filter()
:
// Gac(2000g), Horned(1600g), Hemi(1600g) List<Melon> result = melons.stream() .filter(restOf) .collect(Collectors.toList());
从 JDK11 开始,我们可以否定作为参数传递给not()
方法的Predicate
。例如,让我们使用not()
过滤所有重量小于(或等于)2000 克的西瓜:
Predicate<Melon> pNot2000 = Predicate.not(m -> m.getWeight() > 2000); // Gac(2000g), Horned(1600g), Hemi(1600g) List<Melon> result = melons.stream() .filter(pNot2000) .collect(Collectors.toList());
组合比较器
让我们考虑上一节中相同的Melon
类和Melon
的List
。
现在,让我们使用Comparator.comparing()
按重量对Melon
中的List
进行排序:
Comparator<Melon> byWeight = Comparator.comparing(Melon::getWeight); // Horned(1600g), Hemi(1600g), Gac(2000g), Apollo(3000g), Gac(3000g) List<Melon> sortedMelons = melons.stream() .sorted(byWeight) .collect(Collectors.toList());
我们也可以按类型对列表进行排序:
Comparator<Melon> byType = Comparator.comparing(Melon::getType); // Apollo(3000g), Gac(2000g), Gac(3000g), Hemi(1600g), Horned(1600g) List<Melon> sortedMelons = melons.stream() .sorted(byType) .collect(Collectors.toList());
要反转排序顺序,只需调用reversed()
:
Comparator<Melon> byWeight = Comparator.comparing(Melon::getWeight).reversed();
到目前为止,一切都很好!
现在,假设我们想按重量和类型对列表进行排序。换言之,当两个瓜的重量相同时(例如,Horned (1600g)
、Hemi(1600g
),它们应该按类型分类(例如,Hemi(1600g)
、Horned(1600g)
)。朴素的方法如下所示:
// Apollo(3000g), Gac(2000g), Gac(3000g), Hemi(1600g), Horned(1600g) List<Melon> sortedMelons = melons.stream() .sorted(byWeight) .sorted(byType) .collect(Collectors.toList());
显然,结果不是我们所期望的。这是因为比较器没有应用于同一个列表。byWeight
比较器应用于原始列表,而byType
比较器应用于byWeight
的输出。基本上,byType
取消了byWeight
的影响。
解决方案来自Comparator.thenComparing()
方法。此方法允许我们链接比较器:
Comparator<Melon> byWeightAndType = Comparator.comparing(Melon::getWeight) .thenComparing(Melon::getType); // Hemi(1600g), Horned(1600g), Gac(2000g), Apollo(3000g), Gac(3000g) List<Melon> sortedMelons = melons.stream() .sorted(byWeightAndType) .collect(Collectors.toList());
这种口味的thenComparing()
以Function
为参数。此Function
用于提取Comparable
排序键。返回的Comparator
只有在前面的Comparator
找到两个相等的对象时才应用。
另一种口味的thenComparing()
得到了Comparator
:
Comparator<Melon> byWeightAndType = Comparator.comparing(Melon::getWeight) .thenComparing(Comparator.comparing(Melon::getType));
最后,我们来考虑一下Melon
的以下List
:
List<Melon> melons = Arrays.asList(new Melon("Gac", 2000), new Melon("Horned", 1600), new Melon("Apollo", 3000), new Melon("Gac", 3000), new Melon("hemi", 1600));
我们故意在最后一个Melon
上加了一个错误。它的类型这次是小写的。如果我们使用byWeightAndType
比较器,则输出如下:
Horned(1600g), hemi(1600g), ...
作为一个字典顺序比较器,byWeightAndType
将把Horned
放在hemi
之前。因此,以不区分大小写的方式按类型排序将非常有用。这个问题的优雅解决方案将依赖于另一种风格的thenComparing()
,它允许我们传递一个Function
和Comparator
作为参数。传递的Function
提取Comparable
排序键,给定的Comparator
用于比较该排序键:
Comparator<Melon> byWeightAndType = Comparator.comparing(Melon::getWeight) .thenComparing(Melon::getType, String.CASE_INSENSITIVE_ORDER);
这一次,结果如下(我们回到正轨):
hemi(1600g), Horned(1600g),...
对于int
、long
和double
,我们有comparingInt()
、comparingLong()
、comparingDouble()
、thenComparingInt()
、thenComparingLong()
和thenComparingDouble()
。comparing()
和thenComparing()
方法有相同的味道。
组合函数
通过Function
接口表示的 Lambda 表达式可以通过Function.andThen()
和Function.compose()
方法组合。
andThen(Function<? super R,? extends V> after)
返回一个组合的Function
,它执行以下操作:
- 将此函数应用于其输入
- 将
after
函数应用于结果
我们来看一个例子:
Function<Double, Double> f = x -> x * 2; Function<Double, Double> g = x -> Math.pow(x, 2); Function<Double, Double> gf = f.andThen(g); double resultgf = gf.apply(4d); // 64.0
在本例中,将f
函数应用于其输入(4)。f
的应用结果为 8(f(4) = 4 * 2
。此结果是第二个函数g
的输入。g
申请结果为 64(g(8) = Math.pow(8, 2)
。下图描述了四个输入的流程—1
、2
、3
、4
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qDIqxVzf-1657285412205)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/bb13bcce-901b-4a0c-9af1-9136d137794f.png)]
所以,这就像g(f(x))
。相反的f(g(x))
可以用Function.compose()
来塑造。返回的合成函数将函数之前的应用于其输入,然后将此函数应用于结果:
double resultfg = fg.apply(4d); // 32.0
在本例中,g
函数应用于其输入(4)。应用g
的结果是 16(g(4) = Math.pow(4, 2)
。这个结果是第二个函数f
的输入。应用f
的结果为 32(f(16) = 16 * 2
)。下图描述了四个输入的流程–1
、2
、3
和4
:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z8htdA9A-1657285412205)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/299fb4da-3ba6-408e-bf3c-3bf925db688b.png)]
基于同样的原则,我们可以通过组合addIntroduction()
、addBody()
和addConclusion()
方法来开发编辑文章的应用。请看一下与本书捆绑在一起的代码,看看这本书的实现。
我们也可以编写其他管道,只需将其与合成过程结合起来。
198 默认方法
默认方法被添加到 Java8 中。它们的主要目标是为接口提供支持,以便它们能够超越抽象契约(仅包含抽象方法)而发展。对于编写库并希望以兼容的方式发展 API 的人来说,这个工具非常有用。通过默认方法,可以在不中断现有实现的情况下丰富接口。
接口直接实现默认方法,并通过default
关键字进行识别。
例如,以下接口定义了一个抽象方法area()
,默认方法称为perimeter()
:
public interface Polygon { public double area(); default double perimeter(double...segments) { return Arrays.stream(segments) .sum(); } }
因为所有公共多边形(例如,正方形)的周长都是边的总和,所以我们可以在这里实现它。另一方面,面积公式因多边形而异,因此默认实现将不太有用。
现在,我们定义一个实现Polygon
的Square
类。其目标是通过周长表示正方形的面积:
public class Square implements Polygon { private final double edge; public Square(double edge) { this.edge = edge; } @Override public double area() { return Math.pow(perimeter(edge, edge, edge, edge) / 4, 2); } }
其他多边形(例如矩形和三角形)可以实现Polygon
,并基于通过默认实现计算的周长来表示面积。
但是,在某些情况下,我们可能需要覆盖默认方法的默认实现。例如,Square
类可以覆盖perimeter()
方法,如下所示:
@Override public double perimeter(double...segments) { return segments[0] * 4; }
我们可以称之为:
@Override public double area() { return Math.pow(perimeter(edge) / 4, 2); }
总结
我们的任务完成了!本章介绍无限流、空安全流和默认方法。一系列问题涵盖了分组、分区和收集器,包括 JDK12 teeing()
收集器和编写自定义收集器。此外,takeWhile()
、dropWhile()
、组合函数、谓词和比较器、Lambda 的测试和调试,以及其他一些很酷的话题。
从本章下载应用以查看结果和其他详细信息。