Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin throw NPE when using Lazy Delegate with Gson

Problem

Foo data class can convert various type.

For efficient implementation, the property is implemented by using lazy delegate. But when I try to access the lazy property, I faced an NPE. When i use the convert function toBar, NPE does not occur.

//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)
}

Usage in RecyclerViewAdapter (extends 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

    }
}

Exception

java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object kotlin.Lazy.getValue()' on a null object reference

Why is NPE happening? how to solve it?

like image 634
Ethan Choi Avatar asked Aug 22 '19 03:08

Ethan Choi


1 Answers

The problem lies in the way Gson instantiates classes while deserializing JSON. Gson uses Java's Unsafe in the 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
    }
}

What the call allocateInstance.invoke(unsafe, c) does is simply allocate the memory for the class without invoking its constructor. When the class is instantiated, Gson uses reflection to set its fields.

Now back to Kotlin and the lazy delegate. The lazy { } builder actually creates a Lazy<T> object. The method is invoked during the class initialization, i.e. after its constructor has been called.

So, if the constructor isn't invoked during the unsafe allocation, the Lazy<T> delegate won't be created and will hold a null value. Every access to the delegated property calls getValue() on the delegate and in this case results in a NullPointerException.

To solve it you can either use the methods you've already defined (toBar() and toVar()) or create computed properties asBar and asVar instead of lazy ones:

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

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

However, maybe the better solution would be to leave the Foo class as a dumb wrapper for the data and move the converting logic outside.

like image 123
jsamol Avatar answered Sep 22 '22 07:09

jsamol