先问个简单的问题,如果我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表达式的写法,在编译期这么看肯定有区别的,那么有何区别?
二、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,真的只是语法糖吗?
我们现在知道使用了lambda表达式之后,和匿名内部类去比较,字节码有比较大的变化,那么更好奇了:
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方法调用的。
面试准备+复习分享:
为了应付面试也刷了很多的面试题与资料,现在就分享给有需要的读者朋友,资料我只截取出来一部分哦
到了3682,然后执行
jstack 3682
太感人了,终于把这行隐藏的run方法的堆栈找出来了。
这里大家不要太在意jps,jstack这些指令,都是jdk自带的,你就知道能查堆栈就行了,别出去搜这两个命令去啦,文章看完再说。
另外获取堆栈其实也能通过方法调用,小缘是通过Reflection.getCallerClass看的。
到现在我们具体真相又进了一步:
我们lambda$test$0()
方法是这个对象:com.example.zhanghongyang.blog02.TestJavaLambda$$Lambda$1/1313922862
的run方法调用的。
面试准备+复习分享:
为了应付面试也刷了很多的面试题与资料,现在就分享给有需要的读者朋友,资料我只截取出来一部分哦
[外链图片转存中…(img-ZHjnx0N5-1716570568712)]