Weird Java/Kotlin interopt crash when Int is converted to int

I ran into a weird crash with LiveData in Android. After trying to remove everything unrelated, the minimal issue is something like this:

In Java:

public class Observable<T> {
    interface Observer<T> {
        void onChanged(T value);
    }

    private T value = null;

    public void register(Observer<T> observer) {
        observer.onChanged(value);
    }
}

In Kotlin:

object CrashTest {
    val objInt = Observable<Int>()
    
    inline fun <T> Observable<T>.registerInline(crossinline onChanged: (T) -> Unit) {
        register { t -> onChanged(t) }
    }

    fun startCrash() {
        objInt.registerInline { t -> println("Value $t") }
    }
}

The crash happened because inside registerInline, it tried to convert Int → int.
However, if I have a subclass of Observable, such as:

public final class SubclassObservable<T> extends Observable<T> {}

Then calling registerInline() on a SubclassObservable<Int> will work fine (it will use Interger instead of trying to convert Int → int).

I have created a repo to demonstrate this issue: GitHub - botaydotcom/KotlinIntCrash

Could someone help me to understand:

  • In the first case, why does Kotlin compiler try to convert Int → int here?
  • In the second case (with a subclass), why doesn’t it work the same way? (I suppose something to do with the extension function being defined on a Observable receiver (?))

(Context: The original code use LiveData<Int>, MutableLivedata<Int>, observe(), but the idea is exactly the same).

This should use Int?, since you’re passing a null.

Kotlin in general uses un-boxed types when it can, and the exitence of boxed types is hidden from the user. So it converts Java Integers into int when it’s told they’re not nullable like here.

I’m not too sure I understand the case with the subclass, but probalby it’s just that Kotlin can’t prove that those ints are non-nullable.

1 Like

Thank you!
I agree it’s correct that using Int? should solve most of the issues here.

However, I guess my question mainly was with why the compiler decided to auto-unbox in one case (the first one), and not the other (when we use the Subclass type). Also, if we eliminate the inline call altogether, and just use:

objInt.register { t -> println("Value $t") }

It won’t crash either.

I give it a bit more thinking, and so I guess of what happened here was:

  • In the case when there is no inline fun, and we call objInt.register { t -> println("Value $t") } directly, since this is a Java function, the type of the Observer will be Observer<Int!>. Hence, there is no auto-unboxing, and we are fine.
  • In the case when we use the inline fun and call objInt.registerInline { t -> println("Value $t") }, the issue is: the type of the argument onChanged: (T) -> Unit is inferred from the param type of the receiver Observable<T>. In this case, since objInt is Observable<Int>, the argument will be inferred to be onChanged: (Int) -> Unit (while the register call inside will still use register(Observer<Int!>)). When the callback onChanged(t) is invoked, since it’s inferred to be (Int) -> Unit, the compiler will try to convert from (Int!) → Int → int, and hence it will crash.
  • In the case when we use SubclassObservable<T>: since the extension function is on Observable<Int>, so when we call it with SubclassObservable<Int>, I guess it will first be treated implicitly as Observable<Int!> before the call, and the type of the argument of registerInline function will be (Int!) -> Unit.

Most of these are my guesses, so if there is someone with more definite knowledge to confirm it, that would be great :grin: