/ 今日科技快讯 /
近日,特斯拉的中国竞争对手蔚来正进军挪威市场,计划在9月份向这个北欧国家出口其智能电动汽车。蔚来打算今年在挪威销售电动ES8 SUV,明年销售续航里程接近1000公里的ET7豪华电动汽车。在此之前,特斯拉的另一个中国竞争对手小鹏,已经在2020年12月底向挪威个人客户交付了100多辆G3智能SUV。显然,挪威正迅速成为中国电动汽车初创企业的关键出口市场。
/ 作者简介 /
大家周六好,明天就短暂休息一下吧,我们周日再见!
本篇文章来自
Omnipotent_7
同学的投稿,和大家分享了他理解的Lambda原理,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!
https://blog.csdn.net/weixin_43687181
/ Lambda表达式的背景 /
Lambda函数的概念其实有很久远的历史了,在Lisa,C#中早有实现。且近年来,开发者对语言的表现力有了更高的要求,Java也在JDK 1.8 中引入了Lambda函数这一概念。虽然截止到写下这段文字的一刻已经过去七年之久,但其底层的设计思想仍值得我们参考一番,以便我们更好地使用。
Lambda表达式的核心思想,就是将一段函数作为参数进行传递。那要怎么实现这个目的呢?我们慢慢来看一下。
传递一段代码
传递一个函数作为参数,这在C里面很好实现,函数指针嘛,一两个*号&号能解决的问题,C的语言特性具有先天的优势。
但是Java呢?我们都知道,Java的函数接收的参数都是对象(这个时候可以认为基本类型也是对象),可从来没听说过有函数对象这个说法啊。那要怎么整?
设想有以下场景,函数executeFunc,它接收一个函数,用这个接受到的函数打印另外一个传入的变量word。
static void executeFunc(方法??,String word) {
// todo 用传入的方法打印word变量
}
匿名内部类写法
先介绍一种曲线救国的方式,勤劳勇敢的程序员们想出来,直接传函数不行是吧,那我就用一个接口包裹住这个方法,传接口对象进去就可以啦!好方法,我们下面来看看代码。
package main;
interface Wrapper {
void myPrint(String w);
}
class Solution {
static void executeFunc(Wrapper w, String word) {
w.myPrint(word);
}
}
确实,问题解决了。那怎么在主函数调用呢?
public static void main(String[] args) {
executeFunc(new Wrapper() {
@Override
public void
myPrint(String w) {
// 个性化拓展,例如在打印之前记录时间什么的
System.out.println(w);
}
}, "Hello Lambda!");
}
看起来有一点麻烦,如果需要用executeFunc的次数多起来的时候,显然就会造成很多“不太必要”的代码了。这里说的不太必要是因为,我们必须满足编译器的需求来进行规范的语法编写,但实际上我们关心的逻辑仅仅是里面那一小段打印的语句:
// 个性化拓展,例如在打印之前记录时间什么的
System.out.println(w);
这就是lambda派上用场的地方了。
Lambda写法
其实如果你在编译器编写以上代码时,你会收到一个灰色的智能提示,让你用lambda重写以上语句。
我们应用一下这个修改试试看。
public static void main(String[] args) {
executeFunc(w -> {
// 个性化拓展,例如在打印之前记录时间什么的
System.out.println(w);
}, "Hello Lambda!");
}
我们看到,一大串的new操作,@Override重写被替换成一个传入参数,一对大括号,代码量大大减少。(实际上可以通过方法引用来进一步化简,但是在这里暂不作讨论了)
/ 两种写法的实际操作 /
终于介绍完两种常见的写法了。看了上面的写法,可能有人就会说,这看起来不是差不多嘛,lambda就是帮我们完成了那些override啊之类的东西的重写嘛,语法糖,甜甜的,就这?
非也非也,Lambda是Java 8的一个重要升级特性,为了自动生成这个语句,编译器装个插件啥的,写个宏定义啥的不就好了吗?为什么要费这么大功夫来折腾?下面就来细细分析两者的区别。下面会涉及到一些字节码的理解,不过不用担心,要用到的字节码指令我会讲清楚的(我也曾经深受其苦)。
完整代码如下,我们基于以下代码进行分析。
package main;
interface Wrapper {
void myPrint(String w);
}
class Solution {
static void executeFunc(Wrapper w, String word) {
w.myPrint(word);
}
public static void main(String[] args) {
// 匿名内部类写法
executeFunc(new Wrapper() {
@Override
public void myPrint(String w) {
// 个性化拓展,例如在打印之前记录时间什么的
System.out.println(w);
}
}, "Hello Lambda!");
// lambda写法
executeFunc(w -> {
// 个性化拓展,例如在打印之前记录时间什么的
System.out.println(w);
}, "Hello Lambda!");
}
}
匿名内部类的实际操作
我们先把lambda部分注释掉,仅保留匿名内部类写法的语句。利用IDEA的ShowByteCode功能进行查看。字节码(ByteCode)是JVM运行时读取执行的,一切的语法特性,都会在这里暴露无遗。下面先贴一段字节码。
// class version 55.0 (55)
// access flags 0x20
class main/Solution {
// compiled from: Solution.java
NESTMEMBER main/Solution$1
// access flags 0x0
INNERCLASS main/Solution$1 null null
// access flags 0x0
()V
L0
LINENUMBER 7 L0
ALOAD 0
INVOKESPECIAL java/lang/Object. ()V
RETURN
L1
LOCALVARIABLE this
Lmain/Solution; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x8
static executeFunc(Lmain/Wrapper;Ljava/lang/String;)V
L0
LINENUMBER 10 L0
ALOAD 0
ALOAD 1
INVOKEINTERFACE main/Wrapper.myPrint (Ljava/lang/String;)V (itf)
L1
LINENUMBER 11 L1
RETURN
L2
LOCALVARIABLE w Lmain/Wrapper; L0 L2 0
LOCALVARIABLE word Ljava/lang/String; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 15 L0
NEW main/Solution$1
DUP
INVOKESPECIAL main/Solution$1. ()V
LDC "Hello Lambda!"
INVOKESTATIC main/Solution.executeFunc (Lmain/Wrapper;Ljava/lang/String;)V
L1
LINENUMBER 29 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
JVM里是不管你内部内部类的,它只认类和类实例化的对象。众所周知,接口是不能实例化的,也就是说,是不能通过直接new的方式新建一个纯接口对象,而是要编写一个类来实现接口,进而实例化这个类。那注意到我们上面的语句。
//*****
new Wrapper() {
@Override
public void myPrint(String w) {
// 个性化拓展,例如在打印之前记录时间什么的
System.out.println(w);
}
}
//*****
这不就违背了这一规则了吗?其实不然。它看起来是这样写的,实际上还是通过类来实例化对象,只不过这个工作JVM替我们完成了。
回到字节码,在第六行,我们发现编译器新建了一个类,叫Solution$1。在42行进行了new操作,新建了一个Solution$1的对象以供后续操作。在44行进行了这个对象的初始化工作,init嘛,大家都懂。
本来内部类的名字应该是类似Solution$MyInnerClass这样的命名的,$符号之前其所在的父类Solution,MyInnerClass是内部类的名字。但是由于我们创建的是实现了Wrapper接口的匿名内部类,匿名没有名字,编译器就想,干脆就叫1吧,没错,就简简单单用个1来命名了。
什么?不是说不能用数字命名类吗?注意,我们平时编程就是写给老板(JVM)看的,所以有一大串语法规则,老板看了说OK,我再加一个东西,这个东西我就要这样子,你也不能说什么对吧。
不信你就在匿名内部类的实现里加一个语句,通过反射获取当前匿名内部类的类名,就会发现人家这个内部类的的确确叫 1,完整名字是main.Solution$1
// 匿名内部类写法
executeFunc(new Wrapper() {
@Override
public void myPrint(String w) {
// this指向当前匿名内部类的对象,通过对象获取类名
System.out.println(this.getClass().getName());
// 个性化拓展,例如在打印之前记录时间什么的
System.out.println(w);
}
}, "Hello Lambda!");
输出结果
C:\jdk11\bin\java.exe....
main.Solution$1
Hello Lambda!
Process finished with exit code 0
通过上面的例子,这样也可以理解什么是匿名内部类的“匿名”二字是什么意思了。
Lambda写法的实际操作
那么使用Lambda表达式的时候又发生了什么呢?还是之前的代码,我们注释掉匿名内部类的实现,查看其字节码。
// class version 55.0 (55)
// access flags 0x20
class main/Solution {
// compiled from: Solution.java
// access flags 0x19
public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup
// access flags 0x0
()V
L0
LINENUMBER 8 L0
ALOAD 0
INVOKESPECIAL java/lang/Object. ()V
RETURN
L1
LOCALVARIABLE this Lmain/Solution; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x8
static executeFunc(Lmain/Wrapper;Ljava/lang/String;)V
L0
LINENUMBER 11 L0
ALOAD 0
ALOAD 1
INVOKEINTERFACE main/Wrapper.myPrint (Ljava/lang/String;)V (itf)
L1
LINENUMBER 12 L1
RETURN
L2
LOCALVARIABLE w Lmain/Wrapper; L0 L2 0
LOCALVARIABLE word Ljava/lang/String; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 26 L0
INVOKEDYNAMIC myPrint()Lmain/Wrapper; [
// handle kind 0x6 : INVOKESTATIC
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;
// arguments:
(Ljava/lang/String;)V,
// handle kind 0x6 : INVOKESTATIC
main/Solution.lambda$main$0(Ljava/lang/String;)V,
(Ljava/lang/String;)V
]
LDC "Hello Lambda!"
INVOKESTATIC main/Solution.executeFunc (Lmain/Wrapper;Ljava/lang/String;)V
L1
LINENUMBER 31 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x100A
private static synthetic lambda$main$0(Ljava/lang/String;)V
L0
LINENUMBER 28 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 0
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 29 L1
RETURN
L2
LOCALVARIABLE w Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
字节码有点长,但是关键的地方就那么几个,我们仔细看看。
首先看到61行的这个地方。
private static synthetic lambda$main$0(Ljava/lang/String;)V
这个看起来很像一个函数的东西,其实就是一个函数,synthetic这个词的意思本来就是“人造的; (人工)合成的; 综合(型)的;”,作为关键字,它表示该方法由编译器自动生成。简而言之,你把它当做void,就很好阅读了。这是一个无返回值的静态方法,名称都定好啦,叫lambda$main$0,接受一个String类型的参数。里面又有一系列操作,println啥的。
诶?仔细想想,是不是很像我们之前写的那个东西?没错,就是那个lambda表达式。
w -> {
// 个性化拓展,例如在打印之前记录时间什么的
System.out.println(w);
}
// 就是上面这货
编译器把我们写的lambda表达式转换成了一个静态的私有函数,通过调用这个函数来解决传递一段代码的问题。一切都已经明了~
我们可以尝试在lambda表达式内部使用this关键字,试图像之前匿名内部类那样,获取当前类的类名,其实这个时候通过IDE你也能发现一些端倪了,它会提示你。
不能把this放到一个静态的语境之下,通过上面的分析得知,这个“语境”指的就是静态函数了。