很简单,加了一行打印。
这次,大家觉得运行结果是什么样呢?
还是没问题?当然不是,结果:
boy name is = zhy , girl name is = lmj
Exception in thread “main” java.lang.NullPointerException
at com.example.zhanghongyang.blog01.model.Boy$Girl.getBoyName(Boy.java:12)
at com.example.zhanghongyang.blog01.Test01.main(Test01.java:15)
Boy$Girl.getBoyName报出了npe,是girl为null?明显不是,我们上面打印了girl.name,那更不可能是boy为null了。
那就奇怪了,getBoyName里面就一行代码:
public String getBoyName() {
return boyName; // npe
}
到底是谁为null呢?
return boyName;只能猜测是某对象.boyName,这个某对象是null了。
这个某对象是谁呢?
我们重新看下getBoyName()返回的是boy对象的boyName字段,这个方法更细致一些写法应该是:
public String getBoyName() {
return Boy.this.boyName;
}
所以,现在问题清楚了,确实是Boy.this这个对象是null。
** 那么问题来了,为什么经过Gson序列化之后需,这个对象为null呢?**
想搞清楚这个问题,还有个前置问题:
在Girl类里面为什么我们能够访问外部类Boy的属性以及方法?
探索Java代码的秘密,最好的手段就是看字节码了。
我们下去一看Girl的字节码,看看getBodyName()这个“罪魁祸首”到底是怎么写的?
javap -v Girl.class
看下getBodyName()的字节码:
public java.lang.String getBoyName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #1 // Field this$0:Lcom/example/zhanghongyang/blog01/model/Boy;
4: getfield #3 // Field com/example/zhanghongyang/blog01/model/Boy.boyName:Ljava/lang/String;
7: areturn
可以看到aload_0,肯定是this对象了,然后是getfield获取 t h i s 0 字 段 , 再 通 过 this0字段,再通过 this0字段,再通过this0再去getfield获取boyName字段,也就是说:
public String getBoyName() {
return boyName;
}
相当于:
public String getBoyName(){
return $this0.boyName;
}
那么这个$this0哪来的呢?
我们再看下Girl的字节码的成员变量:
final com.example.zhanghongyang.blog01.model.Boy this$0;
descriptor: Lcom/example/zhanghongyang/blog01/model/Boy;
flags: ACC_FINAL, ACC_SYNTHETIC
其中果然有个this$0字段,这个时候你获取困惑,我的代码里面没有呀?
我们稍后解释。
再看下这个this$0在哪儿能够进行赋值?
翻了下字节码,发现Girl的构造方法是这么写的:
public com.example.zhanghongyang.blog01.model.Boy$Girl(com.example.zhanghongyang.blog01.model.Boy);
descriptor: (Lcom/example/zhanghongyang/blog01/model/Boy;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/example/zhanghongyang/blog01/model/Boy;
5: aload_0
6: invokespecial #2 // Method java/lang/Object.“”😦)V
9: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/example/zhanghongyang/blog01/model/Boy$Girl;
0 10 1 this$0 Lcom/example/zhanghongyang/blog01/model/Boy;
可以看到这个构造方法包含一个形参,即Boy对象,最终这个会赋值给我们的$this0。
而且我们还发下一件事,我们再整体看下Girl的字节码:
public class com.example.zhanghongyang.blog01.model.Boy$Girl {
public java.lang.String girlName;
final com.example.zhanghongyang.blog01.model.Boy this$0;
public com.example.zhanghongyang.blog01.model.Boy$Girl(com.example.zhanghongyang.blog01.model.Boy);
public java.lang.String getBoyName();
}
其只有一个构造方法,就是我们刚才说的需要传入Boy对象的构造方法。
这块有个小知识,并不是所有没写构造方法的对象,都会有个默认的无参构造哟。
也就是说:
如果你想构造一个正常的Girl对象,理论上是必须要传入一个Boy对象的。
所以正常的你想构建一个Girl对象,Java代码你得这么写:
public static void testGenerateGirl() {
Boy.Girl girl = new Boy().new Girl();
}
先有body才能有girl。
这里,我们搞清楚了非静态内部类调用外部类的秘密了,我们再来想想Java为什么要这么设计呢?
因为Java支持非静态内部类,并且该内部类中可以访问外部类的属性和变量,但是在编译后,其实内部类会变成独立的类对象,例如下图:
让另一个类中可以访问另一个类里面的成员,那就必须要把被访问对象传进入了,想一定能传入,那么就是唯一的构造方法最合适了。
可以看到Java编译器为了支持一些特性,背后默默的提供支持,其实这种支持不仅于此,非常多的地方都能看到,而且一些在编译期间新增的这些变量和方法,都会有个修饰符去修饰:ACC_SYNTHETIC。
不信,你再仔细看下$this0的声明。
final com.example.zhanghongyang.blog01.model.Boy this$0;
descriptor: Lcom/example/zhanghongyang/blog01/model/Boy;
flags: ACC_FINAL, ACC_SYNTHETIC
到这里,我们已经完全了解这个过程了,肯定是Gson在反序列化字符串为对象的时候没有传入body对象,然后造成$this0其实一直是null,当我们调用任何外部类的成员方法、成员变量是,熬的一声给你扔个NullPointerException。
现在我就一个好奇点,因为我们已经看到Girl是没有无参构造的,只有一个包含Boy参数的构造方法,那么Girl对象Gson是如何创建出来的呢?
是找到带Body参数的构造方法,然后反射newInstance,只不过Body对象传入的是null?
好像也能讲的通,下面看代码看看是不是这样吧:
这块其实和我之前写的另一个Gson的坑的源码分析类似了:
Android避坑指南,Gson与Kotlin碰撞出一个不安全的操作
我就长话短说了:
Gson里面去构建对象,一把都是通过找到对象的类型,然后找对应的TypeAdapter去处理,本例我们的Girl对象,最终会走走到ReflectiveTypeAdapterFactory.create然后返回一个TypeAdapter。
我只能再搬运一次了:
ReflectiveTypeAdapterFactory.create
@Override
public TypeAdapter create(Gson gson, final TypeToken type) {
Class<? super T> raw = type.getRawType();
if (!Object.class.isAssignableFrom(raw)) {
return null; // it’s a primitive!
}
ObjectConstructor constructor = constructorConstructor.get(type);
return new Adapter(constructor, getBoundFields(gson, type, raw));
}
重点看constructor这个对象的赋值,它一眼就知道跟构造对象相关。
ConstructorConstructor.get
public ObjectConstructor get(TypeToken typeToken) {
final Type type = typeToken.getType();
final Class<? super T> rawType = typeToken.getRawType();
// …省略一些缓存容器相关代码
ObjectConstructor defaultConstructor = newDefaultConstructor(rawType);
if (defaultConstructor != null) {
return defaultConstructor;
}
ObjectConstructor defaultImplementation = newDefaultImplementationConstructor(type, rawType);
if (defaultImplementation != null) {
return defaultImplementation;
}
// finally try unsafe
return newUnsafeAllocator(type, rawType);
}
可以看到该方法的返回值有3个流程:
newDefaultConstructor
newDefaultImplementationConstructor
newUnsafeAllocator
我们先看第一个newDefaultConstructor
private ObjectConstructor newDefaultConstructor(Class<? super T> rawType) {
try {
final Constructor<? super T> constructor = rawType.getDeclaredConstructor();
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
return new ObjectConstructor() {
@SuppressWarnings(“unchecked”) // T is the same raw type as is requested
@Override public T construct() {
Object[] args = null;
return (T) constructor.newInstance(args);
// 省略了一些异常处理
};
} catch (NoSuchMethodException e) {
return null;
}
}
可以看到,很简单,尝试获取了无参的构造函数,如果能够找到,则通过newInstance反射的方式构建对象。
追随到我们的Girl的代码,并没有无参构造,从而会命中NoSuchMethodException,返回null。
返回null会走newDefaultImplementationConstructor,这个方法里面都是一些集合类相关对象的逻辑,直接跳过。
那么,最后只能走:newUnsafeAllocator 方法了。
从命名上面就能看出来,这是个不安全的操作。
newUnsafeAllocator最终是怎么不安全的构建出一个对象呢?
往下看,最终执行的是:
public static UnsafeAllocator create() {
// try JVM
// public class Unsafe {
// public Object allocateInstance(Class<?> type);
// }
try {
Class<?> unsafeClass = Class.forName(“sun.misc.Unsafe”);
Field f = unsafeClass.getDeclaredField(“theUnsafe”);
f.setAccessible(true);
final Object unsafe = f.get(null);
final Method allocateInstance = unsafeClass.getMethod(“allocateInstance”, Class.class);
return new UnsafeAllocator() {
@Override
@SuppressWarnings(“unchecked”)
public T newInstance(Class c) throws Exception {
assertInstantiable©;
return (T) allocateInstance.invoke(unsafe, c);
}
};
} catch (Exception ignored) {
}
// try dalvikvm, post-gingerbread use ObjectStreamClass
// try dalvikvm, pre-gingerbread , ObjectInputStream
}
嗯…我们上面猜测错了,Gson实际上内部在没有找到它认为合适的构造方法后,通过一种非常不安全的方式构建了一个对象。
关于更多UnSafe的知识,可以参考:
其实最好的方式,会被Gson去做反序列化的这个model对象,尽可能不要去写非静态内部类。
在Gson的用户指南中,其实有写到:
https://github.com/google/gson/blob/master/UserGuide.md#TOC-Nested-Classes-including-Inner-Classes-

大概意思是如果你有要写非静态内部类的case,你有两个选择保证其正确:
-
内部类写成静态内部类;
-
自定义InstanceCreator
2的示例代码在这,但是我们不建议你使用。
嗯…所以,我简化的翻译一下,就是:
别问,问就是加static
不要使用这种口头的要求,怎么能让团队的同学都自觉遵守呢,谁不注意就会写错,所以一般遇到这类约定性的写法,最好的方式就是加监控纠错,不这么写,编译报错。
我在脑子里面大概想了下,有4种方法可能可行。
嗯…你也可以选择自己想下,然后再往下看。
-
最简单、最暴力,编译的时候,扫描model所在目录,直接读java源文件,做正则匹配去发现非静态内部类,然后然后随便找个编译时的task,绑在它前面,就能做到每次编译时都运行了。
-
Gradle Transform,这个不要说了,扫描model所在包下的class类,然后看类名如果包含A B 的 形 式 , 且 构 造 方 法 中 只 有 一 个 需 要 A 的 构 造 且 成 员 变 量 包 含 B的形式,且构造方法中只有一个需要A的构造且成员变量包含 B的形式,且构造方法中只有一个需要A的构造且成员变量包含this0拿下。
-
AST 或者lint做语法树分析;
-
运行时去匹配,也是一样的,运行时去拿到model对象的包路径下所有的class对象,然后做规则匹配。
好了,以上四个方案是我临时想的,理论上应该都可行,实际上不一定可行,欢迎大家尝试,或者提出新方案。
有新的方案,求留言补充下知识面
鉴于篇幅…
不,其实我一个都没写过,不太想都写一篇了,这样博客太长了。
-
方案1,大家拍大腿都能写出来,过,不过我感觉1最实在了,而且触发速度极快,不怎么影响研发体验;
-
方案2,大家查一下Transform基本写法,利用javassist,或者ASM,估计也问题不大,过;
-
方案3,AST的语法我也要去查,我写起来也费劲,过;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。





既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)
尾声
如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
这里,笔者分享一份从架构哲学的层面来剖析的视频及资料给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

Android进阶学习资料库
一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

大厂面试真题
PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《2019-2021字节跳动Android面试历年真题解析》

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
础和重要的事情做深度研究。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
这里,笔者分享一份从架构哲学的层面来剖析的视频及资料给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。
[外链图片转存中…(img-umtQ6hSZ-1711555277022)]
Android进阶学习资料库
一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!
[外链图片转存中…(img-66EXgTVm-1711555277022)]
大厂面试真题
PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-3EfP2qUb-1711555277022)]
《2019-2021字节跳动Android面试历年真题解析》
[外链图片转存中…(img-CJHRA3dK-1711555277023)]
文章探讨了在使用Gson进行序列化时,非静态内部类Girl如何引用外部类Boy的属性导致空指针异常。作者通过分析字节码揭示了内部类的构造机制,并指出Gson如何创建对象,最后提出了避免此类问题的建议和监控策略。
404

被折叠的 条评论
为什么被折叠?



