与Gson一起使用Lazy Delegate时Kotlin抛出NPE

时间:2019-08-22 03:08:40

标签: android kotlin gson

问题

Foo数据类可以转换各种类型。

为了高效实现,可以使用lazy delegate来实现该属性。但是,当我尝试访问惰性属性时,我遇到了NPE。当我使用转换函数toBar时,不会发生NPE。

//data from Retrofit response via GsonConverter
data class Foo(
    @SerializedName("type") val type: String,
    @SerializedName("data") val data: JsonElement
) {
    val asBar by lazy { // it's throw NPE
        Bar.fromJson(data)
    }
    val asVar by lazy {
        Var.fromJson(data)
    }

    fun toBar() = Bar.fromJson(data)
    fun toVar() = Var.fromJson(data)
}

在RecyclerViewAdapter中的用法(扩展了PagedListAdapter)

...
override fun onBindViewHolder(
    holder: RecyclerView.ViewHolder,
    position: Int
) {
    when (holder) {
        is BarHolder -> getItem(position)?.asBar?.let(holder::bind) // NPE
        is VarHolder -> getItem(position)?.asVar?.let(holder::bind) // NPE
        //is BarHolder -> getItem(position)?.toBar()?.let(holder::bind) // it's work
        //is VarHolder -> getItem(position)?.toVar()?.let(holder::bind) // it's work

    }
}

异常

  

java.lang.NullPointerException:尝试在空对象引用上调用接口方法'java.lang.Object kotlin.Lazy.getValue()'

为什么发生NPE?该怎么解决?

2 个答案:

答案 0 :(得分:6)

问题在于Gson在反序列化JSON时实例化类的方式。 Gson在Unsafe中使用Java的UnsafeAllocator

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); // instantiation of the class
    }
}

调用allocateInstance.invoke(unsafe, c)所做的只是为类分配内存,而不调用其构造函数。实例化该类时,Gson使用反射来设置其字段。

现在回到Kotlin和lazy代表。 lazy { }构建器实际上创建了一个Lazy<T>对象。在类初始化期间即在调用其构造函数之后调用该方法。

因此,如果在不安全的分配过程中未调用构造函数,则不会创建Lazy<T>委托,该委托将保留一个null值。对委托属性的每次访问都会在委托上调用getValue(),在这种情况下会导致NullPointerException

要解决此问题,您可以使用已经定义的方法(toBar()toVar()),也可以创建计算属性asBarasVar来代替懒惰的方法:

val asBar
    get() = Bar.fromJson(data)

val asVar
    get() = Var.fromJson(data)

但是,也许更好的解决方案是将Foo类保留为数据的哑包装,并将转换逻辑移到外部。

答案 1 :(得分:0)

也许您可以使用no-arg(https://kotlinlang.org/docs/reference/compiler-plugins.html#no-arg-compiler-plugin)。该插件将生成no-arg构造函数,这将使Gson调用默认的构造函数,而不是使用不安全的分配