Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本。 Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,它支持函数式编程,新的 JavaScript 引擎,新的日期 API,新的Stream API 等。
Lambda表达式介绍
当需要启动一个线程去完成任务时,通常会通过 Runnable 接口来定义任务内容,并使用 Thread 类来启动该线程。这个时候可以只用匿名类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行代码");
}
}).start();
在这个代码中匿名内部类所做的事情有:
- 定义了一个没有名字的类
- 这个类实现类Runnable接口
- 创建了这个类的对象
可以看出使用匿名内部类语法是很冗余的,其实我们最关注的是run方法和里面要执行的代码
使用Lambda表达式写法,我们只需要将要执行的代码放到一个Lambda表达式中,不需要定义类,不需要创建对象。
//使用Lambda表达式
new Thread(() -> {
System.out.println("Lambda表达式执行");
}).start();
Lambda表达式的优点
可以简化匿名内部类,是代码变得更加简洁
Lambda的标准格式
Lambda表达式是一个匿名函数,函数相当与Java中的方法 (参数列表) -> { 方法体 } ->没有实际含义起到连接的作用
public class LambdaDemo03 {
public static void main(String[] args) {
ArrayList<Person> persons = new ArrayList<>();
persons.add(new Person("张三",32,175));
persons.add(new Person("李四",28,165));
persons.add(new Person("王五",46,205));
persons.add(new Person("赵六",16,185));
/*Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge()-o2.getAge();
}
});*/
Collections.sort(persons,(Person o1,Person o2)->{
return o1.getAge()-o2.getAge();
});
persons.forEach((t)->{
System.out.println(t);
});
}
}
无参数无返回值的Lambda和有参数有返回值的Lambda
public interface Smokeable {
public abstract int smoking(String name);
}
public interface Swimmable {
public abstract void swimming();
}
public class LambdaDemo02 {
public static void main(String[] args) {
goSwimming(new Swimmable() {
@Override
public void swimming() {
System.out.println("我是匿名内部类的游泳");
}
});
//无参数无返回值的Lambda
//可以认为Lambda表达式就是对接口中的抽象方法的重写
goSwimming(()->{
System.out.println("我是Lambda的游泳");
});
System.out.println("----------------");
goSmoking(new Smokeable() {
@Override
public int smoking(String name) {
System.out.print("匿名内部类抽了"+name+"的烟");
return 1;
}
});
//有参数有返回值的Lambda
goSmoking((String name)->{
System.out.print("Lambda抽了"+name+"的烟");
return 2;
});
}
public static void goSwimming(Swimmable s){
s.swimming();
}
public static void goSmoking(Smokeable s){
int result=s.smoking("普皖");
System.out.println(result+"根");
}
}
Lambda的实现原理
首先是匿名内部类
public class LambdaDemo04 {
public static void main(String[] args) {
//匿名内部类
goSwimming(new Swimmable() {
@Override
public void swimming() {
System.out.println("使用匿名内部类实现游泳");
}
});
/*goSwimming(()->{
System.out.println("Lambda表达式中的游泳");
});*/
}
public static void goSwimming(Swimmable swimmable){
swimmable.swimming();
}
}
将上面的代码执行之后可以在存储的位置找到有$的.class文件
将它反编译(反编译可以参考http://t.csdn.cn/X0KQK)之后可以得到这样的内容
而Lambda在编译之后并没有生成和匿名内部类生成的一样的文件,同时使用反编译工具还会出现报错的情况,我们就可以使用JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令。
可以在DOS命令行输入
javap -c -p 文件名.class
其中-c:表示对代码进行反汇编 -p:显示所有类和成员
就得到了这样的效果
在这里我们可以看到在类中多出了一个私有的静态方法 lambda$main$0 。
这时我们可以在Lambda中设置断点
这也就可以确认 lambda$main$0 里面放的就是Lambda中的内容,可以将Lambda表达式中的内容理解为:
private static void lambda$main$0() {
System.out.println("Lambda游泳");
}
关于这个方法 lambda$main$0 的命名:以lambda开头,因为是在main()函数里使用了lambda表达式,所以带有 $main表示,因为是第一个,所以$0
那么,如何调用这个方法呢?其实Lambda在运行的时候会生成一个内部类,为了验证是否生成内部类,可以在运行时加 上 -Djdk.internal.lambda.dumpProxyClasses ,加上这个参数后,运行时会将生成的内部类class码输出到一个文 件中。使用java命令如下:
java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名
注意:这里包含了包名,所以在使用之前要退出到包外面哦
这样的话就生成了一个.class文件,将其反编译之后得到了如下内容
可以看到这个匿名内部类实现了 Swimmable 接口,并且重写了 swimming 方法, swimming 方法调用 Demo04LambdaImpl.lambda$main$0() ,也就是调用Lambda中的内容。最后可以将Lambda理解为:
public class Demo04LambdaImpl {
public static void main(String[] args) {
goSwimming(new Swimmable() {
public void swimming() {
Demo04LambdaImpl.lambda$main$0();
}
});
}
private static void lambda$main$0() {
System.out.println("Lambda表达式游泳");
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
总结:
Lambda在程序运行的时候形成一个类
- 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
- 还会形成一个匿名内部类,实现接口,重写抽象方法
- 在接口的重写方法中会调用新生成的方法.
Lambda省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
1. 小括号内参数的类型可以省略
2. 如果小括号内有且仅有一个参数,则小括号可以省略
3. 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
这样的话前面所写过的LambdaDemo03中的代码可以省略成这样:
不过省略的前提都是建立在规则之上的
Lambda的前提条件
Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,它是由前提的
1.方法的参数或局部变量类型必须为接口才能使用Lambda
2. 接口中有且仅有一个抽象方法
public class LambdaDemo05 {
public static void main(String[] args) {
//方法的参数或局部变量类型必须为接口才能使用Lambda
test(()->{
});
Flyable f = () ->{
System.out.println("我会飞");
};
}
public static void test(Flyable a){
}
}
// 只有一个抽象方法的接口成为函数式接口,我们就能使用Lambda
interface Flyable{
//接口中有且只能有一个方法
public abstract void eat();
// public abstract void eat();
}
函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以 适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
FunctionalInterface注解
与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注 解可用于一个接口的定义上
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即 使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
Lambda和匿名内部类对比
1. 所需的类型不一样
- 匿名内部类,需要的类型可以是类,抽象类,接口
- Lambda表达式,需要的类型必须是接口
2. 抽象方法的数量不一样
- 匿名内部类所需的接口中抽象方法的数量随意
- Lambda表达式所需的接口只能有一个抽象方法
3. 实现原理不同
- 匿名内部类是在编译后会形成class
- Lambda表达式是在程序运行的时候动态生成class
当接口中只有一个抽象方法时,建议使用Lambda表达式,其他其他情况还是需要使用匿名内部类
Lambda表达式的冗余场景
这样的一个表达式:
LambdaInterface5 lambdaInterface5=(a->{
return a+10;
});
首先通过省略我们可以得到下面的式子:
LambdaInterface5 lambdaInterface7 = a -> a+10
同时还可以写一个方法,让Lambda表达式更加简洁
public static int add(int a){
return a+10;
}
//Lambda表达式就可以变成这样
LambdaInterface5 lambdaInterface8 = a -> add(a);
在这个Lambda表达式中,我们在Lambda中所指定的方法,已经有其他方法存在相同的方案,而这个Lambda起到的作用也仅仅知识调用了这个方法而已。可以直接将其引用过去。就是使用“方法引用”的方式,代码如下
LambdaInterface5 lambdaInterface8 = LambdaDemo02::add;
方法引用
方法引用的格式
符号表示为 ::
符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。