Home > Blockchain >  Surprising: JAVA wildcard codes cannot turn to KOTLIN
Surprising: JAVA wildcard codes cannot turn to KOTLIN

Time:05-21

It is a question about use wildcard in kotlins as the same in Java and my describes will comes with multiple codes example where I tried to make them work and it may be a long question.

Thanks for review my question and sorry for use such a tilte since I want more people to see it.

Recently, I want to design a series of classes in kotlin like

abstract class A<out T : B> {
    abstract fun doASth()
}

interface B

open class A1<out T : B1>(
    val myB: T,
) : A<T>() {
    override fun doASth() {
        myB.doBSth(this)
    }
}

open class B1(
    open val doBSth: (A1<out B1>)->Unit = {}
) : B

// A2 missed since I cannot defined proper B2

open class B2(
    override val doBSth: (A1<out B2>)->Unit = {}  // ERROR: from A1<out B2> to A1<out B1>
):B1(doBSth)

There I found Kotlin cannot do wildcard out since out B2 is clearly a child of out B1.

Next comes my work.

First try: move wildcard to class announcement and I got these codes I got first error

abstract class A<out T : B> {
    abstract fun doASth()
}

interface B

open class A1<out T : B1<out B1<*>>>(
    open val myB: T,
) : A<T>() {
    override fun doASth() {
        myB.doBSth(this)//              ERROR: require A<Nothing> but get A1<T>

        // SOLUTION: here if I use cast them no error but comes warning of unchecked cast
        // and I really hate that. Though I can mark no warning by annotaion, I am 
        // seeking for a better way rather than jump it.

        // By the way, this solution is the only one with no error but only warnings
        // which meaning I can go compile!!! :-)
    }
}

open class B1<T : B>(
    open val doBSth: (A<T>) -> Unit = {}
) : B

open class A2<out T : B2<out B2<*>>>(
    override val myB: T,
) : A1<T>(myB)

open class B2<T : B>(
    override val doBSth: (A<T>) -> Unit = {}
) : B1<T>(doBSth)

I got second error

abstract class A<out T : B> {
    abstract fun doASth()
}

interface B

open class A1<out T : B1< B1<*>>>(
    open val myB: T,
) : A<T>() {
    override fun doASth() {
        myB.doBSth(this)
    }
}

open class B1<T : B>(
    open val doBSth: (A<T>) -> Unit = {}
) : B

open class A2<out T : B2< B2<*>>>(
    override val myB: T,
) : A1<T>(myB) //                         ERROR: except B1<B1<*>> but got T

open class B2<T : B>(
    override val doBSth: (A<T>) -> Unit = {}
) : B1<T>(doBSth)

These two error tells:

  1. use nest out will bring you Nothing in type analysis which is not acceptable for me: I use wildcard to declared all types I needed, why you just bring me Nothing!!! :-(
  2. kotlin out cannot out nested type since they will be erased: This is problem of compile since I agree Apple<Apple> is safe to be used as Fruit<Fruit>.

Next is my final try, I wrote following codes in Java:

abstract class A<T extends B> {
}

interface B {
}

class A1<T extends B1> extends A<B1> {
    public A1() {
        this.self = null;
    }

    public A1(A1<? extends B1> self) {
        this.self = self;
    }

    public A1<? extends B1> self;
}

class B1 implements B {
    public B1() {
        this.aPointer = null;
    }

    public B1(A1<? extends B1> aPointer) {
        this.aPointer = aPointer;
    }

    public A1<? extends B1> aPointer;
}

class A2<T extends B2> extends A<B2> {
    public A2() {
        this.self = null;
    }

    public A2(A2<? extends B2> self) {
        this.self = self;
    }

    public A2<? extends B2> self;
}

class B2 extends B1 {
    public B2() {
        this.aPointer = null;
    }

    public B2(A2<? extends B2> aPointer) {
        this.aPointer = aPointer;
    }

    public A2<? extends B2> aPointer;
}

Then I use AndroidStudio to convert it to kotlin automatically, and I got

internal abstract class A<T : B?>
internal interface B
internal open class A1<T : B1?> : A<B1?> {
    constructor() {
        self = null
    }

    constructor(self: A1<B1>?) {
        this.self = self
    }

    var self: A1<B1>?
}

internal open class B1 : B {
    constructor() {
        aPointer = null
    }

    constructor(aPointer: A1<B1>?) {
        this.aPointer = aPointer
    }

    open var aPointer: A1<out B1>? // here I add open & out to fix convert
}

internal class A2<T : B2?> : A<B2?> {
    constructor() {
        self = null
    }

    constructor(self: A2<B2>?) {
        this.self = self
    }

    var self: A2<B2>?
}

internal class B2 : B1 {
    constructor() {
        aPointer = null
    }

    constructor(aPointer: A2<B2>?) {
        this.aPointer = aPointer
    }

    override var aPointer: A2<B2>? // here I add override to fix convert
                                   // ERROR: A2<B2> or A1<B2> is both not acceptable
}

That's all and as you can see, I cannot make my java code to kotlin and use wildcard in kotlin as I used them in Java.

Thanks again for review such a long question and any discussion about it is welcome.

CodePudding user response:

Kotlin cannot do wildcard out since out B2 is clearly a child of out B1.

Not sure what you mean by this, but as @broot already mentioned in the comments, what you're trying to achieve (at least in the first snippet) is conceptually flawed.

In particular B2 IS-NOT a B1, because an instance of B1 must have a doBSth that accepts any instance of A1<out B1>. But your definition of B2's doBSth only accepts a subset of those.

To put it in technical terms, functions are contravariant in their argument types, so the function type (A1<out B2>)->Unit is not a subtype of (A1<out B1>)->Unit, because it would require A1<out B1> to be a subtype of A1<out B2> (the opposite of what you provide).

We can demonstrate that your first definition doesn't work. Imagine you were allowed to define B2 like you did, now this would compile fine but would be broken:

val b2ViewedAsB1: B1 = B2 { arg: A<B2> -> println("I only want A<B2>") }

val aRealB1: B1 = B1 { arg: A<B1> -> println("I can handle all A<B1>") }

// must be allowed per B1's definition, yet B2 can't deal with this
b2ViewedAsB1.doBSth(A1<B1>(aRealB1))

this solution is the only one with no error but only warnings

Well you had an error, but you're desperately trying to hide it. Adding an unsafe cast and suppressing the associated warning will not solve the underlying conceptual problem. B2 cannot be a subtype of B1 in the way you define it.

use nest out will bring you Nothing in type analysis which is not acceptable for me: I use wildcard to declared all types I needed, why you just bring me Nothing!!!

Nothing is the perfect expression of the fact that no type can satisfy all the generic constraints you defined at the same time (because they contradict each other).

CodePudding user response:

What you try to do in B2 is breaking the type safety and this is why Kotlin does not allow it. You are correct T in A1 is covariant (out), so A1<B2> is a subtype of A1<B1>. However, A1<B1> is used as a parameter to lambda, so this whole type is contravariant. This is similar as you would have a function in B1 that receives A1<B1>. And because it is contravariant, you can't override it with its subtype, i.e. A1<B2>.

To show the problem with this we can use the following example. Assume B2 implements this property as:

override val doBSth: (A1<out B2>)->Unit = {
    val b2 = it.myB // we acquire B2 object from the provided A1
    // do something with B2 object
}

Now, someone creates B2 and uses it as B1:

val b2: B1 = B2()
val a1ProvidingB1: A1<B1> = ...
b2.doBSth(a1ProvidingB1) // this is allowed, but shouldn't be - broken type safety.

b2 expects that it will get B2 object from the passed a1ProvidingB1. But in fact, our a1ProvidingB1 is A1<B1>, so it will provide B1 object, not B2. So this breaks type safety.

Now, I didn't read your all examples thoroughly, but regarding your Java example and why it works. Well, it does a much different thing. First, in Java your aPointer is not a lambda that receives A1/A2, but it is a A1/A2 object itself. If you do the same with Kotlin code, it will start working, because then it will be covariant.

Second, in Java code aPointer is a field, but in Kotlin doBSth is a getter. We can't override fields, aPointer in B2 is not overriding aPointer in B1 - it is just hiding it. But doBSth in Kotlin has to override the function of the super class, which is a much different thing.

  • Related