文章目录
- 环境
- 背景
- 方法
- 方法1:Java 7(传统方法)
- 方法2:Java 7 (策略模式)
- 方法3:Java 8的Lambda表达式
- 方法4:Java 8内建的函数式接口Predicate
- 方法5:Java 8的方法引用
- 方法6:Java 8的Stream
- 流的例子
- 总结
- 参考
注:本文主要参考了《Java 8实战》这本书。
环境
- Ubuntu 22.04
- jdk-17.0.3.1 (兼容Java 8)
背景
已知苹果类定义如下(每个苹果有颜色、重量等属性):
class Apple {
private String color;
private double weight;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
public Apple() {
}
public Apple(String color, double weight) {
this.color = color;
this.weight = weight;
}
现在有一堆苹果:
public class Test0913 {
public static void main(String[] args) {
List<Apple> list0 = new ArrayList<>();
list0.add(new Apple("Red", 200));
list0.add(new Apple("Green", 300));
list0.add(new Apple("Red", 220));
list0.add(new Apple("Red", 280));
list0.add(new Apple("Green", 220));
......
}
}
要求查找满足一定条件的苹果,比如说颜色是红色的苹果。
方法
方法1:Java 7(传统方法)
在Java 8之前,我们可能会这么写:
public static List<Apple> getRedApples(List<Apple> list) {
List<Apple> result = new ArrayList<>();
for (Apple e: list) {
if ("Red".equals(e.getColor())) {
result.add(e);
}
}
return result;
}
注:这里使用了 static
关键字,仅仅是为了方便,可直接使用 Test0913.getRedApples()
来调用该方法,而无需创建对象实例。后续代码中的 static
也同理。
调用该方法来查找红苹果:
List<Apple> list1 = Test0913.getRedApples(list0);
方法2:Java 7 (策略模式)
方法1的缺点是,如果要查找重量超过250的苹果,则需要添加一个与 getRedApples()
类似的方法,如下:
public static List<Apple> getHeavyApples(List<Apple> list) {
List<Apple> result = new ArrayList<>();
for (Apple e: list) {
if (e.getWeight() > 250) {
result.add(e);
}
}
return result;
}
显然,二者的重复代码非常多。
为了减少重复,可以采取设计模式中的“策略模式”,把公共部分提取出来。
- 定义一个接口
AppleStrategy
,它只有一个test()
方法,传入一个苹果实例,返回true/false,代表了对“苹果是否满足要求”的抽象:
interface AppleStrategy {
boolean test (Apple apple);
}
- 接口的实现类,该类实现了对“红苹果”的测试:
class AppleStrategy_Red implements AppleStrategy {
@Override
public boolean test(Apple apple) {
return "Red".equals(apple.getColor());
}
}
- 接口的实现类,该类实现了对“重苹果”的测试:
class AppleStrategy_Heavy implements AppleStrategy {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 250;
}
}
- 策略之外的不变部分,可抽象为如下方法:
public static List<Apple> filterApples(List<Apple> list, AppleStrategy strategy) {
List<Apple> result = new ArrayList<>();
for (Apple e: list) {
if (strategy.test(e)) {
result.add(e);
}
}
return result;
}
- 调用该方法来查找红苹果:
List<Apple> list2 = Test0913.filterApples(list0, new AppleStrategy_Red());
注:如果不想显式定义 AppleStrategy_Red
/ AppleStrategy_Heavy
等接口的实现类,也可以在需要时直接使用匿名类,如下:
List<Apple> list2 = Test0913.filterApples(list0, new AppleStrategy() {
@Override
public boolean test(Apple apple) {
return "Red".equals(apple.getColor());
}
});
方法3:Java 8的Lambda表达式
方法2比方法1要灵活很多,但是方法2需要定义接口、实现类(或匿名类),创建对象实例,等等,代码较为复杂。
如何才能兼顾灵活和简单呢?显然,方法2的精华部分在于“策略”。定义的接口和实现类,就是为了实现不同的策略。要想精简代码,Java 8 提供了一种方法,可以直接把策略以代码的方式(而非对象)传递给调用者:
List<Apple> list3 = Test0913.filterApples(list0, e -> "Red".equals(e.getColor()));
仔细对比一下方法3和方法2的 filterApples()
方法,重点比较一下第二个参数(其类型是 AppleStrategy
),如下:
- 方法2:
new AppleStrategy() {
@Override
public boolean test(Apple apple) {
return "Red".equals(apple.getColor());
}
}
- 方法3:
e -> "Red".equals(e.getColor())
这就是Java 8的Lambda表达式,它是一个匿名方法,它由三部分组成:
- 参数列表:即
e
,完整形式是(Apple e)
- 箭头符号:即
->
,分隔参数和代码 - 代码:即
"Red".equals(e.getColor())
,完整形式是{return "Red".equals(e.getColor());}
(注意花括号和分号),可以有多条语句
简而言之,使用Lambda表达式,就可以通过“直接传代码”来简化复杂度,达到和匿名类同样的效果。
本例中, e -> "Red".equals(e.getColor())
就代表了一个实现了 AppleStrategy
接口的匿名类的实例。
所以,方法3也可以写成:
AppleStrategy strategy = (Apple e) -> "Red".equals(e.getColor());
List<Apple> list3 = Test0913.filterApples(list0, strategy);
在 filterApples()
方法里调用 AppleStrategy
的 test()
方法时,运行的就是Lambda的代码。
那么问题来了,由于 AppleStrategy
接口只有一个 test()
方法,显然Lambda代表的就是这个方法。但是假如接口有多个方法,如果使用Lambda,只传一堆代码的话,怎么能知道是哪个方法?那不是乱套了吗?
确实如此,所以Lambda的使用也是有限制的,它只适用于“函数式接口”。
所谓函数式接口,就是只有一个抽象方法的接口。接口可以定义0个或多个默认方法,只要只定义了一个抽象接口,那就仍然是函数式接口。
函数式接口可以通过 @FunctionalInterface
注解来修饰。如果对接口加上该注解,而实际不是函数式接口,则编译会报错。虽然该注解不是强制的,不过最好还是加上。( @Override
注解也一样)
在本例中, AppleStrategy
就是一个函数式接口(不过没加 @FunctionalInterface
注解)。
总结:Lambda表达式所代表的是对函数式接口的匿名实现,具体来说就是代表接口唯一的那个抽象方法。
方法4:Java 8内建的函数式接口Predicate
说到 AppleStrategy
接口,在方法3中使用了Lambda表达式,节省了 AppleStrategy
接口的实现类,但还是要定义 AppleStrategy
接口,能把这个接口的定义也省掉吗?
实际上Java 8已经内建了很多很实用的接口,需要的时候直接用就行了。本例中的 AppleStrategy
接口,在Java 8中已经有类似的存在了,它的名字叫做 Predicate
(谓词),定义在 java.util.function.Predicate
里:
package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
// 下面还有一些默认方法
......
}
所以,可以删掉 AppleStrategy
接口(及其实现类),并改写 filterApples()
方法如下:
public static List<Apple> filterApples(List<Apple> list, Predicate<Apple> p) {
List<Apple> result = new ArrayList<>();
for (Apple e: list) {
if (p.test(e)) {
result.add(e);
}
}
return result;
}
注:跑个题:如果保留之前的 filterApples()
方法,然后新添加如上 filterApples()
方法,那么这两个方法虽然同名,但第二个参数不同,这是OK的。但是,在调用时,如果第二个参数使用Lambda表达式,则会编译报错,这是因为编译器在处理Lambda的时候,会去查找对应的函数式接口,而这两个方法的第二个参数都能匹配上,编译器无法做出选择。
调用 filterApples()
方法的代码不变:
List<Apple> list4 = Test0913.filterApples(list0, e -> "Red".equals(e.getColor()));
注意,第二个参数所代表的函数式接口已经发生了变化:
- 方法3:代表的是
AppleStrategy
接口(自定义) - 方法4:代表的是
Predicate
接口(Java 8内建,推荐)
与方法3同理,方法4也可以写成:
Predicate<Apple> p = (Apple e) -> "Red".equals(e.getColor());
List<Apple> list4 = Test0913.filterApples(list0, p);
注:Java 8内建了很多函数式接口,常见的比如:
Predicate
Consumer
Function
Supplier
UnaryOperator
BinaryOperator
- …
方法5:Java 8的方法引用
如果Lambda表达式的代码在原本的代码里已经有实现了:
public static boolean isRedApple(Apple apple) {
return "Red".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 250;
}
那么只需要把该方法作为Lambda传入即可:
List<Apple> list5 = Test0913.filterApples(list0, e -> Test0913.isRedApple(e));
这和方法4并没有什么区别。非要找区别的话,方法5的Lambda表达式是一个方法调用。
对于像这样只包含一个方法调用的Lambda,Java 8提供了另外一种被称为“方法引用”的简单写法:
List<Apple> list5 = Test0913.filterApples(list0, Test0913::isRedApple);
Lambda被称为匿名方法,而方法引用显然是命名方法,其实它本质还是Lambda,只不过是在特定条件下的快捷写法,更方便我们理解代码。
方法引用的格式如下:
- 左边部分:即
Test0913
,是类或者对象 - 双冒号:即
::
,分隔类/对象和方法 - 方法:即
isRedApple
,注意不要加括号
当Lambda的内容很长,或者需要复用时,显然封装是一个比较好的做法。这时就可以给它起一个有意义的方法名,然后通过方法引用来调用它。
总结:方法引用是Lambda的快捷简写,可以简化代码,方便理解。
方法6:Java 8的Stream
在前面介绍的方法中,都在 filterApples()
方法里,对list0进行了遍历,这种遍历是显式的,被称为“外部迭代”。Java 8引入了Stream,可以实现“内部迭代”,也就是无需显式遍历集合,以便更加集中关注在业务领域。这有点类似于SQL语句:你只需告诉数据库你想要什么数据,而不用关心数据是怎么得来的。
现在,我们来使用Stream简化代码,这次连 filterApples()
方法也不需要了:
List<Apple> list6 = list0.stream().filter(Test0913::isRedApple).toList();
只需要一行代码就搞定了。
注: toList()
不是Java 8提供的方法,是Java 16才有的,如果是使用Java 8,需要稍微麻烦一点,写成:
List<Apple> list6 = list0.stream()
.filter(Test0913::isRedApple)
.collect(Collectors.toList());
Stream除了能够精简代码,还有运行性能的提升。比如需要红色的重苹果:
List<Apple> list6 = list0.stream()
.filter(Test0913::isRedApple)
.filter(Test0913::isHeavyApple)
.toList();
像 filter()
方法,返回的类型仍然是流,因此可以复合运算。Java会对流的复合运算做优化。比如:
list0.stream()
.map(e -> {System.out.println(e); return e;})
.limit(3)
.toList();
运行结果里,只会打印前三个苹果的信息,这是因为Java对复合流做了优化。本例中, limit(3)
影响到了前面的 map()
操作。
如果苹果的数量非常多,还可以充分利用多核CPU并行(注意不是并发)运行,只需要把流( stream()
)变成并行流( parallelStream()
):
List<Apple> list6 = list0.parallelStream()
.filter(Test0913::isRedApple)
.filter(Test0913::isHeavyApple)
.toList();
不需要编写任何与多线程有关的代码,都隐藏在并行流里了。
流的例子
流的功能非常强大,内容非常多。本文只是简介,不多做解释,直接看例子。
注:下面的例子使用的是 stream()
,也可以使用 parallelStream()
。
- 不是红颜色的苹果:
List<Apple> list8 = list0.stream()
.filter(Predicate.not(Test0913::isRedApple))
.toList();
- 把红苹果按重量从大到小排序:
List<Apple> list7 = list0.stream()
.filter(Test0913::isRedApple)
.sorted(Comparator.comparingDouble(Apple::getWeight).reversed())
.toList();
- 绿苹果的个数:
long count = list0.stream()
.filter(e -> "Green".equals(e.getColor()))
.count();
- 打印每个苹果的重量:
list0.stream()
.mapToDouble(e -> e.getWeight())
.forEach(System.out::println);
- 是否所有苹果的重量都大于200:
boolean b = list0.stream()
.allMatch(e -> e.getWeight() > 200);
- 是否有重量大于300的红苹果:
boolean b = list0.stream()
.filter(Test0913::isRedApple)
.anyMatch(e -> e.getWeight() > 300);
- 查找第一个(或者任何一个)重量大于200的苹果:
Optional<Apple> apple = list0.stream()
.filter(e -> e.getWeight() > 200)
// .findFirst();
.findAny();
注意: findFirst()
和 findAny()
的区别在于并行。在并行流条件下, findAny()
效率更高(找到一个就行,不用关心顺序)。
注意:也许有符合条件的苹果,也许没有,所以返回的是 Optional<Apple>
而非 Apple
。
- 所有苹果重量的总和、最大值、最小值、平均值等统计信息:
DoubleSummaryStatistics summary = list0.stream()
.mapToDouble(Apple::getWeight)
.summaryStatistics();
结果如下:
DoubleSummaryStatistics{count=5, sum=1220.000000, min=200.000000, average=244.000000, max=300.000000}
- 把苹果按颜色分类:
Map<String, List<Apple>> map1 = list0.stream()
.collect(Collectors.groupingBy(Apple::getColor));
- 把苹果按颜色分类,并求每种苹果的平均重量:
Map<String, Double> map2 = list0.stream()
.collect(Collectors.groupingBy(Apple::getColor,
Collectors.averagingDouble(Apple::getWeight)));
注意:该例有点类似于SQL语句: SELECT COLOR, AVG(WEIGHT) FROM LIST0 GROUP BY COLOR
。
总结
流的优点非常多:
- 简化代码,更接近人类语言,对于编写,理解,维护都非常方便
- 复合性:流可以复合,更灵活,而且复合流可以自动优化
- 并行性:不需了解和编写多线程代码,隐藏了实现细节
总之,Java 8的流很好很强大,一定要多用它。
参考
https://livebook.manning.com/book/java-8-in-action/