【版权申明】非商业目的注明出处可自由转载
博文地址:
出自: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.class
和Student$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技术有兴趣,建议关注我
文章评论