1. 怎样理解 Lambda 表达式
- Lambda 这个名字来源于学术界的演算
- 不同于接口和匿名内部类的传递代码的方式,Lambda 表达式是一种紧凑的传递代码的方式。利用 Lambda 表达式可以实现简洁灵活的函数式编程
- 基于 Lambda 表达式,针对常见的集合数据处理,Java 8 引入了一套新的类库,位于包
java.util.stream
下,称为 Stream API。不同于容器类 API,Stream API 是函数式的,非常简洁、灵活、易读 - Stream API 是对容器类的增强,它可以将对集合数据的多个操作以流水线的方式组合在一起
- 利用 Lambda 表达式,Java 8 还增强了日期和时间 API
2. Lambda 表达式的语法
- Lambda 表达式由
->
分隔为两部分 - 前半部分是方法的参数,后半部分
{}
内是方法的代码
3. 使用 Lambda 表达式重写下面代码
1 | //列出当前目录下的所有扩展名为 .txt 的文件 |
标准写法
1
2
3
4
5
6
7
8File f = new File(".");
//不再有实现接口的模板代码,不再声明方法,也没有名字,而是直接给出了方法的实现代码
File[] files = f.listFiles((File dir, String name) -> {
if(name.endsWith(".txt")) {
return true;
}
return false;
});简化版本一
1
2
3
4File f = new File(".");
File[] files = f.listFiles((File dir, String name) -> {
return name.endsWith(".txt"); //对 if 判断语句的传统优化
});简化版本二
1
2
3File f = new File(".");
//当主体代码只有一条语句的时候,大括号 {} 和 return 关键字也可以省略
File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));简化版本三
1
2
3
4
5
6File f = new File(".");
//方法的参数类型声明也可以省略
File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));
//之所以可以省略方法的参数类型,是因为 Java 可以自动推断出来,它知道 listFiles() 方法接受的参数类型是 FilenameFilter。
//这个接口只有一个方法 accept(),这个方法的两个参数类型分别是 File 和 String简化版本四
1
2
3
4
5
6//当参数只有一个的时候,参数部分的括号可以省略
//比如,File 还有如下方法:public File[] listFiles(FileFilter filter)
//FileFilter 的定义为:public interface FileFilter { boolean accept(File path); }
//使用 FileFilter 重写上面的列举文件的例子
File f = new File(".");
File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));
4. 使用 Lambda 表达式重写下面代码
1 | File f = new File("."); |
1 | File f = new File("."); |
5. 使用 Lambda 表达式重写下面代码
1 | //提交一个最简单的任务 |
1 | ExecutorService executor = Executors.newFixedThreadPool(100); |
6. Lambda 表达式和匿名内部类的区别是
与匿名内部类类似,Lambda 表达式也可以访问定义在主体代码外部的变量,但对于局部变量,它也只能访问
final
类型的变量与匿名内部类的区别是,它不要求变量声明为
final
,但变量事实上不能被重新赋值这个原因与匿名内部类是一样的
- Java 会将
msg
的值作为参数传递给 Lambda 表达式,为 Lambda 表达式建立一个副本,它的代码访问的是这个副本,而不是外部声明的 msg 变量 - 如果允许
msg
被修改,则程序员可能会误以为 Lambda 表达式读到修改后的值,引起更多的混淆
- Java 会将
为什么非要建立副本,直接访问外部的
msg
变量不行吗- 不行,因为
msg
定义在栈中,当 Lambda 表达式被执行的时候,msg
可能早已被释放了 - 如果希望能够修改值,可以将变量定义为实例变量,或者将变量定义为数组
- 不行,因为
Lambda 表达式与匿名内部类很像,主要就是简化了语法,但 Lambda 表达式的内部实现并不是匿名内部类
- Java 会为每个匿名内部类生成一个类,但 Lambda 表达式不会
- Lambda 表达式通常比较短,为每个表达式生成一个类会生成大量的类,性能会受到影响
内部实现上,Java 利用了 Java 7 引入的为支持动态类型语言引入的
invokedynamic
指令、方法句柄method handle
等,具体实现比较复杂,实现细节可参见 http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html。需要知道的是,Lambda 表达式的 Java 实现是非常高效的,不用担心生成太多类影响性能的问题Lambda 表达式的类型是函数式接口
7. 什么是函数式接口
- Java 8 引入了函数式接口的概念
- 函数式接口也是接口,但只能有一个抽象方法,之所以强调是抽象方法,是因为 Java 8 中还允许定义静态方法和默认方法
- Lambda 表达式可以赋值给函数式接口
8. 怎样理解 @FunctionalInterface
注解
Java 8 中函数式接口都有一个注解
@FunctionalInterface
。比如1
2
3
4
public interface Runnable {
public abstract void run();
}注解
@FunctionalInterface
用于清晰地告知使用者这是一个函数式接口。不过,这个注解不是必需的不加,只要只有一个抽象方法,也是函数式接口。但如果加了,而又定义了超过一个抽象方法,Java 编译器会报错,类似于
@Override
注解
9. Java 8 中预定义的函数式接口有哪些
Java 8 定义了大量的预定义函数式接口,用于常见类型的代码传递,这些函数定义在包
java.util.function
下主要的预定义函数式接口
函数接口 方法定义 说明 Predicate
boolean test(T t)
谓词,测试输入是否满足条件 Function
R apply(T t)
函数转换,输入类型 T
,输出类型R
Consumer
void accept(T t)
消费者,输入类型 T
Supplier
T get()
工厂方法 UnaryOperator
T apply(T t)
函数转换的特例,输入和输出类型一样 BiFunction
R apply(T t, U u)
函数转换,接受两个参数,输出 R
BinaryOperator
T apply(T t, T u)
BiFunction
的特例,输入和输出类型一样BiConsumer
void accept(T t, U u)
消费者,接受两个参数 BiPredicate
boolean test(T t, U u)
谓词,接受两个参数 int
类型的函数式接口:除int
之外,对于基本类型boolean
、long
、double
,为避免装箱/拆箱,Java 8 也提供了一些专门的函数函数接口 方法定义 说明 IntPredicate
boolean test(int value)
谓词,测试输入是否满足条件 IntFunction
R apply(int value)
函数转换,输入类型 int
,输出类型R
IntConsumer
void accept(int value)
消费者,输入类型 int
IntSupplier
int getAsInt()
工厂方法 这些函数的作用是:它们被大量用于 Java 8 的函数式数据处理
Stream
相关的类中,即使不使用Stream
,也可以在自己的代码中直接使用这些预定义的函数
10. 使用预定义的 Predicate
过滤学生列表,筛选 90 分以上的学生
1 | //学生类 |
11. 使用预定义的 Function
转换学生列表,将学生名称转换为大写
1 | //借助 Function 写一个通用的方法 |
12. 使用预定义的 Consumer
转换学生列表,将学生名称转换为大写(直接修改原对象,而不是为每个学生创建新对象)
1 | //用 Consumer 写一个通用的方法 |
13. 怎样理解 Java 8 的方法引用语法
下面两种写法是等价的
List<String> names = map(students, t -> t.getName());
List<String> names = map(students, Studetn::getName);
Student::getName
这种写法是 Java 8 引入的一种新语法,称为:方法引用方法引用是 Lambda 表达式的一种简写方法,由
::
分隔为两部分,前面是类名或变量名,后面是方法名(不带小括号 ())方法可以是实例方法,也可以是静态方法,但含义不同:举例,以
Student
为例,先增加一个静态方法:public static String getCollegeName() {return "Laoma School"}
对于静态方法,下面两条语句是等价的:(参数都是空,返回类型为
String
)Supplier<String> s = Student::getCollegeName;
Supplier<String> s = () -> Student.getCollegeName();
对于实例方法,下面两条语句是等价的:(它的第一个参数就是该类型的实例)
Function<Student, String> f = Student::getName;
Function<Student, String> f = (Student t) -> t.getName();
对于
Student::setName
,它是一个BiConsumer
,即下面两条语句是等价的BiConsumer<Student, String> c = Student::seName;
BiConsumer<Student, String> c = (t, name) -> t.setName(name);
如果方法引用的第一部分是变量名,则相当于调用那个对象的方法。比如,假定
t
是一个Student
类型的变量,则下面两条语句是等价的Supplier<String> s = t::getName;
Supplier<String> s = () -> t.getName();
下面两条语句也是等价的
Consumer<String> consumer = t::setName;
Consumer<String> consumer = (name) -> t.setName(name);
对于构造方法,方法引用的语法是
类名::new
,如Student::new
,即下面两条语句是等价的BiFunction<String, Double, Student> s = (name, score) -> new Student(name, score);
BiFunction<String, Double, Student> s = Student::new;
14. Java 8 函数式编程中函数的复合是什么意思
- 函数式接口和 Lambda 表达式除了用作方法的参数,还可以用作方法的返回值,传递代码回调用者。将这两种用法结合起来,可以构造复合的函数,使程序简洁易读
- 复合函数经常会用到 Java 8 对接口的增强,即静态方法和默认方法
Comparator
、Consumer
、Predicate
等都有一些复合方法,被大量用于函数式数据处理 API 中