jdk8新特性之函数式编程
参考文档/内容:
本系列文章内容主要来自于《java8 实战》,加上了自己一些想法和重新编写的例子,主要目的是为了加深自己的印象,方便后期的复习。
所以有任何问题请购阅《java8 实战》内容,支持正版。
一、原书问题和内容的阐述
1.1、Java怎么又变了
原因具体如下:
- 因为之前的JDK不好用(具体可参考上一篇的文章)
- 多核心CPU的利用和大数据的处理优化考虑
- 其它技术相关优点的借鉴,“要么改变,要么衰亡”
二、函数编程
2.1、什么是函数编程?
一等公民
编程语言的整个目的就在于操作值:原始值(例如:int类型42、double类型3.14等)、对象(更严格地说是对象的引用值),这些值被称为一等公民
二等公民
编程语言中的其他结构也许有助于我们表示值的结构(如方法和类等,虽然方法可以来定义类,类还可以实例化来产生值,但方法和类本身都不是值),但在程序执行期间不能传递,因而是二等公民
函数编程
首先:编程语言中的函数一词通常是指方法,尤其是静态方法;将方法(方法引用)作为一等公民进行操作的过程就称之为函数编程
2.2、函数式接口
函数式接口就是只定义一个抽象方法的接口
后面会介绍到:接口现在还可以拥有默认方法,哪怕接口中有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
@FunctionalInterface注解:
新版本的Java API,会发现函数式接口带有@FunctionalInterface的注解,这个注解用于表示该接口会设计成一个函数式接口。
如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
所以通常情况下我们约定俗成在函数式接口中添加 @FunctionalInterface注解(如果没有这个注解也不会报错)
2.3、Lambda表达式(匿名函数)的介绍
Lambda表达式会生成函数接口的一个实例,所以Lambda表达式(匿名函数)的参数类型和返回值类型必须要和函数接口中抽象方法的保持一致
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
Lambda(匿名函数)表达式的介绍:
1、下面的Lambda表达式表示:参数为空,方法体为空,没有返回值的匿名函数
() -> {}
2、下面的Lambda表达式表示:参数为空,返回值为"Raoul"的匿名函数
() -> "Raoul"
3、下面的Lambda表达式和第二个表达式含义相同
() -> {return "Mario";}
4、下面的Lambda表达式表示:参数为两个Apple 对象,返回值为 int 类型,方法体为:两个Apple对象中 weight 值的比较
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
回到上一章提到的“模板代码”问题:
new Thread(new Runnable() {
@Override
public void run() {
System.err.println("run......");
}
}).start();
使用Lambda表达式来优化代码如下:
new Thread(() -> System.err.println("lambda run......")).start();
怎么理解这个上面的代码?
Lambda表达式会生成函数接口的一个实例,上面的代码中 () -> System.err.println(“lambda run…”) 相当于执行了以下几步操作:
- 动态的生成一个匿名的 Runnable 函数接口实现类
- Lambda函数内容动态覆盖匿名实现类中唯一的run方法
- 创建匿名实现类的实例对象
- 将当前实现类对象作为参数传递
// 第一步:动态创建匿名的函数接口实现类
class Runnable_12315 implements Runnable{
@Override
public void run() {
}
}
class Runnable_12315 implements Runnable{
// 第二步:Lambda函数内容动态覆盖匿名实现类中唯一的run方法
@Override
public void run() {
System.err.println("lambda run......");
}
}
// 第三步:创建匿名实现类的实例对象
final Runnable_12315 runnable_12315 = new Runnable_12315();
// 第四步:将当前实现类对象作为参数传递
new Thread(runnable_12315).start();
2.4、方法引用
这里我有一个需求:指定一个目录,获取这个目录下所有的目录
public File[] findDirs(File dir){
File[] dirFiles = dir.listFiles(new java.io.FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
});
return dirFiles;
}
考虑一下:我们已经有一个方法isDirectory可以使用,为什么非得把它包在一个啰嗦的FileFilter类里面再实例化呢?因为在Java 8之前你必须这么做!
如今在Java 8里,使用Lambda表达式构建第一个版本:
public File[] findDirs2(File dir){
return dir.listFiles((file) -> {
return file.isDirectory();
});
}
使用Lambda表达式构建第二个版本:
public File[] findDirs2(File dir){
return dir.listFiles(file -> file.isDirectory());
}
方法引用的版本:
public File[] findDirs3(File dir){
return dir.listFiles(File::isDirectory);
}
由于你已经有了函数isDirectory,因此只需用Java 8的方法引用::语法(即“把这个方法作为值”)将其传给listFiles方法;当写下File::isDirectory的时候,你就创建了一个方法引用,你同样可以传递它.
File::isDirectory 相当于是 file -> file.isDirectory() 相当于执行了以下几步操作:
- 动态的生成一个匿名的FileFilter 函数接口实现类
- 将匿名实现类中唯一 accept 方法中的内容修改为:file.isDirectory()
- 创建匿名实现类的实例对象
- 将当前实现类对象作为参数传递
// 第一步:动态的生成一个匿名的**FileFilter** 函数接口实现类
class FileFilter_12315 implements java.io.FileFilter{
}
class FileFilter_12315 implements java.io.FileFilter{
// 第二步:将匿名实现类中唯一 accept 方法中的内容修改为:file.isDirectory()
@Override
public boolean accept(File file) {
return file.isDirectory();
}
}
// 第三步:创建匿名实现类的实例对象
java.io.FileFilter filter = new FileFilter_12315();
// 第四步:将当前实现类对象作为参数传递
dir.listFiles(filter);
2.5、流(Stream)
在Unix或Linux中,很多程序都从标准输入(Unix和C中的stdin,Java中的System.in)读取数据,然后把结果写入标准输出(Unix和C中的stdout,Java中的System.out)。
Unix的cat命令会把两个文件连接起来创建一个流,tr会转换流中的字符,sort会对流中的行进行排序,而tail -3则给出流的最后三行。Unix命令行允许这些程序通过管道(|)连接在一起,比如:
$ cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
上面的命令:会(假设file1和file2中每行都只有一个词)先把字母转换成小写字母,然后打印出按照词典排序出现在最后的三个单词。我们说sort把一个行流①作为输入,产生了另一个行流(进行排序)作为输出。
这个过程就像汽车组装流水线一样,汽车基础零件排队进入加工站,每个加工站会接收、修改汽车,然后将之传递给下一站做进一步的处理。尽管流水线实际上是一个序列,但不同加工站的运行一般是并行的,就好比工厂开辟了多条流水线并行处理数据。
现在再理解流的概念:流是一系列数据项(汽车基础零件),一次只生成一项(流水线中某个节点某个时间点只能处理一个汽车基础零件)。程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。
基于这一思想,Java 8在java.util.stream中添加了一个Stream API;Stream就是一系列T类型的项目。你现在可以把它看成一种比较花哨的迭代器。Stream API的很多方法可以链接起来形成一个复杂的流水线,就像先前例子里面链接起来的Unix命令一样。
这种做法的好处是:Java 8程序可以很方便的底层“暗箱操作”,使用多个CPU内核去分别执行你Stream流中的数据——这是几乎免费的并行,用不着去费劲搞Thread了
Stream 中的方法分为两种:惰性求值方法 和 及早求值方法;
- 惰性求值方法:方法的返回值是 Stream 对象的方法,如果 Stream 链式对象只调用惰性求值方法,则惰性求值方法的内容不会被调用(可以理解为变量没有使用,则变量没有初始化)
- 及早求值方法:方法的返回值是另一个值或为空的方法,只有Stream 链式对象最终调用了及早求值方法,之前配置的所有方法才会被一一调用。
惰性求值方法的特点介绍例子1:
由于 Stream 对象没有调用及早求值方法,所以惰性求值方法中的内容并不会被调用
@Test
public void inertiaMethod(){
final Stream<Integer> integerStream = Stream.of(1, 3, 5, 9, 21, 36);
// 由于 Stream 对象没有调用及早求值方法,所以惰性求值方法中的内容并不会被调用
final Stream<Integer> add1Stream = integerStream.map(num -> {
System.err.println(num);
return num + 1;
});
System.err.println("inertiaMethod()执行完毕........");
}
执行结果:
inertiaMethod()执行完毕........
惰性求值方法的特点介绍例子2:
先调用了惰性求值方法,然后调用了及早求值方法,所以惰性求值方法内容会被执行
@Test
public void earlyMethod(){
final IntStream intStream = IntStream.of(1, 3, 5, 9, 21, 36);
// 这里先调用了惰性求值方法,然后调用了及早求值方法,所以惰性求值方法内容会被执行
final int sum = intStream.map(num -> {
System.err.println(num);
return num + 1;
}).sum();
System.err.println("earlyMethod()执行的结果........"+sum);
}
执行结果:
1
3
5
9
21
36
earlyMethod()执行的结果........81
惰性求值方法的特点介绍例子3:
Stream 处理数据的特点:流中的每个数据项 依次调用第一个map方法, 第二个map方法,及早求值reduce方法,执行完所有配置了的 Stream 链式方法,下一个数据项才开始从头开始重新执行处理
@Test
public void stream01(){
final int sum = IntStream.of(2, 3, 5)
// 先调用了第一个map惰性求值方法
.map(num -> {
System.err.println("第一个map方法:"+num);
return num + 1;
// 然后调用第二个map惰性求值方法
}).map(num -> {
System.err.println("第二个map方法:"+num);
return num * 2;
// 这里调用了及早求值方法
}).reduce(0,(tempSum, item) -> {
System.err.println("reduce方法:"+item);
return tempSum + item;
});
System.err.println("执行的结果:"+sum);
}
执行结果:
第一个map方法:2
第二个map方法:3
reduce方法:6
第一个map方法:3
第二个map方法:4
reduce方法:8
第一个map方法:5
第二个map方法:6
reduce方法:12
执行的结果:26
2.6、默认方法
大家考虑一下这个问题:
Stream 提供了非常棒的数据处理方式,同时也是一个容器,但是目前我们最最最常用的容器为 List 或者是 Collection,所以我们需要在List 或者 Collection接口中添加一个转换为 Stream 的方法,这样是不是很棒?
貌似很棒,但是你有没有考虑到:那些已经实现了List 或者Collection接口实现类由于你添加了转换为 Stream的抽象方法,都编译报错了,这…(头皮爆炸了)
所以为了兼容低版本的JDK,所以JDK8中引入了默认方法的概念:使用 default 关键字,即使默认方法是在接口中,默认方法也不是抽象方法(方法内是有方法内容的),如果List 或者 Collection接口将对应的转换方法声明为默认方法,即使接口的实现类并没有实现对应的转换方法,程序也能正常运行(会调用接口的默认方法)。
例子:
定义 MyCollection 接口,接口中定义一个默认方法
// 定义一个接口
interface MyCollection<E>{
boolean add(E e);
void clear();
// 这里添加一个默认方法(注意默认方法并不是抽象方法)
default Stream stream() {
System.err.println("MyCollection接口的stream()......");
return Stream.of(1,2,3,4);
}
}
定义一个接口实现类,可以看到接口中的默认方法可以不实现
class MyCollectionImple1 implements MyCollection{
@Override
public boolean add(Object o) { return false; }
@Override
public void clear() {
System.err.println("MyCollectionImple实现类的clear()......");
}
}
定义一个接口实现类,可以覆盖接口中的默认方法
class MyCollectionImple2 implements MyCollection{
@Override
public boolean add(Object o) { return false; }
@Override
public void clear() {}
@Override
public Stream stream() {
System.err.println("MyCollectionImple2接口实现类的stream()......");
return Stream.of(5,6,7,8);
}
}
测试:
@Test
public void test(){
final MyCollectionImple1 imple1 = new MyCollectionImple1();
imple1.stream();
final MyCollectionImple2 imple2 = new MyCollectionImple2();
imple2.stream();
}
执行的结果:
MyCollection接口的stream()......
MyCollectionImple2接口实现类的stream()......
这里可以看到:接口实现类可以不实现接口中的默认方法,也可实现默认方法;有点类似于类与类之间的继承(但这可比类方法继承复杂的多,因为类只能单继承,接口实现类可以实现多个接口,如果多个接口中有相同的名称的默认方法,那该如何处理呢?)