kotlingenerics

Star projection generics has weird interaction with `Box<Box<*>>` in Kotlin


So I was playing around generics in Kotlin. Let's say I have a typical Box type:

class Box<T>

Now, I understand I can use the * projection in a function when I do not care about any methods or properties that include the T type.

So for example, if a have a takeBox1 that takes a box and does not use T at all, I could declare it as:

fun takeBox1(box: Box<*>) {}

And then later if I have a specific T Box I can provide because takeBox1 does not care what T it is:

fun <T> giveBox1(box: Box<T>) {
    takeBox1(box)
}

However, it gets weird when I have a doubly-wrapped box, i.e. a takeBox2 that takes Box<Box<*>>, it does not accept a specific Box<Box<T>>:

fun takeBox2(box: Box<Box<*>>) {}
fun <T> giveBox2(box: Box<Box<T>>) {
    takeBox2(box) // compiler error!
}

Which is quite weird, if it can accept Boxes of Boxes of anything, why can't it accept a Box of Box of something? Note that again I am not using T at all in the implementation - maybe I was just doing box.size or something.

After more testing, it does work if I declare the outer box as out, but only if the outer Box is the OutBox; so:

class OutBox<out T>

fun takeOutBox(box: OutBox<Box<*>>) {}
fun <T> giveOutBox2(box: OutBox<Box<T>>) {
    takeOutBox(box) // works
}

fun takeBoxOut(box: Box<OutBox<*>>) {}
fun <T> giveBoxOut2(box: Box<OutBox<T>>) {
    takeBoxOut(box) // does not compile
}

fun takeOutBoxOut(box: OutBox<OutBox<*>>) {}
fun <T> giveOutBoxOut2(box: OutBox<OutBox<T>>) {
    takeOutBoxOut(box) // works
}

If I make it in, it does not work at all in any cases.

I've read the documentation on Kotlin Generics, specifically about in, out, and star projection (*), and I thought I had a good grasp on it, but I cannot seem to justify/explain this particular behaviour. I also read other answers about the behaviour of * but I don't understand why a doubly-wrapped Box would behave differently than a singular Box.


Solution

  • Which is quite weird, if it can accept Boxes of Boxes of anything, why can't it accept a Box of Box of something? Note that again I am not using T at all in the implementation - maybe I was just doing box.size or something.

    The compiler doesn't care that what you are doing with the box. It only looks at the types. The types describe all the possible things you can do with a value, and in this case, the types do not ensure that "I was just doing box.size or something".

    The problem with Box<Box<*>> is that it allows you to input a Box<U> where U is any type, not necessarily the T declared in giveBox2.

    Suppose Box is

    class Box<T>(var boxed: T)
    

    If your code were allowed to compile, one could pass a Box<Box<String>> to giveBox2, and takeBox2 could do:

    box.boxed = Box(100)
    

    and after that you end up with a variable of type Box<Box<String>> that actually contains a Box<Box<Int>>.

    "But I am not actually going to do things like box.boxed = Box(100)!" you might say. Then you should tell the compiler that, by changing the parameter type of takeBox2 to something like Box<out Box<*>>.

    If you are only going to only use size on the outer Box, then you can go a step further and just change it to Box<*>.