JDK8 Lambda表达式的底层实现原理 (二)

875 阅读4分钟

我们现在已经学会使用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表达式在程序运行的时候行成一个类

  1. 在类中新增一个方法,这个方法的方法体就是Lambda表达式的代码
  2. 还会形成一个匿名内部类,实现接口,重写抽象方法
  3. 在接口的重写方法中会调用新生成的方法

Lambda表达式的省略模式

Lambda标准格式的基础上,使用省略写法的规则为:

  • 小括号内参数的类型可以省略
  • 如果小括号内有且只有一个参数,则小括号可以省略
  • 如果小括号内有且只有一个语句,可以同时省略大括号、return关键字及语句分号

省略前:

(int a ) ->{
	return new Person();
}

省略后:

a->new Person()

Lambda表达式的前提条件

Lambda表达式的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件需要特别注意:

  1. 方法的参数或局部变量类型必须为接口才能使用Lambda
  2. 接口中有且仅有一个抽象方法
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();
    }
}
  1. 方法的参数或变量的类型是接口
  2. 这个接口中只能有一个抽象方法

了解Lambda和匿名内部类在使用上的区别

  1. 所需的类型不一样

    匿名内部类: 需要的类型可以是类、抽象类、接口

    Lambda表达式:需要的类型必须是接口

  2. 抽象方法的数量不一样

    匿名内部类: 所需的接口中抽象方法的数量随意

    Lambda表达式:所需的接口只能有一个抽象方法

  3. 实现原理

    匿名内部类: 在编译后形成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 返回值类型 方法名() {
		 ....
   }
}

接口默认方法的使用

  1. 实现类直接调用接口默认方法
  2. 实现类重写接口默认方法
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 {
}

接口默认方法和静态方法的区别

  1. 默认方法通过实例调用,静态方法通过接口名调用
  2. 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法
  3. 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用