当Gson遇上kotlin data class,会发生一些很有意思的现象:
现象1: 非空类型失效
data class TestData(
val a: String,
val b: String
)
val data = Gson().fromJson("{}", TestData::class.java)
println("a:${data.a}, b:${data.b}") //输出: a:null, b:null
现象2: 构造函数不会被调用
data class TestData(
val a: String,
val b: String
) {
init {
println("TestData init!!!") // 这一行代码不会执行到
}
}
val data = Gson().fromJson("{}", TestData::class.java)
现象3: 默认值失效
data class TestData(
val a: String,
val b: String = "bbb"
)
val data = Gson().fromJson("{\"a\":\"aaa\"}", TestData::class.java)
println("$data") //输出: TestData(a=aaa, b=null)
现象4: 当全部成员都有默认值的时候默认值和构造函数又生效了
data class TestData(
val a: String = "",
val b: String = "bbb"
) {
init {
println("TestData init!!!") // 这一行代码能执行到
}
}
val data = Gson().fromJson("{\"a\":\"aaa\"}", TestData::class.java)
println("$data") //输出: TestData(a=aaa, b=bbb)
Gson解析流程
要理解上面的现象我们先要了解Gson是怎样工作的。
Gson解析json分两步,创建对象实例和给成员变量赋值.
创建对象实例是通过ConstructorConstructor.get(TypeToken
public ObjectConstructor get(TypeToken typeToken) {
final Type type = typeToken.getType();
final Class super T> rawType = typeToken.getRawType();
// 从instanceCreators中查找,我们可以用GsonBuilder.registerTypeAdapter指定某种类型的构造器,默认情况下instanceCreators是空的
final InstanceCreator typeCreator = (InstanceCreator) instanceCreators.get(type);
if (typeCreator != null) {
return new ObjectConstructor() {
@Override public T construct() {
return typeCreator.createInstance(type);
}
};
}
// 这里还是在instanceCreators里查找,只不过用rawType当key
final InstanceCreator rawTypeCreator =
(InstanceCreator) instanceCreators.get(rawType);
if (rawTypeCreator != null) {
return new ObjectConstructor() {
@Override public T construct() {
return rawTypeCreator.createInstance(type);
}
};
}
// 查找一些特殊集合如EnumSet、EnumMap的构造器
ObjectConstructor specialConstructor = newSpecialCollectionConstructor(type, rawType);
if (specialConstructor != null) {
return specialConstructor;
}
// 通过rawType.getDeclaredConstructor()反射获取类的无参构造函数
FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, rawType);
ObjectConstructor defaultConstructor = newDefaultConstructor(rawType, filterResult);
if (defaultConstructor != null) {
return defaultConstructor;
}
// 查找普通的Collection或者Map,如ArrayList、HashMap等的构造器
ObjectConstructor defaultImplementation = newDefaultImplementationConstructor(type, rawType);
if (defaultImplementation != null) {
return defaultImplementation;
}
// 判断类型是否可以实例化,例如接口和抽象类就不能实例化
final String exceptionMessage = checkInstantiable(rawType);
if (exceptionMessage != null) {
return new ObjectConstructor() {
@Override public T construct() {
throw new JsonIOException(exceptionMessage);
}
};
}
// 最后使用sun.misc.Unsafe去兜底创建实例
if (filterResult == FilterResult.ALLOW) {
return newUnsafeAllocator(rawType);
} else {
final String message = "Unable to create instance of " + rawType + "; ReflectionAccessFilter "
+ "does not permit using reflection or Unsafe. Register an InstanceCreator or a TypeAdapter "
+ "for this type or adjust the access filter to allow using reflection.";
return new ObjectConstructor() {
@Override public T construct() {
throw new JsonIOException(message);
}
};
}
}
获取到对象的构造器,之后就能用它去创建对象实例,然后遍历json字段查找对象是否有对应的成员变量,如果有就通过反射设置进去:
@Override
public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
// 通过ConstructorConstructor.get(TypeToken\ typeToken)查询的构造器创建实例对象
A accumulator = createAccumulator();
try {
in.beginObject();
// 遍历json
while (in.hasNext()) {
String name = in.nextName();
// 从对象的成员变量列表查询是否有该字段
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
// 对象没有该成员变量则跳过
in.skipValue();
} else {
// 对象有该成员变量则读取json的值,通过反射设置给对象
readField(accumulator, in, field);
}
}
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
in.endObject();
return finalize(accumulator);
}
非空类型失效和构造函数不会被调用的原理
了解了Gson的解析流程之后我们再来看看问题1的data class对应的java代码:
// kotlin代码
data class TestData(
val a: String,
val b: String
)
// java对应的类
public final class TestData {
private final String a;
private final String b;
...
public final String getA() {
return this.a;
}
public final String getB() {
return this.b;
}
...
public TestData(@NotNull String a, @NotNull String b) {
Intrinsics.checkNotNullParameter(a, "a");
Intrinsics.checkNotNullParameter(b, "b");
super();
this.a = a;
this.b = b;
}
...
@NotNull
public String toString() {
return "TestData(a=" + this.a + ", b=" + this.b + ")";
}
...
}
可以看到只有在构造函数里面做了判空,但是它并没有无参构造函数所以gson是通过Unsafe去兜底创建TestData实例的。Unsafe创建类的实例并不会调用到构造函数,所以就绕过类判空的步骤。
同理也能解释现象2构造函数不会被调用的问题。
Unsafe
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。 -- Java魔法类:Unsafe应用解析
我们可以通过下面的代码创建TestData实例而不调用TestData的构造函数:
val unsafeClass = Class.forName("sun.misc.Unsafe")
val theUnsafe = unsafeClass.getDeclaredField("theUnsafe")
theUnsafe.isAccessible = true
val unsafe = theUnsafe.get(null)
val allocateInstance = unsafeClass.getMethod("allocateInstance", Class::class.java)
val testData = allocateInstance.invoke(unsafe, TestData::class.java) as TestData
data class 默认值的原理
接着我们继续来看现象3默认值失效的问题,这里会牵扯到data class默认值的原理,我们来看看对应的java代码:
// kotlin代码
data class TestData(
val a: String,
val b: String = "bbb"
)
// java对应的类
public final class TestData {
private final String a;
private final String b;
...
public TestData(@NotNull String a, @NotNull String b) {
Intrinsics.checkNotNullParameter(a, "a");
Intrinsics.checkNotNullParameter(b, "b");
super();
this.a = a;
this.b = b;
}
public TestData(String var1, String var2, int var3, DefaultConstructorMarker var4) {
if ((var3 & 2) != 0) {
var2 = "bbb";
}
this(var1, var2);
}
...
}
可以看到,kotlin的默认参数并不是通过重载实现的,而是新增一个构造函数,用一个int的各个bit位来表示前面的参数是否需要设置成默认值。
// 例如下面这行kotlin代码:
val testData = TestData("aaa")
// 对应的java代码是这样的:
TestData testData = new TestData("aaa", (String)null, 2, (DefaultConstructorMarker)null);
这样做的好处在于只需要新建一个构造函数。用下面这种java传统的函数重载来做,如果有很多的默认值的话需要创建很多的构造函数:
public final class TestData {
...
public TestData(@NotNull String a, @NotNull String b) {
...
}
public TestData(String var1) {
this(var1, "bbb");
}
...
}
除了这个之外它也能比较方便的去支持任意位置的默认值.
到这里我们也能理解现象3默认值失效的原因了,和前面的两个现象一样是因为没有调用到TestData的构造函数,所以就没有赋默认值.
DefaultConstructorMarker
另外在生成的构造函数里我们还看到了一个DefaultConstructorMarker参数:
public TestData(String var1, String var2, int var3, DefaultConstructorMarker var4)
这个参数会在kotlin自动生成的构造函数里面出现,目的是为了防止和我们自己定义的构造函数碰撞:
// kotlin代码
data class TestData(
val a: String,
val b: String = "bbb"
) {
constructor(a: String, b: String, i: Int) : this(a, b) {
}
}
// 对应的java代码
public final class TestData {
private final String a;
private final String b;
public TestData(@NotNull String a, @NotNull String b) {
...
}
// 假设没有DefaultConstructorMarker参数,下面的两个构造函数就会撞车了
public TestData(String var1, String var2, int var3, DefaultConstructorMarker var4) {
...
}
public TestData(@NotNull String a, @NotNull String b, int i) {
Intrinsics.checkNotNullParameter(a, "a");
Intrinsics.checkNotNullParameter(b, "b");
this(a, b);
}
...
}
全部成员都有默认值的情况
最后我们来分析下现象4当全部成员都有默认值的情况:
// kotlin代码
data class TestData(
val a: String = "",
val b: String = "bbb"
) {
init {
println("TestData init!!!")
}
}
// 对应的java代码
public final class TestData {
private final String a;
private final String b;
...
public TestData(@NotNull String a, @NotNull String b) {
Intrinsics.checkNotNullParameter(a, "a");
Intrinsics.checkNotNullParameter(b, "b");
super();
this.a = a;
this.b = b;
String var3 = "TestData init!!!";
boolean var4 = false;
System.out.println(var3);
}
public TestData(String var1, String var2, int var3, DefaultConstructorMarker var4) {
if ((var3 & 1) != 0) {
var1 = "";
}
if ((var3 & 2) != 0) {
var2 = "bbb";
}
this(var1, var2);
}
public TestData() {
this((String)null, (String)null, 3, (DefaultConstructorMarker)null);
}
...
}
可以看到当所有成员都有默认值的时候,会生成无参构造函数,这样的话Gson就会调用无参构造函数去创建实例。
解决思路
了解完原理我们来看看怎么解决默认值无效的问题,下面有一些思路:
- 当需要使用默认值的时候全部成员变量都加上默认值
- 使用代码生成的方式创建InstanceCreator并注册到gson,在里面创建实例并预先填好默认值
- 改用对kotlin支持更好的kotlinx.serialization或者moshi
kotlinx.serialization原理
kotlinx.serialization的原理在于@Serializable注解的data class对应的java代码会多出一个$serializer类,它会记录所有构造参数对应的json key,然后在解析出来的json里面读取出value去传入构造函数:
// kotlin代码
@Serializable
data class TestData(
val a: String,
val b: String = "aaa"
)
// java对应的类
public final class TestData {
private final String a;
private final String b;
...
public static final class $serializer implements GeneratedSerializer {
...
private static final SerialDescriptor $$serialDesc;
static {
TestData.$serializer var0 = new TestData.$serializer();
INSTANCE = var0;
PluginGeneratedSerialDescriptor var1 = new PluginGeneratedSerialDescriptor("me.linjw.demo.debugtool.TestData", (GeneratedSerializer)INSTANCE, 2);
var1.addElement("a", false);
var1.addElement("b", true);
$$serialDesc = var1;
}
...
public TestData deserialize(@NotNull Decoder decoder) {
Intrinsics.checkNotNullParameter(decoder, "decoder");
SerialDescriptor var2 = $$serialDesc;
int var4 = 0;
String var5 = null;
String var6 = null;
Decoder decoder = decoder.beginStructure(var2);
if (decoder.decodeSequentially()) {
var5 = decoder.decodeStringElement(var2, 0);
var6 = decoder.decodeStringElement(var2, 1);
var4 = Integer.MAX_VALUE;
} else {
label19:
while(true) {
int var3 = decoder.decodeElementIndex(var2);
switch(var3) {
case -1:
break label19;
case 0:
var5 = decoder.decodeStringElement(var2, 0);
var4 |= 1;
break;
case 1:
var6 = decoder.decodeStringElement(var2, 1);
var4 |= 2;
break;
default:
throw (Throwable)(new UnknownFieldException(var3));
}
}
}
decoder.endStructure(var2);
return new TestData(var4, var5, var6, (SerializationConstructorMarker)null);
}
...
}
}
毕竟是kotlin官方的库,能够对生成的字节码任意的做改动去实现。
moshi原理
moshi则比较委婉,通过kotlin的反射机制遍历构造函数的参数,判断有没有可选参数,如果有的话就走callBy方法通过key-value map的方式传入参数,如果没有可选参数则通过vararg可变参数列表的方式顺序传入参数:
// Confirm all parameters are present, optional, or nullable.
var isFullInitialized = allBindings.size == constructorSize
for (i in 0 until constructorSize) {
if (values[i] === ABSENT_VALUE) {
when {
constructor.parameters[i].isOptional -> isFullInitialized = false
constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
else -> throw missingProperty(
constructor.parameters[i].name,
allBindings[i]?.jsonName,
reader
)
}
}
}
// Call the constructor using a Map so that absent optionals get defaults.
val result = if (isFullInitialized) {
constructor.call(*values)
} else {
constructor.callBy(IndexedParameterMap(constructor.parameters, values))
}