网易首页 > 网易号 > 正文 申请入驻

Android开发太难了:Java Lambda ≠ Android Lambda

0
分享至

我又来了,继续回归写作中,目标 1 月 2 篇。

需要两篇才能阐述清楚Java Lambda ≠ Android Lambda,本篇为上篇,先解释清楚 Java Lambda 的一些知识。

耐心阅读本文,你一定会有收获。

1

Java Lambda 与 匿名内部类

测试环境JDK8。

首先我们看一段比较简单的代码片段:


public class TestJavaAnonymousInnerClass {

public void test() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("hello java lambda");
}
};
runnable.run();
}
}

先问个简单的问题,如果我javac编译一下,你觉得会生成几个class文件?

不用问,肯定是两个,一个是TestJavaLambda.class,一个是

TestJavaLambda$1.class,那么试下:

没错,确实两个,扎实的Java基础怎么会被这种问题打败。

大家都知道上面这个匿名内部类的写法,我们可以换成lambda表达式的写法对吧,甚至编译器都会提醒你使用lambda,我们改成lambda表达式的写法:


public class TestJavaLambda {
public void test() {
Runnable runnable = () -> {
System.out.println("hello java lambda");
runnable.run();

再问个简单的问题,如果我javac编译一下,你觉得会生成几个class文件?

嗯...你在搞我?这和刚才的问题有啥区别?

还认为是两个吗?我们再javac试一下?

不好意思,只有一个class文件了。

那么,我的一个新的问题来了:

Java匿名内部类的写法和Lambda表达式的写法,在编译期这么看肯定有区别的,那么有何区别?

2

Java Lambda的背后,invokedynamic的出现

看这类问题,第一件事肯定是对比字节码了,那我们javap -v 一哈,看一下test()方法区别:

匿名内部类的test():


public void test();

descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class com/example/zhanghongyang/blog02/TestJavaAnonymousInnerClass$1
3: dup
4: aload_0
5: invokespecial #3 // Method com/example/zhanghongyang/blog02/TestJavaAnonymousInnerClass$1."":(Lcom/example/zhanghongyang/blog02/TestJavaAnonymousInnerClass;)V
8: astore_1
9: aload_1
10: invokeinterface #4, 1 // InterfaceMethod java/lang/Runnable.run:()V
15: return

很简单,就是new了一个TestJavaAnonymousInnerClass$1对象,然后调用其run()方法。

有个比较有意思的,就是调用构造方法的时候先aload_0,0就是当前对象this,把this传过去了,这个就是匿名内部类可以持有外部类对象的秘密,其实把当前对象this引用给了人家。

再来看lambda的test():


public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return

和匿名内部类不同,取而代之的是一个invokedynamic指令。

如果大家比较熟悉Java字节码方法调用相关,应该经常会看到一个问题:

invokespecial,invokevirtual,invokeinterface,invokestatic,invokedynamic有和区别?

invokespecial 其实上面一段字节码上也出现了,一般指的是调用super方法,构造方法,private方法等;special嘛,指定的意思,调用的都是一些确定调用者的方法。

你可能会问,调用一个类的方法,调用者还能有不确定的时候?

有呀,比如重载,是不是能将父类的方法调用转而变成子类的?

所以类中非private成员方法,一般调用指令为invokevirtual。

invokeinterface,invokestatic字面意思理解就可以了。

这块大概解释是这样的,如果有困惑自己打字节码看就好了,例如抽象类抽象方法调用和接口方法调用指令一样吗?加了final修饰的方法不能被复写,指令会有变化吗?

最后一个就是invokedynamic了:

一般很罕见,今天我们也算是见到了,在Java lambda表达式的时候能够见到。

一些深入的研究,可以看这里:

每日一问 | Java中匿名内部类写成 lambda,真的只是语法糖吗?

https://www.wanandroid.com/wenda/show/16717

我们现在知道使用了lambda表达式之后,和匿名内部类去比较,字节码有比较大的变化,那么更好奇了:

lambda表达式运行的时候,背后到底是什么样的呢?

3

lambda表达式不是真的没有内部类生成

想了解一段代码运行时状态,最简单的方式是什么呢?

嗯...debug?

现在IDE都越来越智能了,很多时候debug一些编译细节都给你抹去了。

有个比较简单的方式,打堆栈,我们修改下代码:


public class TestJavaLambda {
public void test() {
Runnable runnable = () -> {
System.out.println("hello java lambda");

int a = 1/0;
};
runnable.run();
}

public static void main(String[] args) {
new TestJavaLambda().test();
}
}

运行下,看下出错的堆栈:


hello java lambda
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.example.zhanghongyang.blog02.TestJavaLambda.lambda$test$0(TestJavaLambda.java:8)
at com.example.zhanghongyang.blog02.TestJavaLambda.test(TestJavaLambda.java:10)
at com.example.zhanghongyang.blog02.TestJavaLambda.main(TestJavaLambda.java:14)

看下到底和何方神圣调用的我们的run方法:

嗯...最后的堆栈是:


TestJavaLambda.lambda$test$0(TestJavaLambda.java:8)

是我们TestJavaLambda中的lambda$test$0方法调用的?

是我们刚才发编译看漏了,还有这个方法?我们再反编译看下:


javap /Users/zhanghongyang/repo/KotlinLearn/app/src/main/java/com/example/zhanghongyang/blog02/TestJavaLambda.class
Compiled from "TestJavaLambda.java"
public class com.example.zhanghongyang.blog02.TestJavaLambda {
public com.example.zhanghongyang.blog02.TestJavaLambda();
public void test();
public static void main(java.lang.String[]);
private void lambda$test$0();

这次javap -p 查看,-p代表private方法也输出出来。

还真有这个方法,看下这个方法的字节码:


private static void lambda$test$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #8 // String hello java lambda
5: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 7: 0
line 8: 8

很简单,就是我们上面lambda表达式{}里面的内容,打印一行日志。

那这个方法是test调用的?不对呀,这个堆栈好像有问题,我们在回头看下刚才堆栈:


Exception in thread "main" java.lang.ArithmeticException: / by zero

at com.example.zhanghongyang.blog02.TestJavaLambda.lambda$test$0(TestJavaLambda.java:8)
at com.example.zhanghongyang.blog02.TestJavaLambda.test(TestJavaLambda.java:10)
at com.example.zhanghongyang.blog02.TestJavaLambda.main(TestJavaLambda.java:14)

有没有发现这个堆栈太过于简单了,我们的Runnable.run的调用栈呢?

这个堆栈应该是被简化了,那我们再加一行日志,看下run()方法执行时,自己身处于哪个类?

我们在run方法里面加了一行


System.out.println(this.getClass().getCanonicalName());

看下输出:


com.example.zhanghongyang.blog02.TestJavaLambda

嗯..其实我们执行了一个废操作,当前这个方法里面的代码都被放到lambda$test$0()了,当然输出是TestJavaLambda。

不行了,我要放大招了。

我们修改下方法,让这个进程活的久一点:


public void test() {
Runnable runnable = () -> {
System.out.println("hello java lambda");
System.out.println(this.getClass().getCanonicalName());
// 新增
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
int a = 1 / 0;
runnable.run();

运行后...

切到命令行,执行jps命令,查看当前程序进程的pid:


java zhanghongyang$ jps

99315 GradleDaemon
3682 TestJavaLambda
21298 Main
3685 Jps
3258 GradleDaemon
1275
3261 KotlinCompileDaemon

看到了3682,然后执行


jstack 3682

太感人了,终于把这行隐藏的run方法的堆栈找出来了。

这里大家不要太在意jps,jstack这些指令,都是jdk自带的,你就知道能查堆栈就行了,别出去搜这两个命令去啦,文章看完再说。 另外获取堆栈其实也能通过方法调用,小缘是通过Reflection.getCallerClass看的。

到现在我们具体真相又进了一步:

我们lambda$test$0()方法是这个对象:

com.example.zhanghongyang.blog02.TestJavaLambda$$Lambda$1/1313922862的

run方法调用的。

我们又能下个结论了:

文中lambda表达式的写法,在运行时,会帮我们生成中间类,类名格式为 原类名$$Lambda$数字,然后通过这个中间类最终完成调用。

那么你可能表示不服:

你说运行时生成就生成呀?你拿出来给我看看?

嗯...等会我拿出来给你看。

不过我们先思考另一个问题。

4

编译产物中遗漏的信息

上文我们一直在说:

  1. 对于文中例子中的Lambda表达式编译时没有生成中间类;

  2. 运行时帮我们生成了中间类;

那有个很明显的问题,编译时你没给我生成,运行时生成了;运行时它怎么知道要不要生成,生成什么样的类,你编译产物就那一个class文件,里面肯定要包含这类信息的呀?

是这么个道理。

我们再次发编译javap -v查看,在输出信息的最后:


SourceFile: "TestJavaLambda.java"
InnerClasses:
public static final #78= #77 of #81; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #35 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;
Method arguments:
#36 ()V
#37 invokespecial com/example/zhanghongyang/blog02/TestJavaLambda.lambda$test$0:()V
#36 ()V

果然包含一段信息,而且包含TestJavaLambda.lambda$test$0关键词。

大家不用管那么多,你就知道,文中lambda的例子,会在编译的class文件中新增一个方法lambda$test$0(),并且会携带一段信息告知JVM在运行时创建一个中间class。

其实LambdaMetafactory.metafactory正是用来生成中间class的,jdk中也有相关类可以查看,后续我们再详细说这个。

5

把中间类拿出来看看?

我们一直说运行时帮我们生成了一个中间类,类名大概为:TestJavaLambda$$Lambda$1,但是口说无凭,得拿出来大伙才信,对吧。

还好不是说我吃了两碗凉粉...

我们刚才说了JVM帮我们生成了中间类,其实java在运行的时候可以带很多参数,其中有个系统属性很神奇,我用给你们看:


java -Djdk.internal.lambda.dumpProxyClassescom.example.zhanghongyang.blog02.TestJavaLambda

加上这个系统属性运行,可以dump出生成的类:

是不是有点意思。

其实动态代理中间也会生成代理类,也可以通过类似方式导出。

然后我们看看这个类呗,这个类我们就不太在乎细节了,直接AS里面看反编译之后的:

真简单...

所以,本文中的例子,Lambda表达式和匿名内部类的区别还是挺大的,大家只要了解:

  1. invokedynamic可以用于lambda;

  2. Java lambda表达式的中间类并不是没有,而是在首次运行时生成的。

下面有个灵魂一问:

你看这些有啥用?

毕竟我是搞Android的,其实我更在乎Android中lambda的实现,所以就先以Java Lambda为开始了,至于你问我为啥要看Android Lambda实现,毕竟现在经常要字节码插抓桩,自定义Transform,对于一些类背后的行为还是要搞清楚的。

但是,大家一定要注意,本文讲的是 Java lambda 的原理。

不要套用到Android上!

不要套用到Android上!

不要套用到Android上!

最后, 祝大家周五工(摸)作(鱼)愉快。

--- End ---

点击关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
女子新房装玫红色入户门贴花壁纸,网友直呼“全网独一无二”,当事人:装修花费近100万元,老公每次来都像游客一样

女子新房装玫红色入户门贴花壁纸,网友直呼“全网独一无二”,当事人:装修花费近100万元,老公每次来都像游客一样

极目新闻
2026-01-07 13:36:53
中方反制后,日本萌生大胆想法,西方媒体发出提醒,别忘了16年前

中方反制后,日本萌生大胆想法,西方媒体发出提醒,别忘了16年前

博览历史
2026-01-07 17:10:36
巧立名目地从老百姓口袋里掏钱,真是不遗余力

巧立名目地从老百姓口袋里掏钱,真是不遗余力

胖胖说他不胖
2026-01-07 10:00:09
合口味深圳地铁广告引争议!企业致歉:涉事广告已调整更换

合口味深圳地铁广告引争议!企业致歉:涉事广告已调整更换

南方都市报
2026-01-07 16:34:20
闫学晶风波不断升级!网友扒出其儿子考中戏新疆班,分数要低30分

闫学晶风波不断升级!网友扒出其儿子考中戏新疆班,分数要低30分

小徐讲八卦
2026-01-07 16:31:06
争议!CBA官方解说员公然搞地域歧视:听到两岸猿声 处罚结果来了

争议!CBA官方解说员公然搞地域歧视:听到两岸猿声 处罚结果来了

念洲
2026-01-08 07:24:21
坐稳东部第一!活塞双核缺席大胜公牛 斯图尔特31分生涯新高

坐稳东部第一!活塞双核缺席大胜公牛 斯图尔特31分生涯新高

醉卧浮生
2026-01-08 10:17:03
寒风中,南京数十民工为何扒在桥栏上当街吃午饭?

寒风中,南京数十民工为何扒在桥栏上当街吃午饭?

扬子晚报
2026-01-07 12:13:02
美媒:克林根曾被视为杨瀚森过渡替身 如今已成开拓者建队基石

美媒:克林根曾被视为杨瀚森过渡替身 如今已成开拓者建队基石

罗说NBA
2026-01-08 06:52:21
英国证实参与美国北大西洋扣押油轮行动

英国证实参与美国北大西洋扣押油轮行动

澎湃新闻
2026-01-08 02:02:18
柬政府确认陈志6日已被遣返回中国,柬国王颁布王令撤销其柬埔寨国籍

柬政府确认陈志6日已被遣返回中国,柬国王颁布王令撤销其柬埔寨国籍

红星新闻
2026-01-07 23:38:19
闫学晶的事越闹越大,辽宁卫视估计要慌了

闫学晶的事越闹越大,辽宁卫视估计要慌了

麦杰逊
2026-01-07 20:16:03
四川高县一村支书暴打残疾村妇致轻伤二级!法院:免于刑事处罚!

四川高县一村支书暴打残疾村妇致轻伤二级!法院:免于刑事处罚!

兵叔评说
2026-01-07 12:13:15
神奇的4-3!纽卡3次扳平+102分钟超时绝杀创纪录 3连胜升英超第6

神奇的4-3!纽卡3次扳平+102分钟超时绝杀创纪录 3连胜升英超第6

我爱英超
2026-01-08 07:29:14
“刚买的新车,就要拆发动机大修!”知名大牌汽车,引发沪上消费者“集体维权”

“刚买的新车,就要拆发动机大修!”知名大牌汽车,引发沪上消费者“集体维权”

新民晚报
2026-01-07 20:30:39
雷军全面回应“营销大师”标签:娱乐节目中刘强东团队开个玩笑,被人放大利用,现在听到营销两个字都有点恶心

雷军全面回应“营销大师”标签:娱乐节目中刘强东团队开个玩笑,被人放大利用,现在听到营销两个字都有点恶心

每日经济新闻
2026-01-08 00:48:20
去年全国公安机关共有210名民警、142名辅警因公牺牲

去年全国公安机关共有210名民警、142名辅警因公牺牲

新京报
2026-01-08 10:12:06
江苏调查组在徐湖平别墅搜出啥?太离谱!

江苏调查组在徐湖平别墅搜出啥?太离谱!

鹤羽说个事
2026-01-07 11:06:57
原来,重复到极致就是天赋! 重复熟练,熟能生巧

原来,重复到极致就是天赋! 重复熟练,熟能生巧

夜深爱杂谈
2026-01-06 21:05:20
出差前我把家里地暖关了,当晚楼下阿姨在群里开骂…

出差前我把家里地暖关了,当晚楼下阿姨在群里开骂…

极品小牛肉
2026-01-05 14:43:53
2026-01-08 10:40:49
君伟说
君伟说
分享职场故事
356文章数 48关注度
往期回顾 全部

科技要闻

雷军:现在听到营销这两个字都有点恶心

头条要闻

博主发4条微博被控损害华为商誉 二审定罪免罚

头条要闻

博主发4条微博被控损害华为商誉 二审定罪免罚

体育要闻

卖水果、搬砖的小伙,与哈兰德争英超金靴

娱乐要闻

《马背摇篮》首播,革命的乐观主义故事

财经要闻

农大教授科普:无需过度担忧蔬菜农残

汽车要闻

不谈颠覆与奇迹,智驾企业还能聊点什么?

态度原创

时尚
教育
家居
健康
数码

蓝色+灰色、红色+棕色,这4组配色怎么搭都好看!

教育要闻

电气专业为什么非要去国家电网

家居要闻

理性主义 冷调自由居所

这些新疗法,让化疗不再那么痛苦

数码要闻

AMD计划2026Q2推出桌面版锐龙AI 400处理器,PRO款也存在

无障碍浏览 进入关怀版