jdk8之所以引入Lambda表达式,是受到函数式编程的启发,将其以一种特殊的方式引入至java的面向对象编程中。 好处是在很多场景下可以大大简化编程,然而其引入的代价则是 java编译器 和 jvm虚拟机都需要做额外的工作以适应这种新的“函数式”编程语法。
在java编程中,有些一次性临时使用的对象方法,我们可以使用匿名内部类的方式来进行简化。Lambda表达式的一个核心想法就是,借用函数式编程思想和语法进行更彻底的简化,即只需要在代码中说明要做什么即可,编译器可以自动进行类、对象方法中的匹配,不需要再写额外的 new overwride 等等冗余的代码。(函数式编程:别给我扯那些那些类模板、对象属性方法啥的,直接告诉我要做什么,好吗?)
一 Lambda表达式初体验
在上面的例子中,直观的能体会Lambda表达式的作用。简化了什么内容呢?
new 关键词、匿名内部类实现的接口名称、方法签名 、@Override
多余的{} ;
等。
这种写法实在是非常清爽。这能够感受到Lambda表达式的简化,其实这也是Lambda表达式最终极的简化形态。
二 原理深入
那么其中的原理是什么呢?
为什么仅仅只需要声明“做什么事情”即可让编译器无歧义的理解代码呢?
这其中编译之后的class文件是如何的呢?
在jvm层面是否违背了面向对象调用方法运行呢?jvm运行的时候又是如何的呢?
从匿名内部类开始理解
我们可以从匿名内部类开始理解。在面向对象编程过程中我们往往是将我们要做的事情封装在一个方法中,使用方法之前可能要通过对象来进行调用,而要产生对象则必须通过类模板来进行创建,其中经历了两次包装。而匿名内部类则让我们免于写类模板,减少了一次复杂的包装。先看看匿名内部类的原理:
定义MyInterface
,同时AnonymousTest
类中的test
方法接收一个MyInterface
的实现对象,该实现对象采用匿名内部类的写法。
编译AnonymousTest
,输出如下,其中发现多了一个AnonymousTest$1.class
文件。
反编译查看AnonymousTest
和AnonymousTest$1.class
。
发现了一些端倪。猜测如下:
- 编译器帮我们自动生成了一个类,且自动实现了
MyInterface
接口重写了run
方法,方法体的内容就是我们在匿名内部类中重写的方法。 AnonymousTest
中main
方法中test
方法调用时,传递了一个对象,这个对象理应是AnonymousTest$1
对象,不过这个名字好像不对(猜测是反编译工具的一些处理不够完善,不过这个“new 1()
”倒是可以猜测到一些端倪。)
想进一步验证,上字节码!!!
AnonymousTest
的main
方法和test
方法字节码解读:
从main
方法的第0~4行中发现,的确是新建了一个AnonymousTest$1
对象并执行了初始化,第7行中将刚刚创建出来的AnonymousTest$1
对象的引用传递给test
方法,调用结束后直接返回。
test
方法:第0行将main
方法传递进来的AnonymousTest$1
对象的引用入操作数栈,第1行加载“hello
”字符串,第3
行以多态的形式调用MyInterface
接口中的run
方法,也就是动态调用,程序执行的过程中将会调用AnonymousTest$1
对象的run
方法。
再看AnonymousTest$1
对象的run
方法,将方法入参(String str)放入操作数栈,执行打印控制台方法。
完美!!! 到此分析结束(字节码yyds)。
总结:
- 匿名内部类的确是编译器帮我们自动生成了一个匿名类(这个匿名只是相对于程序员而言的,相对于编译器和jvm是有名字的,该类为xxx$x的格式。其中xxx为匿名内部类所在类的类名,x表示是这个类中的第几个类,从1开始(很显然不能从0开始,因为0一般表示“本”“自己”的意思,这个序号不能分配给其他类也是避免引起混淆,也可以类比java方法中的局部变量表中的索引0代表本对象的引用))。
- 编译器会将原始匿名内部类的写法转换为标准的传递对象引用的写法。然后交由jvm执行。
很显然,匿名内部类只是一个语法层面的优化,为了避免程序员多写一个类,创造了一种新的语法,然后将创建类的过程交给了编译器。对于jvm来说,其实并没有发生任何的变化。最终的结果就是程序员少写了一点代码,编译器做了一些额外的工作,总工作量其实并没有减少。(不过只要程序员写得少了,那就有这样做的必要,毕竟编译器做额外的重复工作并无所谓。)