我们现在已经学会使用lamdba表达式了,现在同学们肯定很好奇Lambda是如何实现的,现在我们就来探究一lambda表达式的底层实现原理
@FunctionalInterface
public interface Swimmable {
public abstract void swimming();
}
public class demo01LambdaImpl {
public static void main(String[] args) {
// 匿名内部类在编译后会形成一个新的类,.$
goSwimming(new Swimmable() {
@Override
public void swimming() {
System.out.println("我是匿名内部类的游泳");
}
});
}
//参数无返回值的Lambda表达式
private static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
匿名内部类
我们看到匿名内部类可以在编译后产生一个类:demo01LambdaIntro$1.class
反编译这个类demo01LambdaIntro$1.class,得到如下代码
package com.example.jdk.demo01Lamdba;
public class demo04LambdaImpl$1 implements Swimmable {
public demo04LambdaImpl$1() {
}
public void swimming() {
System.out.println("我是lambda的游泳");
}
}
lambda表达式
我们再来看看Lambda的效果,代码如下
package com.example.jdk.demo01Lamdba;
public class demo04LambdaImpl {
public demo04LambdaImpl() {
}
public static void main(String[] args) {
goSwimming(() -> {
System.out.println("我是lambda的游泳");
});
}
private static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
运行程序,控制台可以得到预期的结果,但是并没有出一个新类,也就是说Lambda并没有在编译的时候产生一个新的类。实我们使用JDK自带的一个工具:javap,对字节码进反编译,查看字节码指令:
在控制台输入:
javap -c -p 文件名.class
-c: 表示对代码进行反汇编
-p: 表示所有的类和成员
反编译结果如下:
Lambda会在这个类中新生成一个私有的静态方法,Lambda表达式中的方法会放到这个新增的方法中:
private static void lambda$main$0();
可以确认lambda$main$0里面放的是Lambda中的内容,我们可以这么理解lambda$main$0方法:
public class demo04LambdaImpl {
public static void main(String[] args) {
...
}
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 运行的包名.类名
执行完成以后,会生成一个一个新的类,反编译效果如下:
public class demo04LambdaImpl {
public static void main(String[] args) {
goSwimming(new Swimming() -> {
demo04LambdaImpl.lambda$main$0();
});
}
private static void lambda$main$0() {
System.out.println("我是lambda的游泳");
}
}
小结:
匿名内部类会在编译的时候形成一个class文件
Lambda表达式在程序运行的时候行成一个类
- 在类中新增一个方法,这个方法的方法体就是Lambda表达式的代码
- 还会形成一个匿名内部类,实现接口,重写抽象方法
- 在接口的重写方法中会调用新生成的方法
Lambda表达式的省略模式
在Lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略
- 如果小括号内有且只有一个参数,则小括号可以省略
- 如果小括号内有且只有一个语句,可以同时省略大括号、return关键字及语句分号
省略前:
(int a ) ->{
return new Person();
}
省略后:
a->new Person()
Lambda表达式的前提条件
Lambda表达式的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件需要特别注意:
- 方法的参数或局部变量类型必须为接口才能使用Lambda
- 接口中有且仅有一个抽象方法
public interface Flyable{
public abstract void fly();
}
public class demo06LambdaCondition {
public static void main(String[] args) {
test(()-> System.out.println("我会飞了"));
}
public static void test(Flyable flyable){
flyable.fly();
}
Flyable f = ()->{
System.out.println("我会飞了");
};
/**
只有一个抽象方法的接口称为函数式接口,我们就能使用Lambda表达式
@FunctionalInterface 检测这个接口是不是只有一个抽象方法
*/
@FunctionalInterface
interface Flyable{
public abstract void fly();
}
}
- 方法的参数或变量的类型是接口
- 这个接口中只能有一个抽象方法
了解Lambda和匿名内部类在使用上的区别
-
所需的类型不一样
匿名内部类: 需要的类型可以是类、抽象类、接口
Lambda表达式:需要的类型必须是接口
-
抽象方法的数量不一样
匿名内部类: 所需的接口中抽象方法的数量随意
Lambda表达式:所需的接口只能有一个抽象方法
-
实现原理
匿名内部类: 在编译后形成class文件
Lambda表达式:在程序运行的时候动态生成class JDK8以前的接口
interface 接口名{
静态常量;
抽象方法;
}
JDK8对接口的增强,接口还可以有默认方法和静态方法
JDK8的接口
interface 接口名{
静态常量;
抽象方法;
默认方法;
静态方法;
}
接口引入默认方法的背景
在JDK8以前接口中只能有抽象方法,存在以下问题:
如果给接口新增抽象方法,所有实现类都必须重写这个抽象方法. 不利于接口的扩展.
public interface A {
public abstract void test01();
//public abstract void test02();
}
class B implements A{
@Override
public void test01() {
System.out.println("B test01");
}
}
class C implements A{
@Override
public void test01() {
System.out.println("C test01");
}
}
因此,在JDK8的时候接口新增了默认方法,效果如下:
public interface Map<K,V> {
default void forEach(BiConsumer<? super K, ? super V> action) {
}
}
接口的默认方法实现类不必重写,可以直接使用,实现类也可以根据需要重写,这样就方便接口的拓展
接口默认方法的定义格式
interface 接口名 {
修饰符 default 返回值类型 方法名() {
....
}
}
接口默认方法的使用
- 实现类直接调用接口默认方法
- 实现类重写接口默认方法
public class Demo2UseDefaultFunction {
public static void main(String[] args) {
BB bb = new BB();
bb.test01();
CC cc = new CC();
cc.test01();
}
}
interface AA {
public default void test01() {
System.out.println("我是接口AA的默认方法");
}
}
//默认方法使用方式一: 实现类可以直接使用
class BB implements AA {
}
//默认方法使用方式二: 实现类可以重写默认方法
class CC implements AA {
@Override
public void test01() {
System.out.println("我是CC重写的test01()");
}
}
接口中的静态方法
interface 接口名 {
修饰符 static 返回值类型 方法名() {
....
}
}
public class Demo2UseStaticFunction {
public static void main(String[] args) {
BBB bbb = new BBB();
// bbb.test01(); 不能使用
//使用接口名.静态方法名调用
AAA.test01();
}
}
interface AAA {
public static void test01() {
System.out.println("我是接口AA的静态方法");
}
}
//默认方法使用方式一: 实现类可以直接使用
class BBB implements AAA {
}
接口默认方法和静态方法的区别
- 默认方法通过实例调用,静态方法通过接口名调用
- 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法
- 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用