NullPointerException when implementing Java interface from Kotlin
Asked Answered
P

3

9

I'm trying to implement BiFunction interface from RxJava in Kotlin and I'm getting a NullPointerException.

This is the Java interface that I'm implementing in Kotlin. It's from RxJava 2.

package io.reactivex.functions;

import io.reactivex.annotations.NonNull;

/**
 * A functional interface (callback) that computes a value based on multiple input values.
 * @param <T1> the first value type
 * @param <T2> the second value type
 * @param <R> the result type
 */
public interface BiFunction<T1, T2, R> {

    /**
     * Calculate a value based on the input values.
     * @param t1 the first value
     * @param t2 the second value
     * @return the result value
     * @throws Exception on error
     */
    @NonNull
    R apply(@NonNull T1 t1, @NonNull T2 t2) throws Exception;
}

This is my implementation

class MonitoringStateReducer: BiFunction<MonitoringViewState, MonitoringResult, 
    MonitoringViewState> {
    override fun apply(
        previousState: MonitoringViewState,
        result: MonitoringResult
    ): MonitoringViewState {
        when (result) {
           //Returns a non-null new state
        }
    }
}

And then, in the ViewModel, I try to use it, but it throws a NullPointerException.

2019-08-22 09:57:41.049 6925-6925/com.name.app E/AndroidRuntime: FATAL EXCEPTION: main Process: com.name.app, PID: 6925 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.name.app/com.name.app.features.monitoring.presentation.MonitoringActivity}: java.lang.NullPointerException: accumulator is null at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2907) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2986) at android.app.ActivityThread.-wrap11(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1641) at android.os.Handler.dispatchMessage(Handler.java:105) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6694) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769) Caused by: java.lang.NullPointerException: accumulator is null at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39) at io.reactivex.Observable.scanWith(Observable.java:11537) at io.reactivex.Observable.scan(Observable.java:11502) at com.name.app.features.monitoring.presentation.MonitoringViewModel.compose(MonitoringViewModel.kt:47) at com.name.app.features.monitoring.presentation.MonitoringViewModel.(MonitoringViewModel.kt:18) at com.name.app.features.monitoring.presentation.MonitoringViewModel_Factory.get(MonitoringViewModel_Factory.java:25) at com.name.app.features.monitoring.presentation.MonitoringViewModel_Factory.get(MonitoringViewModel_Factory.java:8) at dagger.internal.DoubleCheck.get(DoubleCheck.java:47) at com.name.app.di.viewmodel.ViewModelFactory.create(ViewModelFactory.kt:12) at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:164) at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:130) at com.name.app.features.monitoring.presentation.MonitoringActivity$viewModel$2.invoke(MonitoringActivity.kt:46) at com.name.app.features.monitoring.presentation.MonitoringActivity$viewModel$2.invoke(MonitoringActivity.kt:26) at kotlin.UnsafeLazyImpl.getValue(Lazy.kt:81) at com.name.app.features.monitoring.presentation.MonitoringActivity.getViewModel(Unknown Source:7) at com.name.app.features.monitoring.presentation.MonitoringActivity.bind(MonitoringActivity.kt:85) at com.name.app.features.monitoring.presentation.MonitoringActivity.onCreate(MonitoringActivity.kt:119) at android.app.Activity.performCreate(Activity.java:6984) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1235) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2860) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2986)  at android.app.ActivityThread.-wrap11(Unknown Source:0)  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1641)  at android.os.Handler.dispatchMessage(Handler.java:105)  at android.os.Looper.loop(Looper.java:164)  at android.app.ActivityThread.main(ActivityThread.java:6694)  at java.lang.reflect.Method.invoke(Native Method)  at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769)

class MonitoringViewModel @Inject constructor(
    private val processor: MonitoringProcessor
) : BaseViewModel<MonitoringIntention, MonitoringViewState>() {
    //Properties that are not relevant for the question

    private val reducer: MonitoringStateReducer = MonitoringStateReducer()

    private fun compose(): Observable<MonitoringViewState> {
        return intentsSubject.compose(intentFilter)
            .map(actionFromIntent)
            .compose(processor)
            .scan(MonitoringViewState.init(), reducer) //Exception is here
            .distinctUntilChanged()
            .replay(1)
            .autoConnect(0)
    }

    override fun state(): Observable<MonitoringViewState> = compose()

    //Functions that are not relevant for the question
}

This code doesn´t work either.

private val reducer by lazy(LazyThreadSafetyMode.NONE) {
    MonitoringStateReducer()
}

However, if I replace the reducer with this code, it works.

private val reducer: BiFunction<MonitoringViewState, MonitoringResult, MonitoringViewState>
    get() = MonitoringStateReducer()

Tested on Kotlin 1.3.40 and 1.3.50.

Pondweed answered 21/8, 2019 at 16:45 Comment(2)
check the stack trace, it could be that state() is called and subscribed to somewhere from parent constructor.Pemba
Post the full MonitoringStateReducer.apply method. If it's too long, then post at least a few branches of the when block. The problem likely has to do with something in there.Yodle
U
11

The problem comes from the Kotlin class initialization order. The crash is due to the fact that BaseViewModel constructor is calling the state() method that is overridden in the child MonitoringViewModel class. As a result, when the reducer is accessed, it hasn't initialized yet. During construction of a new instance of a derived class, the base class initialization is done as the first step and it happens before the initialization logic of the derived class is run. Take a look at this article, which describes a pretty similar problem. Derived class initialization order Kotlin's documentation section should be useful too.

Unstriped answered 24/8, 2019 at 5:51 Comment(1)
Related: What's wrong with overridable method calls in constructorAtrabilious
F
4

@Valeriy Katkov's answer is correct, please accept his answer.

Here is a code simulating the same scenario:

fun main() {
    MonitoringViewModel()
}

open abstract class BaseViewModel {

    init {
        state()
    }

    abstract fun state()
}

class MonitoringViewModel: BaseViewModel() {

    private val reducer1: MonitoringStateReducer = MonitoringStateReducer()
    private val reducer2: BiFunction<MonitoringViewState, MonitoringResult, MonitoringViewState>
        get() = MonitoringStateReducer()

    override fun state() {
        Observable.just(MonitoringResult(3))
                .scan(MonitoringViewState(0), reducer1)
                .subscribe { state -> println(state.value) }
    }

}

data class MonitoringViewState(val value: Int)

data class MonitoringResult(val value: Int)

class MonitoringStateReducer : BiFunction<MonitoringViewState, MonitoringResult,
        MonitoringViewState> {
    override fun apply(
            previousState: MonitoringViewState,
            result: MonitoringResult
    ): MonitoringViewState {
        return MonitoringViewState(previousState.value + result.value)
    }
}

If you run this code with the scan operator using reducer1 you will see the exception:

Exception in thread "main" java.lang.NullPointerException: accumulator is null

If you run the code with scan operator using reducer2, it succeeds.

Why does it crash when using reduce1?

From @Valeriy Katkov's answer:

The crash is due to the fact that BaseViewModel constructor is calling the state() method that is overridden in the child MonitoringViewModel class. As a result, when the reducer is accessed, it hasn't initialized yet. During construction of a new instance of a derived class, the base class initialization is done as the first step and it happens before the initialization logic of the derived class is run.

Fluorine answered 26/8, 2019 at 12:13 Comment(0)
K
1

I suspect that this part of the stacktrace holds the answer:

at com.name.app.features.monitoring.presentation.MonitoringViewModel_Factory.get(MonitoringViewModel_Factory.java:8)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)
at com.name.app.di.viewmodel.ViewModelFactory.create(ViewModelFactory.kt:12)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:164)

What I'm taking from this is that you're using Dagger to create instances of your MonitoringViewModel class. I'm not sure that Dagger does what I'm about to describe, but I know other libraries (like Gson) do, and it fits the pattern...

It is possible to create instances of objects without actually invoking constructors if you use the Unsafe class. See the "Avoid Initialization" section of this article: http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/

If that's happening here, then your private val reducer = MonitoringStateReducer() won't ever actually be executed, because that's part of the class initialization. Therefore, reducer is never initialized and is null when you try to use it in compose().

I'm not sure why that would make it not work when using the by lazy delegate, but it does make sense for why it works when you provide a custom get(): it is no longer part of class initialization, and is instead evaluated on demand.

Try creating instances of this class without using Dagger and see if your "normal" initialization works.

Kisumu answered 23/8, 2019 at 18:6 Comment(3)
Was curious how you went about this. Sounds like a very good possibility. I naively searched dagger's repo for Unsafe and found no mentions to it. The reason I ask is also because being a framework that generates the code at compile tine and requires all the info about classes, if find it strange that they would require unsafe.Piderit
Maybe it isn't explicitly Unsafe, and there's some other way (that I'm not aware of) to create instances without normal initialization. As for how I went about it, really it was just that I had read that blog post somewhat recently so the idea of creating instances without initialization was fresh in my mind.Kisumu
Upvoting as this is a good answer, but for the scenario of this question, the problem is correctly described by @Valeriy KatkovFluorine

© 2022 - 2024 — McMap. All rights reserved.