Skip to content

Commit 2792c22

Browse files
committed
适配 Kotlin Data Class 默认值
1 parent a232b43 commit 2792c22

33 files changed

+1175
-107
lines changed

README.md

Lines changed: 250 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ android {
3939
4040
dependencies {
4141
// Gson 解析容错:https://github.com/getActivity/GsonFactory
42-
implementation 'com.github.getActivity:GsonFactory:6.6'
42+
implementation 'com.github.getActivity:GsonFactory:8.0'
4343
// Json 解析框架:https://github.com/google/gson
4444
implementation 'com.google.code.gson:gson:2.10.1'
4545
}
@@ -150,9 +150,7 @@ GsonFactory.setJsonCallback(new JsonCallback() {
150150

151151
* 如果客户端定义的是 **int** 或者 **long** 类型,但后台返回浮点数,框架就对数值进行**直接取整**并赋值给字段
152152

153-
#### 适配 Kotlin 默认值介绍
154-
155-
* 这个问题来源大家的反馈,issue 地址:[issues/24](https://github.com/getActivity/GsonFactory/issues/24)
153+
#### 适配 Kotlin 空值介绍
156154

157155
* 如果你在 Kotlin 中定义了以下内容的 Bean 类
158156

@@ -163,17 +161,261 @@ class XxxBean {
163161
}
164162
```
165163

166-
* 大家是不是以为在后台没有返回 `age` 字段的情况下`age` 字段的值会等于 `18` ?我帮大家测试过了,不但不会等于 `18`并且还会吃系统一记 `NullPointerException`
164+
* 大家是不是以为在后台返回 `{ "age" : null }``age` 字段的值会等于 `18` ?我帮大家测试过了,不会等于 `18`会等于空
167165

168-
* 那么这到底是为什么呢?聊到这个就不得不先说一下 Gson 解析的机制,我们都知道 Gson 在解析一个 Bean 类的时候,会反射创建一个对象出来,但是大家不知道的是,Gson 会根据 Bean 类的字段名去解析 Json 串中对应的值,然后简单粗暴进行反射赋值,你没有听错,简单粗暴,如果后台没有返回这个 `age` 字段的值,那么 `age` 就会被赋值为空,但是你又在 Kotlin 中声明了 `age` 变量不为空,这个时候塞一个 `null` 值进去,触发 `NullPointerException` 也是在预料之中。
166+
* 那么这到底是为什么呢?聊到这个就不得不先说一下 Gson 解析的机制,我们都知道 Gson 在解析一个 Bean 类的时候,会反射创建一个对象出来,但是大家不知道的是,Gson 会根据 Bean 类的字段名去解析 Json 串中对应的值,然后简单粗暴进行反射赋值,你没有听错,简单粗暴,如果后台返回这个 `age` 字段的值为空,那么 `age` 就会被赋值为空,但是你又在 Kotlin 中声明了 `age` 变量不为空,外层一调用,触发 `NullPointerException` 也是在预料之中。
169167

170168
* 框架目前的处理方案是,如果后台没有返回这个字段的值,又或者返回这个值为空,则不会赋值给类的字段,因为 Gson 那样做是不合理的,会导致我在 Kotlin 上面使用 Gson 是有问题,变量不定义成可空,每次用基本数据类型还得去做判空,定义成非空,一用还会触发 `NullPointerException`,前后夹击,腹背受敌。
171169

172-
* 到这里可能会有人发出疑问了,为什么在 Java 上用没事,偏偏在 Kotlin 上用有问题,你能解释一下这个问题吗?这个问题也很简单,这是因为 Gson 在反射赋值的时候需要满足两个条件,第一个是值不为空,第二个是类型不是基本数据类型,这两个条件同时满足的情况才会进行赋值,而 Java 和 Kotlin 最大的不同是,Kotlin 没有基本数据类型,只有对象,拿短整数举例,Java 用基本数据类型表示则为 `int`,如果用对象类型表示则为 `Integer`,而 Kotlin 只能用对象类型 `Int` 表示,这下知道为什么了吧!
170+
#### 适配 Kotlin 默认值介绍
171+
172+
* 如果你在 Kotlin 中定义了以下内容的 Bean 类
173+
174+
```kotlin
175+
data class DataClassBean(val name: String?, val age: Int = 18)
176+
```
177+
178+
* 如果丢给 Gson 解析,最终会得到以下结果
179+
180+
```
181+
name = null
182+
age = 0
183+
```
184+
185+
* age 为什么不等于 18?为什么会等于 0 呢?要知道这个问题的原因,我们需要反编译看一下 DataClassBean 的源码
186+
187+
```
188+
public final class DataClassBean {
189+
private final int age;
190+
private final String name;
191+
192+
public static /* synthetic */ DataClassBean copy$default(DataClassBean bean, String str, int i, int i2, Object obj) {
193+
if ((i2 & 1) != 0) {
194+
str = bean.name;
195+
}
196+
if ((i2 & 2) != 0) {
197+
i = bean.age;
198+
}
199+
return bean.copy(str, i);
200+
}
201+
202+
public final String component1() {
203+
return this.name;
204+
}
205+
206+
public final int component2() {
207+
return this.age;
208+
}
209+
210+
public final DataClassBean copy(String str, int i) {
211+
return new DataClassBean(str, i);
212+
}
213+
214+
public boolean equals(Object obj) {
215+
if (this == obj) {
216+
return true;
217+
}
218+
if (obj instanceof DataClassBean) {
219+
DataClassBean bean = (DataClassBean) obj;
220+
return Intrinsics.areEqual(this.name, bean.name) && this.age == bean.age;
221+
}
222+
return false;
223+
}
224+
225+
public int hashCode() {
226+
String str = this.name;
227+
return ((str == null ? 0 : str.hashCode()) * 31) + this.age;
228+
}
229+
230+
public String toString() {
231+
return "DataClassBean(name=" + ((Object) this.name) + ", age=" + this.age + ')';
232+
}
233+
234+
public DataClassBean(String name, int age) {
235+
this.name = name;
236+
this.age = age;
237+
}
238+
239+
public /* synthetic */ DataClassBean(String str, int i, int i2, DefaultConstructorMarker defaultConstructorMarker) {
240+
this(str, (i2 & 2) != 0 ? 18 : i);
241+
}
242+
243+
public final int getAge() {
244+
return this.age;
245+
}
246+
247+
public final String getName() {
248+
return this.name;
249+
}
250+
}
251+
```
252+
253+
* 不知道大家发现问题没有?DataClassBean 类里面并没有空参构造函数,那 Gson 到底是怎么创建对象的呢?让我们看一段源码
254+
255+
```
256+
package com.google.gson.internal;
257+
258+
public final class ConstructorConstructor {
259+
260+
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
261+
262+
......
263+
264+
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
265+
if (defaultConstructor != null) {
266+
return defaultConstructor;
267+
}
268+
269+
......
270+
271+
if (filterResult == FilterResult.ALLOW) {
272+
// finally try unsafe
273+
return newUnsafeAllocator(rawType);
274+
} else {
275+
........
276+
}
277+
}
278+
279+
private <T> ObjectConstructor<T> newUnsafeAllocator(final Class<? super T> rawType) {
280+
281+
......
282+
283+
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
284+
if (defaultConstructor != null) {
285+
return defaultConstructor;
286+
}
287+
288+
......
289+
290+
if (useJdkUnsafe) {
291+
return new ObjectConstructor<T>() {
292+
@Override public T construct() {
293+
try {
294+
@SuppressWarnings("unchecked")
295+
T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType);
296+
return newInstance;
297+
} catch (Exception e) {
298+
throw new RuntimeException(("Unable to create instance of " + rawType + "."
299+
+ " Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args"
300+
+ " constructor may fix this problem."), e);
301+
}
302+
}
303+
};
304+
} else {
305+
......
306+
}
307+
}
308+
309+
private static <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType, FilterResult filterResult) {
310+
311+
......
312+
313+
final Constructor<? super T> constructor;
314+
try {
315+
constructor = rawType.getDeclaredConstructor();
316+
} catch (NoSuchMethodException e) {
317+
return null;
318+
}
319+
320+
......
321+
322+
return new ObjectConstructor<T>() {
323+
@Override public T construct() {
324+
try {
325+
@SuppressWarnings("unchecked") // T is the same raw type as is requested
326+
T newInstance = (T) constructor.newInstance();
327+
return newInstance;
328+
}
329+
// Note: InstantiationException should be impossible because check at start of method made sure
330+
// that class is not abstract
331+
catch (InstantiationException e) {
332+
throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
333+
+ " with no args", e);
334+
} catch (InvocationTargetException e) {
335+
// TODO: don't wrap if cause is unchecked?
336+
// TODO: JsonParseException ?
337+
throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
338+
+ " with no args", e.getCause());
339+
} catch (IllegalAccessException e) {
340+
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
341+
}
342+
}
343+
};
344+
}
345+
}
346+
```
347+
348+
```java
349+
package com.google.gson.internal;
350+
351+
public abstract class UnsafeAllocator {
352+
353+
public abstract <T> T newInstance(Class<T> c) throws Exception;
354+
355+
public static final UnsafeAllocator INSTANCE = create();
356+
357+
private static UnsafeAllocator create() {
358+
// try JVM
359+
// public class Unsafe {
360+
// public Object allocateInstance(Class<?> type);
361+
// }
362+
try {
363+
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
364+
Field f = unsafeClass.getDeclaredField("theUnsafe");
365+
f.setAccessible(true);
366+
final Object unsafe = f.get(null);
367+
final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
368+
return new UnsafeAllocator() {
369+
@Override
370+
@SuppressWarnings("unchecked")
371+
public <T> T newInstance(Class<T> c) throws Exception {
372+
assertInstantiable(c);
373+
return (T) allocateInstance.invoke(unsafe, c);
374+
}
375+
};
376+
} catch (Exception ignored) {
377+
// OK: try the next way
378+
}
379+
380+
......
381+
}
382+
}
383+
```
384+
385+
* 相信你看完就懂了,Gson 确实是反射创建无参构造函数来创建对象,但是如果没有空参构造函数的情况下,它也会通过另外的手段创建对象,借助 `sun.misc.Unsafe` 创建对象,这样会有一个问题,这样创建出来的对象它不会走任何构造函数,通过查看刚刚反编译出来的 DataClassBean 类,就知道为什么这样 Kotlin 默认值都不会生效了
386+
387+
* 框架的做法很简单,既然没有无参构造函数,那我就通过其他构造函数来创建,就拿 Kotlin 生成的 `DataClassBean(String str, int i, int i2, DefaultConstructorMarker defaultConstructorMarker)` 来创建对象
388+
389+
这个构造函数特别有意思,最后第一个参数是 DefaultConstructorMarker 类,里面啥也没有
390+
391+
```java
392+
public final class DefaultConstructorMarker {
393+
private DefaultConstructorMarker() {
394+
}
395+
}
396+
```
397+
398+
* 最后第二个参数是参数标记,标记是否使用 data class 定义的默认值
399+
400+
```java
401+
public final class DataClassBean {
402+
403+
public /* synthetic */ DataClassBean(String str, int i, int i2, DefaultConstructorMarker defaultConstructorMarker) {
404+
this(str, (i2 & 2) != 0 ? 18 : i);
405+
}
406+
407+
public DataClassBean(String name, int age) {
408+
this.name = name;
409+
this.age = age;
410+
}
411+
}
412+
```
413+
414+
* 框架的解决方案是:反射最后第一个参数类型为 DefaultConstructorMarker,然后传入空对象即可,最后第二个参数类型为 int 的构造函数,并且让最后第二个参数的位运算逻辑为 true,让它走到默认值赋值那里,这样可以选择传入 `Integer.MAX_VALUE`,这样每次使用它去 & 不大于 0 的某个值,都会等于某个值,也就是不会等于 0,这样就能保证它的运算条件一直为 true,也就是使用默认值,其他参数传值的话,如果是基本数据类型,就传入基本数据类型的默认值,如果是对象类型,则直接传入 null。这样就完成了对 Kotlin Data Class 类默认值不生效问题的处理。
173415

174416
## 常见疑问解答
175417

176-
#### Retrofit 怎么替换 Gson?
418+
#### Retrofit 怎么替换 Gson?
177419

178420
```java
179421
Retrofit retrofit = new Retrofit.Builder()

app/build.gradle

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
apply plugin: 'com.android.application'
2+
apply plugin: 'kotlin-android'
23

34
android {
45
compileSdkVersion 31
@@ -7,8 +8,8 @@ android {
78
applicationId "com.hjq.gson.factory.demo"
89
minSdkVersion 16
910
targetSdkVersion 31
10-
versionCode 66
11-
versionName "6.6"
11+
versionCode 80
12+
versionName "8.0"
1213
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
1314
}
1415

@@ -60,19 +61,19 @@ dependencies {
6061
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
6162
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
6263

64+
// Json 解析框架:https://github.com/google/gson
65+
// noinspection GradleDependency
66+
androidTestImplementation 'com.google.code.gson:gson:2.10.1'
67+
6368
// AndroidX 库:https://github.com/androidx/androidx
6469
implementation 'androidx.appcompat:appcompat:1.4.0'
6570
// Material 库:https://github.com/material-components/material-components-android
6671
implementation 'com.google.android.material:material:1.4.0'
6772

68-
// Json 解析框架:https://github.com/google/gson
69-
// noinspection GradleDependency
70-
implementation 'com.google.code.gson:gson:2.10.1'
71-
7273
// 标题栏框架:https://github.com/getActivity/TitleBar
73-
implementation 'com.github.getActivity:TitleBar:10.0'
74+
implementation 'com.github.getActivity:TitleBar:10.5'
7475

7576
// Bugly 异常捕捉:https://bugly.qq.com/docs/user-guide/instruction-manual-android/?v=20190418140644
76-
implementation 'com.tencent.bugly:crashreport:4.0.4'
77+
implementation 'com.tencent.bugly:crashreport:4.1.9'
7778
implementation 'com.tencent.bugly:nativecrashreport:3.9.2'
7879
}

0 commit comments

Comments
 (0)