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)
}
...
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: Attempt to invoke interface method 'java.lang.Object kotlin.Lazy.getValue()' on a null object reference
Why is NPE happening? how to solve it?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With