输出结果:

null

是不是有些奇怪,感觉意外绕过了Kotlin的空类型检查。

所以那位出问题的同学,在这里之后数据就出了问题,导致一直排查困难。

我们再改一下代码:

data class Person(var name: String, var age: Int): People(){

}

我们让Person继承自People类:

public class People {

public People(){

System.out.println(“people cons”);

}

}

在People类的构造方法中打印日志。

我们都清楚,正常情况下,一般构造子类对象,必然会先执行父类的构造方法。

运行一下:

没有执行父类构造方法,但对象构造出来了

这里可以猜到,Person对象的构建,并不是常规的构建对象,没有走构造方法。

那么它是怎么做到的呢?

那只能去Gson的源码中取找答案了。

找到其怎么做的,其实就相当于解答了我们文首的问题。

追查原因


Gson这样构造出一个对象,但是没有走父类构造这种,如果真是的这样,那么是极其危险的。

会让程序完全不符合运行预期,少了一些必要逻辑。

所以我们提前说一下,大家不用太惊慌,并不是Gson很容易出现这样的情况,而是恰好上例的写法碰上了,我们一会会说清楚。

首先我们把Person这个kotlin的类,转成Java,避免背后藏了一些东西:

反编译之后的显示

public final class Person extends People {

@NotNull

private String name;

private int age;

@NotNull

public final String getName() {

return this.name;

}

public final void setName(@NotNull String var1) {

Intrinsics.checkParameterIsNotNull(var1, “<set-?>”);

this.name = var1;

}

public final int getAge() {

return this.age;

}

public final void setAge(int var1) {

this.age = var1;

}

public Person(@NotNull String name, int age) {

Intrinsics.checkParameterIsNotNull(name, “name”);

super();

this.name = name;

this.age = age;

}

// 省略了一些方法。

}

可以看到Person有一个包含两参的构造方法,并且这个构造方法中有name的空安全检查。

也就是说,正常通过这个构造方法构建一个Person对象,是不会出现空安全问题的。

那么只能去看看Gson的源码了:

Gson的逻辑,一般都是根据读取到的类型,然后找对应的TypeAdapter去处理,本例为Person对象,所以会最终走到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个流程:

  1. newDefaultConstructor

  2. newDefaultImplementationConstructor

  3. 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反射的方式构建对象。

追随到我们的Person的代码,其实该类中只有一个两参的构造函数,并没有无参构造,从而会命中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在没有找到无参的构造方法后,通过sun.misc.Unsafe构造了一个对象。

注意:Unsafe该类并不是所有的Android 版本中都包含,不过目前新版本都包含,所以Gson这个方法中有3段逻辑都是用来生成对象的,你可以认为3重保险,针对不同平台。 本文测试设备:Android 29模拟器

我们这里暂时只讨论sun.misc.Unsafe,其他的其实一个意思。

sun.misc.Unsafe和许API?

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。

https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

具体可以参考美团的这篇文章。

好了,到这里就真相大白了。

原因是我们Person没有提供默认的构造方法,Gson在没有找到默认构造方法时,它就直接通过Unsafe的方法,绕过了构造方法,直接构建了一个对象。

到这里,我们收获了:

  1. Gson是如何构建对象的?

  2. 我们在写需要Gson转化为对象的类的时候,一定要记得有默认的构造方法,否则虽然不报错,但是很不安全!

  3. 我们了解到了还有这种Unsafe黑科技的方式构造对象。

回到文章开始的问题


Java中咋么构造一个下面的Student对象呢?

public class Student {

private Student() {

throw new IllegalArgumentException(“can not create.”);

}

public String name;

}

我们模仿Gson的代码,编写如下:

try {

val unsafeClass = Class.forName(“sun.misc.Unsafe”)

val f = unsafeClass.getDeclaredField(“theUnsafe”)

f.isAccessible = true

val unsafe = f.get(null)

val allocateInstance = unsafeClass.getMethod(“allocateInstance”, Class::class.java)

val student = allocateInstance.invoke(unsafe, Student::class.java)

(student as Student).apply {

name = “zhy”

}

println(student.name)

} catch (ignored: Exception) {

ignored.printStackTrace()

}

输出:

zhy

成功构建。

Unsafe 一点用没有?


看到这里,大家可能最大的收获就是了解Gson构建对象流程,以及以后写Bean的时候会注意提供默认的无参构造方法,尤其在使用Kotlin data class的时候。

那么刚才我们所说的Unsafe方法就没有其他实际用处吗?

这个类,提供了类似C语言指针一样操作内存空间的能力。

大家都知道在Android P上面,Google限制了app对hidden API的访问。

但是,Google不能限制自己对hidden API访问对吧,所以它自己的相关类,是允许访问hidden API的。

那么Google是如何区分是我们app调用,还是它自己调用呢?

通过ClassLoader,系统认为如果ClassLoader为BootStrapClassLoader则就认为是系统类,则放行。

那么,我们突破P访问限制,其中一个思路就是,搞一个类,把它的ClassLoader换成BootStrapClassLoader,从而可以反射任何hidden api。

怎么换呢?

只要把这个类的classLoader成员变量设置为null就可以了。

参考代码:

private void testJavaPojie() {

try {

Class reflectionHelperClz = Class.forName(“com.example.support_p.ReflectionHelper”);

Class classClz = Class.class;

Field classLoaderField = classClz.getDeclaredField(“classLoader”);

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

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

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

img

img

img

img

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

如果你觉得这些内容对你有帮助,可以扫码领取!!!!

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

【Android高级架构视频学习资源】
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可免费领取!

794)]

[外链图片转存中…(img-SM4aIB9C-1711286349794)]

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

如果你觉得这些内容对你有帮助,可以扫码领取!!!!

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

[外链图片转存中…(img-hkms7W6C-1711286349794)]

【Android高级架构视频学习资源】
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可免费领取!

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐