Kotlin 暗坑之一个 data class 不可空类型为 null 的惨案

  • Kotlin,data class,
  • 2020.08.16

data class 算是 kotlin 的一大特性,很多人对它非常喜欢。在设计 Kotlin 的时候,充分借鉴了 Java 语言中在写 JavaBean 中非常繁琐的特点,特意设计了 data class 这样的类型,用来定义数据类。

在项目中,我们通常会用 data class 来定义网络接口返回的响应数据体,一般写法如下:

data class User(var userId:Int,var name:String)

在使用 Gson 解析的时候,可以正常的生成 User 对象。省去了我们写 JavaBean 的大量代码。

我们碰见的问题

但是这样写有两个问题,一是这个 User 类并没有无参构造器,如果我们需要实例化一个 User 对象的时候,需要传递很多参数,而且 User 类的参数一般会特别的多。

第二个问题是一个惨痛问题,在 Gson 解析的时候,如果 json 字符串中没有的字段,会被赋值为 null ,为此会引发空检测异常。比如在解析 {} 的时候,可以得到一个 User 对象,但是 userId 和 name 都是 null ,而我们明明定义的是 String 不是 String? 啊。

为了分析这个问题,我查了一个 Gson 的代码。

Gson 是如何实例化对象的

首先 ReflectiveTypeAdapterFactory 是引用类型的 Adapter 工厂。Adapter 里包含了一个 ObjectConstructor 对象,ObjectConstructor 的 construct() 用来创建对应类型的实例。

com.google.gson.internal.bind.ReflectiveTypeAdapterFactory
@Override public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type){
    Class<? super T> raw = type.getRawType();
    if (!Object.class.isAssignableFrom(raw)) {
      return null; // it's a primitive!
    }
    ObjectConstructor<T> constructor = constructorConstructor.get(type);
    return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
}

public static final class Adapter<T> extends TypeAdapter<T> {
    @Override public T read(JsonReader in) throws IOException {
        ...
        T instance = constructor.construct();
        ...
    }
}

我们可以简化代码为两行,就是 Gson 需要找到一个合适的对象构造器,也就是 ObjectConstructor 对象,找到合适的对象构造器,就可以通过调用 construct() 方法来构造对象了。

ObjectConstructor<T> constructor = constructorConstructor.get(type);
T instance = constructor.construct();

如何找到合适的对象构造器 ObjectConstructor

我们再看 ConstructorConstructor.get() 方法是如何来获取一个合适的 ObjectConstructor 对象的。

首先前两步是从一个 instanceCreators 的 Map 里找。如果找不到,找默认构造器,newDefaultConstructor() 这个方法。如果这个类没有默认的构造器,那么从 newDefaultImplementationConstructor() 找,这个是找这个类的默认实现的构造器,这里主要是一些集合,比如 List 是一个抽象类,它的默认实现是 ArrayList 。如果最后还找不到,Gson 会通过 newUnsafeAllocator() 方法去找。

public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
final Type type = typeToken.getType();
final Class<? super T> rawType = typeToken.getRawType();

    // first try an instance creator

    @SuppressWarnings("unchecked") // types must agree
    final InstanceCreator<T> typeCreator = (InstanceCreator<T>) instanceCreators.get(type);
    if (typeCreator != null) {
      return new ObjectConstructor<T>() {
        @Override public T construct() {
          return typeCreator.createInstance(type);
        }
      };
    }

    // Next try raw type match for instance creators
    @SuppressWarnings("unchecked") // types must agree
    final InstanceCreator<T> rawTypeCreator =
        (InstanceCreator<T>) instanceCreators.get(rawType);
    if (rawTypeCreator != null) {
      return new ObjectConstructor<T>() {
        @Override public T construct() {
          return rawTypeCreator.createInstance(type);
        }
      };
    }

    ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
    if (defaultConstructor != null) {
    return defaultConstructor;
    }

    ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
    if (defaultImplementation != null) {
    return defaultImplementation;
    }

    // finally try unsafe
    return newUnsafeAllocator(type, rawType);
}

无参构造器的类的实例化

无参构造器的处理其实很简单,就是通过反射,找到个类的无参构造器,并且实例化对象。

com.google.gson.internal.ConstructorConstructor
private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) {
    try {
        final Constructor<? super T> constructor = rawType.getDeclaredConstructor();
        if (!constructor.isAccessible()) {
            constructor.setAccessible(true);
        }
        return new ObjectConstructor<T>() {
            @Override public T construct() {
            try {
                Object[] args = null;
                return (T) constructor.newInstance(args);
            }catch(...){
                ...
            }
        }
    }catch(NoSuchMethodException e){
        return null
    }
}

这里的代码,我们也可以简化一下,这样就好理解了。

final Constructor<? super T> constructor = rawType.getDeclaredConstructor();
Object[] args = null;
return (T) constructor.newInstance(args);

不安全的实例化对象

我们在上面分析了无参构造器的情况,跳过了默认实现类的构造器的情况(我们自己写的类明显不符合这种 case),最后我们再来看 newUnsafeAllocator() 这个方法。

private <T> ObjectConstructor<T> newUnsafeAllocator(
    final Type type, final Class<? super T> rawType) {
    return new ObjectConstructor<T>() {
      private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
      @SuppressWarnings("unchecked")
      @Override public T construct() {
        try {
          Object newInstance = unsafeAllocator.newInstance(rawType);
          return (T) newInstance;
        } catch (Exception e) {
          throw new RuntimeException(("Unable to invoke no-args constructor for " + type + ". "
              + "Register an InstanceCreator with Gson for this type may fix this problem."), e);
        }
      }
    };
  }

这里我们也可以简化一下:

private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
Object newInstance = unsafeAllocator.newInstance(rawType);

于是我们重点看一下 UnsafeAllocator.newInstance() 方法。代码如下

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> T newInstance(Class<T> c) throws Exception {
      assertInstantiable(c);
      return (T) allocateInstance.invoke(unsafe, c);
    }
  };
} catch (Exception ignored) {
}

我们也可以简化为:

Unsafe unsafe = sun.misc.Unsafe.getUnsafe()
Object object = unsafe.allocateInstance(clazz)

先通过 Unsafe 的静态方法 getUnsafe() 获得一个 Unsafe 对象,再调用 Unsafe 的成员方法 allocateInstance(Class<?> clazz) 获取 clazz 的对象。allocateInstance 方法是一个 native 方法。从方法名字上和设计上,可以大概猜出,它是直接在内存中开辟一个对象空间,所以通过这个方式获取的对象,成员变量都是初始值。

破案了

于是我们找到了我们定义的 data class ,明明声明的时候是不可空的,但是实际跑起来的时候空指针满天飞。

解决方案

两种解决方案,一种是加问号,一种是提供无参构造器。

data class User(var id : Int?, var name : String?)
data class User(var id :Int = 0 , var name :String = "")

其中,无参构造器的写法,要求每一个参数都需要有默认值。

相关文章

- EOF -

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处,尊重他人劳动。
转载请注明:文章转载自 Binkery 技术博客 [https://binkery.com]
本文标题: Kotlin 暗坑之一个 data class 不可空类型为 null 的惨案
本文地址: https://binkery.com/archives/2020.08.16-Kotlin-一个DataClass不可空类型为null的惨案.html

赞助商
网站统计
  • 网站运行:3318 天
  • 文章总数:738 篇
  • 网站统计