【问题标题】:Kotlin throw NPE when using Lazy Delegate with GsonKotlin 在将 Lazy Delegate 与 Gson 一起使用时抛出 NPE
【发布时间】:2019-08-22 03:08:40
【问题描述】:

问题

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?如何解决?

【问题讨论】:

  • 请分享更多细节。 getItem() 有什么作用?是否涉及任何序列化?如果 Foo 类是由某个 Json 反序列化库实例化的,而不是由调用构造函数的代码实例化的,那么这可能会解释问题。想想这个类在 Java 中的样子。
  • @MateuszStefek 感谢您的回复。我附上了更多详细信息。

标签: android kotlin gson


【解决方案1】:

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

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&lt;T&gt; 对象。该方法在类初始化期间被调用,即在其构造函数被调用之后。

因此,如果在不安全分配期间未调用构造函数,则不会创建 Lazy&lt;T&gt; 委托并将持有 null 值。对委托属性的每次访问都会在委托上调用getValue(),在这种情况下会产生NullPointerException

要解决它,您可以使用已经定义的方法(toBar()toVar())或创建计算属性 asBarasVar 而不是懒惰的:

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

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

但是,也许更好的解决方案是将Foo 类作为数据的愚蠢包装器,并将转换逻辑移到外面。

【讨论】:

  • 值得注意的是,此解决方案失去了“惰性”初始化行为。使用上面的代码,每次调用 asBarasVal 时都会反序列化 Bar 和 Var。如果走这条路线,绝对应该牢记。
【解决方案2】:

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

【讨论】:

    猜你喜欢
    • 2021-05-12
    • 2020-11-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-27
    • 1970-01-01
    • 2014-03-15
    • 1970-01-01
    相关资源
    最近更新 更多