@@ -39,7 +39,7 @@ android {
3939
4040dependencies {
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
179421Retrofit retrofit = new Retrofit .Builder ()
0 commit comments