Android 避坑指南:Gson 又搞了个坑!

但是,这样的代码埋下了深深的坑。

什么样的坑呢?

再回到我们的刚才测试代码,我们现在尝试解析完成json字符串,调用一下girl.getBoyName():

public class Test01 {

public static void main(String[] args) {

Gson gson = new Gson();

String boyJsonStr = “{“boyName”:“zhy”,“girl”:{“girlName”:“lmj”}}”;

Boy boy = gson.fromJson(boyJsonStr, Boy.class);

System.out.println("boy name is = " + boy.boyName + " , girl name is = " + boy.girl.girlName);

// 新增

System.out.println(boy.girl.getBoyName());

}

}

很简单,加了一行打印。

这次,大家觉得运行结果是什么样呢?

还是没问题?当然不是,结果:

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支持非静态内部类,并且该内部类中可以访问外部类的属性和变量,但是在编译后,其实内部类会变成独立的类对象,例如下图:

01_01.png 让另一个类中可以访问另一个类里面的成员,那就必须要把被访问对象传进入了,想一定能传入,那么就是唯一的构造方法最合适了。

可以看到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。

四、Gson怎么构造的非静态匿名内部类对象?


现在我就一个好奇点,因为我们已经看到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的知识,可以参考:

每日一问 | Java里面还能这么创建对象?

五、如何避免这个问题?


其实最好的方式,会被Gson去做反序列化的这个model对象,尽可能不要去写非静态内部类。

在Gson的用户指南中,其实有写到:

https://github.com/google/gson/blob/master/UserGuide.md#TOC-Nested-Classes-including-Inner-Classes-

01_02.png

大概意思是如果你有要写非静态内部类的case,你有两个选择保证其正确:

  1. 内部类写成静态内部类;

  2. 自定义InstanceCreator
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

结语

  • 现在随着短视频,抖音,快手的流行NDK模块开发也显得越发重要,需要这块人才的企业也越来越多,随之学习这块的人也变多了,音视频的开发,往往是比较难的,而这个比较难的技术就是NDK里面的技术。
  • 音视频/高清大图片/人工智能/直播/抖音等等这年与用户最紧密,与我们生活最相关的技术一直都在寻找最终的技术落地平台,以前是windows系统,而现在则是移动系统了,移动系统中又是以Android占比绝大部分为前提,所以AndroidNDK技术已经是我们必备技能了。
  • 要学习好NDK,其中的关于C/C++,jni,Linux基础都是需要学习的,除此之外,音视频的编解码技术,流媒体协议,ffmpeg这些都是音视频开发必备技能,而且
  • OpenCV/OpenGl/这些又是图像处理必备知识,下面这些我都是当年自己搜集的资料和做的一些图,因为当年我就感觉视频这块会是一个大的趋势。所以提前做了一些准备。现在拿出来分享给大家。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

[外链图片转存中…(img-8oPvRKEW-1713770153338)]

结语

  • 现在随着短视频,抖音,快手的流行NDK模块开发也显得越发重要,需要这块人才的企业也越来越多,随之学习这块的人也变多了,音视频的开发,往往是比较难的,而这个比较难的技术就是NDK里面的技术。
  • 音视频/高清大图片/人工智能/直播/抖音等等这年与用户最紧密,与我们生活最相关的技术一直都在寻找最终的技术落地平台,以前是windows系统,而现在则是移动系统了,移动系统中又是以Android占比绝大部分为前提,所以AndroidNDK技术已经是我们必备技能了。
  • 要学习好NDK,其中的关于C/C++,jni,Linux基础都是需要学习的,除此之外,音视频的编解码技术,流媒体协议,ffmpeg这些都是音视频开发必备技能,而且
  • OpenCV/OpenGl/这些又是图像处理必备知识,下面这些我都是当年自己搜集的资料和做的一些图,因为当年我就感觉视频这块会是一个大的趋势。所以提前做了一些准备。现在拿出来分享给大家。

[外链图片转存中…(img-ws7X0bJG-1713770153339)]

[外链图片转存中…(img-Cm19wyW8-1713770153340)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值