秒懂Java之深入理解Lambda表达式

【版权申明】非商业目的注明出处可自由转载
博文地址:
出自:shusheng007

概述

今天在Pluralsight看了一个讲Java Lambda 表达式的视频教程,觉得很好,自己研究并记录分享一下以飨读者。

因为Java8已经出来好久了,Lambda已经被大量使用了,所以这里只是分享一下对其的思考和总结,不准备过多讲解其用法,目的是使我们对其有更加深刻的理解。

匿名类到Lambda表达式

我们知道,只有函数接口才可以使用Lambda表达式。

函数接口:只有一个abstract的方法的接口

那我们怎么将实现了函数接口的匿名类转换成Lambda表达式呢?我们以code来说话:

示例1,抽象方法无入参,无返回值

我们以Runnable函数接口为例

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

匿名类:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("hello thread");
    }
};
new Thread(runable).start();

Lambda 表达式

  • 将方抽象方法括号及其中参数拷贝出来并加上->
    ()->

  • 将抽象方法方法体拷贝出来,如果其中只有一句代码则{} 可以省略

    ()->{ System.out.println("hello thread");}
    
    省略括号后
    ()->System.out.println("hello thread")
    
    new Thread(() -> System.out.println("hello thread")).start();    

    示例2,抽象方法带入参,带返回值的情形

    例如用于比较的Comparator函数接口

    @FunctionalInterface
    public interface Comparator<T> {
    int compare(T o1, T o2);
    ...
    }

    匿名类

    Comparator<Integer> comparator = new Comparator<Integer>() {
     @Override
     public int compare(Integer o1, Integer o2) {
         return Integer.compare(o2, o1);
     }
    };

    转换为Lambda表达式

  • 将方抽象方法括号及其中参数拷贝出来并加上->
    (Integer o1, Integer o2) ->

  • 将抽象方法方法体拷贝出来放在->后面

    (Integer o1, Integer o2) ->`{
        return Integer.compare(o2, o1);
    }
  • 简化
    参数类型可以省略,如果方法体只有一句代码则{}return 关键字可以省略

    (o1, o2)-> Integer.compare(o2, o1)

    不是说越简单越好,如果你发现带上类型可读性更好,那就带上,完全没有问题。

不知道你们平时在开发中是否遇到过特别复杂函数接口,手写lambda表达式变得很困难,不好理解。此时反而利用IDE的提示功能直接new匿名类对象反而更方便,然后写完了再使用IDE一键将其转换为Lambda表达式。

Method reference(方法引用)

Lambda的一种简写方式,无他。在可以使用Lambda的地方都可以使用Method Reference, 反之亦然。唯一决定你使用哪种方式的是可读性,哪个可读性高就使用哪个。

其语法为

类名::方法名

其中方法既可以是类方法,也可以是实例方法。

 //方法引用
 stream.forEach(System.out::println);
 //Lambda表达式
 stream.forEach(x->System.out.println(x));

灵魂拷问

Lambda 有类型吗?有的话是什么类型?

Lambda可以赋值给变量吗?可以当方法的入参和返回值吗?

Lambda是对象吗?有的话我们可以在代码中引用它吗?

第一问:
Java是强类型语言,在Java中任何事物都有类型,Lambda也不例外,它的类型就是其对应的函数接口。

第二问:
Lambda可以赋值给变量,而且可以作为方法的参数及返回值

第三问:
Lambda是个对象吗?这块情况比较就比较复杂了,我只能将自己的调查和理解呈上,至于更深刻的机制需要你去研究。不过我这里说的,对于普通程序员已经足够了,我个人觉得这部分很有意思,如果你也想知道Java编译器到底如何处理Lambda表达式这种语法糖的话,应该接着往下看。

首先我们来看一段代码

package top.ss007;
...
public class Student {
    Runnable runnable = () -> {
    };
    Comparator<Integer> comparator = (o1, o2) -> 1;

    Runnable runnableAnon = new Runnable() {
        @Override
        public void run() {
        }
    };
}

定义了一个Student类,里面声明了3个field,两个赋值为Lambda表达式,一个赋值为匿名内部类对象。

我们使用Java编译器javac将其编译为class文件

javac -g  Student.java

-g 保留所有调试信息

执行上述命令后生成了Student.classStudent$1.class两个文件

然后我们使用字节码查看器ByteCodeViewer查看一下这两个文件,从命名上就可以看出Student$1.class是Student类的内部类.

我们先看使用匿名内部类实现函数接口的部分:

Runnable runnableAnon = new Runnable() {
    @Override
    public void run() {
    }
};

我们知道Javac 会为其生成一个实现了Runnable接口的Student的内部类,这块我们就不看字节码了直接看生成的代码类

class Student$1 implements Runnable {
   // $FF: synthetic field
   final Student this$0;

   Student$1(Student this$0) {
      this.this$0 = this$0;
   }

   public void run() {
   }
}

可见,非静态的内部类是会持有其包含类的一个引用的。

接下来看一下其具体的赋值字节码

public Student() { // <init> //()V
    <localVar:index=0 , name=this , desc=Ltop/ss007/Student;, sig=null, start=L1, end=L2>

    L1 {
        aload0 // reference to self
        invokespecial java/lang/Object.<init>()V
    }

...

    L5 {
        aload0 // reference to self
        new top/ss007/Student$1
        dup
        aload0 // reference to self
        invokespecial top/ss007/Student$1.<init>(Ltop/ss007/Student;)V
        putfield top/ss007/Student.runnableAnon:java.lang.Runnable
        return
    }
}

上面是Student类的构造函数的字节码

L1{} 里面是调用Object的构造函数的代码,又一次印证了Object是所有类型的基类。

L5{} 的代码是我们要关注的,通过new top/ss007/Student$1 new 一个Student$1的对象,初始化后赋值给field runableAnon.

通过上面的调查我们已经清楚了javac是如何处理使用匿名内部类实现函数接口的方式,接下来让我看一下Lambda表达式是如何处理的。

查看Student的字节码文件可以发现如下两段代码

 private static synthetic lambda$new$1(java.lang.Integer arg0, java.lang.Integer arg1) { //(Ljava/lang/Integer;Ljava/lang/Integer;)I
     <localVar:index=0 , name=o1 , desc=Ljava/lang/Integer;, sig=null, start=L1, end=L2>
     <localVar:index=1 , name=o2 , desc=Ljava/lang/Integer;, sig=null, start=L1, end=L2>

     L1 {
         iconst_1
         ireturn
     }
     L2 {
     }
 }

 private static synthetic lambda$new$0() { //()V
     L1 {
         return
     }
 }

其中有两个使用了synthetic的方法,说明这两个方法是编译器帮我们生成的。
lambda$new$0 对应的是

()->{}

lambda$new$1 对应的是

(o1, o2) -> 1;

赋值语句位于Student的构造函数中,如下所示:

public Student() { // <init> //()V
    <localVar:index=0 , name=this , desc=Ltop/ss007/Student;, sig=null, start=L1, end=L2>
...
    L3 {
        aload0 // reference to self
        invokedynamic java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; : run()Ljava/lang/Runnable; ()V top/ss007/Student.lambda$new$0()V (6) ()V
        putfield top/ss007/Student.runnable:java.lang.Runnable
    }
    L4 {
        aload0 // reference to self
        invokedynamic java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; : compare()Ljava/util/Comparator; (Ljava/lang/Object;Ljava/lang/Object;)I top/ss007/Student.lambda$new$1(Ljava/lang/Integer;Ljava/lang/Integer;)I (6) (Ljava/lang/Integer;Ljava/lang/Integer;)I
        putfield top/ss007/Student.comparator:java.util.Comparator
    }
...
}

L3{} 块内是

Runnable runnable = () -> {   };

的处理代码

L4{} 块内是

Comparator<Integer> comparator = (o1, o2) -> 1;

的处理代码

其中最为关键的就是

invokedynamic java/lang/invoke/LambdaMetafactory.metafactory(
        Ljava/lang/invoke/MethodHandles$Lookup;
        Ljava/lang/String;
        Ljava/lang/invoke/MethodType;
        Ljava/lang/invoke/MethodType;
        Ljava/lang/invoke/MethodHandle;
        Ljava/lang/invoke/MethodType;)
Ljava/lang/invoke/CallSite; : run()Ljava/lang/Runnable; ()V top/ss007/Student.lambda$new$0()V (6) ()V

上面的代码的作用是什么呢?
总的来说,其是JVM在Runtime时创建 () -> { } 对应的类,即在运行时创建一个实现了Runable接口的类(字节码)。

我们看到此处使用了invokedynamic ,只是为了动态的生成其对应类的字节码,但是Lambda方法的执行仍然使用的是 invokevirtual或者invokeinterface, 在首次调用时,生成对应类的字节码,然后就缓存起来,下次使用缓存。

那么是时候回答一下开头的问题了:每一个Lambda表达式是否对应一个对象?
答案是:是的!但是,注意但是,每个Lambda是对应一个对象,但是有可能出现多对一的情况。
例如Lambda1,Lambda2 结构相同,那么他们在JVM中就有可能对应同一个对象

总结

写一篇误人子弟的文章实在是太费时间了,因为你要不断的确认自己写的是不是真的可以实现,而不是自己的想当然。

技术真的是没有止境,应尽早确定自己的方向,少年加油。

我会不定期写一些有质量的IT博文,如果你对IT技术有兴趣,建议关注我

You May Also Like

About the Author: shusheng007

发表评论

邮箱地址不会被公开。