Trying to understand variance, but I think my book explanation is inaccurate

I’m currently studying generics and I’m trying to understand variance, “in”, and “out”. However, it seems to me that there are some inaccuracies in the book I’m studying. The book I’m reading gives the following code as an intentional example of an error that occurs:

class Barrel <T> (var item: T)

open class Loot(val value: Int)

class Fedora (val name: String, value: Int) : Loot(value)

class Coin (value: Int) : Loot(value)

fun main() {
    var fedoraBarrel : Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15))
    var lootBarrel: Barrel<Loot> = Barrel(Coin(15))

    lootBarrel = fedoraBarrel //type mismatch error here
}

Because this code hasn’t specified the variance for the generic Barrel() class, the last line-- lootBarrel = fedoraBarrel --gives a type mismatch error.

The chapter attempts to explain why by walking through what COULD happen if the compiler allowed this code and this code succeeded.

Forgive my longevity here, I’d just rather quote from the book than attempt to summarize due to my poor understanding of the concept:

If the compiler allowed you to assign the fedoraBarrel instance to the lootBarrel variable, lootBarrel would then point to fedoraBarrel, and it would be possible to interface with fedoraBarrel’s item as Loot, instead of Fedora (because of lootBarrel’s type, Barrel<Loot>).

For example, a coin is valid Loot, so it would be possible to assign a coin to lootBarrel.item (which points to fedoraBarrel).

...
fun main() {
    var fedoraBarrel : Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15))
    var lootBarrel: Barrel<Loot> = Barrel(Coin(15))

    lootBarrel = fedoraBarrel
    lootBarrel.item = Coin(15) //new line of code added here. This is also a type mismatch.
}

So far so good. I understand this. lootBarrel’s declared type is Barrel<Loot>, and changing the value of lootBarrel to equal fedoraBarrel doesn’t change it’s declared type.

  1. Quick clarification regarding the above quote though: lootBarrel.item doesn’t point to “fedoraBarrel”, right? lootBarrel points to fedoraBarrel, and lootBarrel.item would technically equal fedoraBarrel.item, correct? The difference being the type: lootBarrel.item, as with fedoraBarrel.item, point to a Fedora, whereas fedoraBarrel is a Barrel<Fedora>, and lootBarrel is a Barrel<Loot>.

Regarding the new line of code lootBarrel.item = Coin(15), I find this interesting. We went from assigning lootBarrel to a Barrel(Coin(15)), then to a fedoraBarrel, then back to coins by assigning lootBarrel.item to Coin(15). Seems meaningless.

It’s also interesting that this new line of code gives a NEW type mismatch error, stating that it expected a Fedora, and got a Coin instead, despite the fact that setting lootBarrel = fedoraBarrel technically failed because of a type mismatch.

  1. At this point I’d like to ask if I’m missing something. I’ve noticed several times here that we are accessing and making changes to the .item property of this lootBarrel object, not to the object itself. Is this pertinent to following the chapter’s logic on why the compiler doesn’t allow the lootBarrel = fedoraBarrel assignment?

Finally, the chapter continues:

Now, suppose you tried to access fedoraBarrel.item, expecting a fedora:

...
fun main() {
    var fedoraBarrel : Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15))
    var lootBarrel: Barrel<Loot> = Barrel(Coin(15))

    lootBarrel = fedoraBarrel
    lootBarrel.item = Coin(15)
    val myFedora: Fedora = fedoraBarrel.item //new line of code added here
}

The compiler would then be faced with a type mismatch - fedoraBarrel.item is not a Fedora, it is a Coin - and you would be faced with a ClassCastException. This is the problem that arises, and the reason the assignment is not allowed by the compiler.

  1. This is where I call B.S. We have made absolutely no changes to fedoraBarrel or it’s .item property. fedoraBarrel.item is still a Fedora, and was never assigned a Coin. Indeed, this new line of code does compile without errors. So, am I misunderstanding something, or is this an inaccuracy, because this is really making it hard for me to understand this concept.

Yes correct.

It’s an example, it doesn’t have to make sense, it’s a sequence of actions that gets to some “wrong” state you don’t want to be in.
Like having the compliler believing something has a type while that something has a different type.

Well yes .item is the thing whose type is T which in some version of the code further down will have some variance annotations. Variance annotations let you know something about how the class Barrel interacts with its content without knowing exactly T.

Looks like a typo, val myFedora: Fedora = lootBarrel.item is the one that would fail.

Personally I’d read https://kotlinlang.org/docs/reference/generics.html which is shorter and more concise, and definitely has higher standards.

The text is explaining what would happen, if the compiler did not raise an error at lootBarrel = fedoraBarrel: You would get a ClassCastException at runtime, when assigning val myFedora: Fedora = fedoraBarrel.item.

Ok, well I think the issue here is that there’s a tiny bit of misunderstanding about how assignment works.

This line is incredibly confusing for any beginner, and I get that. For the issue that the book is trying to show you, just please assume that the line is actually just lateinit var lootBarrel: Barrel<Loot>. That initial barrel of coin right there is totally not related to the issue.

This is probably the heart of the misunderstanding here. To put it simply, assignments in Kotlin just basically refer to the same object. So when you put fedoraBarrel as the value for lootBarrel, what basically happens is that now lootBarrel refers to the object that fedoraBarrel was referring to at the time (i.e. it’s referring to a “hidden val” that fedoraBarrel was also referring to). Whenever you assign anything to a var, you are basically changing what that var is pointing it (i.e. what it is referring to), but you are not copying that object fully. So then when you execute this line:

What is actually happening is that the object that lootBarrel is referring to (which also happens to be the same object that fedoraBarrel is referring to because the reference that fedoraBarrel holds is to the “generic looking fedora”. In other words, both lootBarrel and fedoraBarrel refer to the barrel with the “generic looking fedora”). What this line is doing is that it is changing the item that the barrel that had a fedora in it is holding to a new coin. What this means is that both fedoraBarrel and lootBarrel are now referring to that same Barrel that now has a coin in it instead of a fedora. The problem is that fedoraBarrel is expecting that the item inside of it is a Fedora, but now the item has been changed because the object that those 2 variables are referring to has changed. So now whenever you take the item out of fedoraBarrel, you’ll get a ClassCastException because the code is trying to cast a Coin to a Fedora.

That is the inaccuracy. fedoraBarrel is referring to the same barrel that lootBarrel is referring to, so fedoraBarrel’s item has, indeed, been changed to a Coin.

I think that a more minimal example might make this simpler to understand, so here it goes:

var fedoraBarrel : Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15))
(fedoraBarrel as Barrel<Loot>).item = Coin(15)
val myFedora: Fedora = fedoraBarrel.item

This code does basically the same thing that the example was doing. The only difference here is that the compiler is only going to flag that as as an UncheckedCast, but without going into too much details that basically means that you are breaking type generics and the compiler is screaming at you to not do that. The code will still compile because of historical reasons, but it will still give you a ClassCastException, the example before didn’t compile tho, because it was trying to directly use fedoraBarrel as a Barrel<Loot> without explicitly casting it.

2 Likes

I hope that this explains it, but don’t hesitate to ask any questions. Generics are a tough concept to understand, but once you get them it all just ticks into your brain.

Thank you everyone for the replies!

@kyay10
So if I may reiterate what you are saying to see if I understand:
The code here creates some object, in this case, an object of type Barrel<Fedora> (it also creates another object of type Barrel<Loot>).
These objects exists in computer memory somewhere. Then, a var is created called fedoraBarrel. A link between the fedoraBarrel var and the Barrel<Fedora> object is generated so that the object can be indirectly manipulated via the var, and this link is called an assignment.
When the lootBarrel var, which was originally assigned to the Barrel<Loot> object, is “reassigned” to fedoraBarrel, the link between lootBarrel and the object of type Barrel<Loot> is broken, and a new link is formed between lootBarrel and the same object of type Barrel<Fedora> that fedoraBarrel is assigned to/linked to.
So now both these vars are assigned to/linked to the same object.

Barrel assignment

This Barrel<Fedora> object has an item property that is assigned to another object, a Fedora. So now both lootBarrel.item and fedoraBarrel.item point to the same Fedora.

Fedora item

Now, you are saying that because fedoraBarrel and lootBarrel are assigned to the same Barrel<Fedora> object (blue arrows in the image), that fedoraBarrel.item and lootBarrel.item are forced to point to the same object (pink arrows), and changing the value of one of these affects the other. So by changing the assignment of lootBarrel.item to a Coin, we are essentially removing a Fedora from the Barrel, and putting in a Coin, in turn forcing fedoraBarrel.item to point to a Coin as well.

Coin item

Do I understand that correctly? So when we reassign lootBarrel.item to a Coin, we aren’t simply breaking the link/assignment and pointing it elsewhere (like we did with the Barrel reassignment), but we are in fact actually changing the “contents” of the Barrel, so everything that references it is reassigned as well.

1 Like

Yep, and that’s the reason why people argue a lot about the danger of mutable state. This is also the reason why when you call a function, it can actually change a var that is in one of its parameters (i.e. this will work normally:

//Changed the name property to a var for this example
class Fedora (var name: String, value: Int) : Loot(value)

fun capitalizeContainedFedorasName(fedoraContainer: Barrel<Fedora>) {
    fedoraContainer.item.name = fedoraContainer.item.name.toUpperCase()
}

fun main() {
    var fedoraBarrel : Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15))
    capitalizeContainedFedorasName(fedoraBarrel)
    println(fedoraBarrel.item.name) //prints "A GENERIC-LOOKING FEDORA"
}

(Try it here)
if you did actually copy any object whenever you passed it in to a function, this code would not work.

Another thing to keep in mind is that when you do lootBarrel = fedoraBarrel, you are assigning only the current reference that fedoraBarrel contains, so if later on you change fedoraBarret itself to be a totally new barrel, lootBarrel will still point to the old barrel that was in fedoraBarrel. Basically, lootBarrel and fedoraBarrel hold a “key” that they use to open a mailbox to access a Barrel, so if one of them opens that mailbox and opens one of the letters in it for example, then when the second one opens that mailbox, they’ll now see that one of the letters is opened. That’s because both of their keys open the same mailbox.