先来看一段代码:
final class A {
String selfIntroduction() {
return "I'm A";
}
}
class B {
String selfIntroduction() {
return "I'm B";
}
}
class Test {
public final A a = new A();
}
问题来了:
1. Test.a
能被替换吗?
2. Test.a
能被替换成B对象的实例吗?
3. 如果问题2成立,在成功替换对象之后,调用Test.a.selfIntroduction
方法,返回的是什么? 为什么会这样?
把代码稍微改一下:
final class A {
String selfIntroduction = "I'm A";
String selfIntroduction() {
return selfIntroduction;
}
}
class B {
String selfIntroduction = "I'm B";
String selfIntroduction() {
return selfIntroduction;
}
}
class Test {
public final A a = new A();
}
4. 在成功替换对象之后,调用Test.a.selfIntroduction
方法,返回的是什么? 为什么?
再把代码改一下:
final class A {
String selfIntroduction = "I'm A";
String selfIntroduction() {
return selfIntroduction;
}
}
class B {
String selfIntroduction = "I'm B";
}
class Test {
public final A a = new A();
}
5. 在成功替换对象之后,调用Test.a.selfIntroduction
方法,会报错吗? 为什么?
继续改一下代码:
final class A {
String selfIntroduction = "I'm A";
String selfIntroduction() {
return selfIntroduction;
}
}
class B {
String fakeSelfIntroduction = "I'm fake B";
String selfIntroduction = "I'm B";
}
class Test {
public final A a = new A();
}
6. 在成功替换对象之后,调用Test.a.selfIntroduction
方法,会报错吗? 如果不会报错,返回值是什么? 为什么会这样?
再改一次代码吧:
final class A {
String selfIntroduction = "I'm A";
String selfIntroduction() {
return selfIntroduction;
}
}
class B {
int i = 1;
String fakeSelfIntroduction = "I'm Fake B";
String selfIntroduction = "I'm B";
}
class Test {
public final A a = new A();
}
7. 在成功替换对象之后,调用Test.a.selfIntroduction
方法,会报错吗? 为什么?
更多问答 >>
-
每日一问 | Call requires API level 23 (current min is 14) 扫描出来的原理是?
2020-12-27 22:39 -
每日一问 | View invalidate() 相关的一些细节探究~
2020-12-27 22:38 -
每日一问 | RxJava中Observable、Flowable、Single、Maybe 有何区别?
2021-01-03 20:34 -
每日一问 | Java中匿名内部类写成 lambda,真的只是语法糖吗?
2021-01-11 00:00 -
每日一问 | Java 中的 lambda 与 Android 中的 lambda 有什么不同?
2021-01-31 17:20 -
每日一问 | 属性动画与硬件加速的相遇,不是你想的那么简单?
2020-10-26 23:45 -
每日一问 | 关于 RecyclerView$Adapter setHasStableIds(boolean)的一切
2020-10-26 23:44 -
每日一问 | 玩转 Gradle,可不能不熟悉 Transform,那么,我要开始问了。
2020-10-26 23:45 -
每日一问 | 启动了Activity 的 app 至少有几个线程?
2020-10-12 00:47 -
2020-10-03 11:43
首先,能替换,替换的方式是使用sun.misc.Unsafe,方法如下:
Unsafe类是已经灰飞烟灭了的sun公司留下来的,这个类很强大,能够直接操作到内存,借助它能实现很多超脱java语言认知的事情,更多可参考https://mp.weixin.qq.com/s/h3MB8p0sEA7VnrMXFq9NBA
然后,替换之后的方法调用以及字段调用的返回是啥?
回答这个问题前,先来补充下知识:
方法调用
先来看这段代码:
这就是一个简单的方法调用,这个方法调用对应到字节码上是长什么样子了?
可以看到这个调用具体写了调用哪一个类的哪一个方法,这个方法调用的简单流程是,找到这个方法,将调用这个方法的对象以及输入参数传进去,然后执行字节码指令。
字段引用
然后对于字段的调用,首先,对于一个java对象来说,其在内存中占的内存大小是确定的,如此我们才能够在new指令执行的时候为这个对象分配确定大小的内存空间,之后再创建的对象能够直接分配在这个内存空间的后面,这也是数组不能够自动扩容的原因,因为后面的内存可能已经被占用了。
然后,每一个对象,由于他的类结构是确定的,所以他所占的内存空间里,每一个字段的值存储的位置的偏移是确定的。
例如对于类:
偏移分别为:
然后当调用时:
其对应的字节码如下:
他的基本流程是,根据这个类的信息,也就是art/bytecode/yuan/A类,计算出其testField 属性所对应的偏移,然后在当前对象去这个偏移值。
结果解析
然后回到我们最初的问题,看如下代码:
他对应的结果如下:
先看强转前的结果,由于方法调用的是A类的方法,前面的字符串就很好理解,然后,由于当前的对象以及被强制换成了B,所以类名是art.bytecode.yuan.B。
然后看field,testField是A类的第一个属性(偏移不为0,因为有对象头,存储GC信息hash等),所以逻辑是取当前传入的对象的第一个属性,由于传入的对象是B,所以取B的第一个属性,也就是I'm B field22222222222。
至于为啥能强转为B,很好理解,因为这个内存地址的对象真的是B。
然后强转后的打印的结果,就很好理解了。
然后如果把B换成如下:
在System.out.println(test.a.testField);这段代码处会报错,空指针,因为在打印的时候,是走的如下方法:
然后看
首先这个对象不是空的,因为有值1,然后就对他掉toString,显然1这个内存地址没有指向对象,所以空指针了。
如果把i的默认值改为0,这里就会打印null了。
其实你在这里也能看出为啥要叫Unsafe类了,因为判空了但还是报了空指针的情况都是有可能发生的。。。。。。。。。。。
然后对于这7个问题,你要是能读懂到这里,相信你已经有答案了。
峰哥牛逼爆了
简直爱死徐佳峰了
begin end
峰哥换工作吗?字节跳动,简历投递:wangzhengyi.wzy@bytedance.com
本来准备年底的,但如果是头条的话,周末我就写简历。。。
字节跳动 懂车帝团队,欢迎周末写简历,秒推
补充:
看问题3:当使用Unsafe把Test.a
替换成B的对象实例之后,调用Test.a.selfIntroduction
方法,返回的是什么?按道理,既然已经替换成了B对象,调用其selfIntroduction
方法,返回值肯定就是"I'm B"了吧?写代码测试下:我们在替换Test.
运行结果:!!!!!为什么?!!a
的前后,分别打印了一次它的toString
和selfIntroduction
方法的返回值。test.a
明明已经变成了B的实例了,为什么此时调用selfIntroduction
返回的还是 I'm A 呢?看下字节码:这是对象替换前的:对象替换之后:根本没区别嘛!仔细看一下上面class A的代码,是用final修饰的,如果把它去掉,重新编译运行:会发现,这次的结果是 "正常" 的。去掉final之后,在替换对象后面的指令会发生什么变化吗?看看:咦?调用的还是A类的selfIntroduction
方法啊!同样的指令,为什么有final修饰时,返回值是"I'm A"(也就是最终调用了A类的selfIntroduction
方法),无final时就是"I'm B"呢?其实是这样的:熟悉java字节码的同学会知道,调用方法的指令有invokestatic、invokespecial、invokevirtual等,前面两个指令调用的方法,会在类加载时就钉死了,比如静态方法、private修饰的方法,还有构造函数等,因为它们都不会因为继承而改变原来的逻辑(非虚方法)。invokevirtual就是调用常规的实例方法(虚方法),比如上面的a.selfIntroduction
。在执行invokevirtual指令(调用虚方法)时,考虑到子类的重写,会进行一个【动态分派】的流程:JVM会从目标类的【虚方法表】中找到对应方法的地址,如果目标类有重写父类方法,那么对应方法的地址会变成子类重写的那个方法的地址,其他没有被重写的方法,地址跟父类中的一样。但是有一种情况例外!就是当这个方法被final修饰的时候!想想就知道了,final方法同样不会因为继承重写而覆盖原来的逻辑,所以它也是在类加载时确定的!也就是不管它当前引用的是哪个类的实例,最终调用的都是声明时的那个类的final方法!现在可以解释,为什么a
被替换B的对象实例之后,调用的仍然是A的final方法了:因为final方法跟static、private方法一样,不会因为继承重写而改变原来的逻辑,所以在类加载时就能确定它的地址,当它被调用时,就不需要走【动态分派】流程(不需要查【虚方法表】)了。问题4、6、7 @xujiafeng 同学已经讲了,就是因为字段偏移量的原因。这也能解释上面为什么去掉final后,明明字节码写着还是调用A类的
selfIntroduction
,最终却走了B类selfIntroduction
的逻辑:因为【虚方法表】也是通过索引来排序的。问题5,为什么B类没有了
如果你还是不明白,那我这个夜就白熬了。selfIntroduction
方法依然不会报错?牛逼
首先,小缘牛逼,是对上面回答的很好补充(ps:字节跳动懂车帝团队求贤若渴,简历发送 wangzhengyi.wzy@bytedance.com) 其次,还是想回复一下,其实问题5不报错的根本原因,还是 ...查看更多
首先,小缘牛逼,是对上面回答的很好补充(ps:字节跳动懂车帝团队求贤若渴,简历发送 wangzhengyi.wzy@bytedance.com) 其次,还是想回复一下,其实问题5不报错的根本原因,还是因为class A是final的,TestMain调用A类方法时不走动态分派流程。如果把A去掉final修饰符,这里还是会报NPE的。
以前一直以为final修饰的方法字节码会变成invoke-special,嗯....
嘻嘻嘻,点赞~
但是是一样的效果啦
据java虚拟机书上写的,被final修饰的方法是属于非虚方法,在编译期就能确定调用的具体是哪一个方法,至于为啥调用指令用的不是invoke-special,历史原因 ...查看更多
据java虚拟机书上写的,被final修饰的方法是属于非虚方法,在编译期就能确定调用的具体是哪一个方法,至于为啥调用指令用的不是invoke-special,历史原因
每日一问:
看到答案还不懂,是不是可以退出开发圈了.答案区,神仙打架啊
一问全不知
回家等通知