I have read topics on Generics and Wildcards in Kotlin and also their differences compared with Java, I have tried to search online, but I couldn’t find the answer to this question nor make sure if anyone has asked it.
I’ve got the class Note
and the class FavouriteNote
, derived from the class Note
. I’ve also got an array list with type parameter Note
and an array list with type parameter FavouriteNote
. I’m trying to assign List<FavouriteNote>
to List<Note>
, which of course won’t work in Java.
public class Main { public static void main(String[] args) { List<Note> notes = new ArrayList<Note>(); List<FavouriteNote> favouriteNotes = new ArrayList<FavouriteNote>(); notes = favouriteNotes; // this won't compile } } class Note { public final String name; public Note(String name) { this.name = name; } } class FavouriteNote extends Note { public FavouriteNote(String name) { super(name); } }
In Kotlin, though, I am free to assign List<FavouriteNote>
to List<Note>
:
fun main() { var notes = emptyList<Note>() val favouriteNotes = emptyList<FavouriteNote>() notes = favouriteNotes // compiles and runs successfully } open class Note(val name: String) class FavouriteNote(name: String) : Note(name)
Why is that or what can I read to learn more about how this works in Kotlin and what is happening under the hood?
Advertisement
Answer
For the following examples, let’s introduce one more class to use:
class ShortNote extends Note { public ShortNote(String name) { super(name); } }
The biggest difference between Kotlin and Java generics is that Kotlin introduces declaration site variance for classes and interfaces. Java only has use site variance for classes and interfaces.
The variance of variable in Java can only be declared at the use site. If you don’t specifically declare a List to be covariant, it is invariant:
List<Note> notes = new ArrayList<Note>(); List<FavouriteNote> favouriteNotes = new ArrayList<FavouriteNote>(); notes = favouriteNotes; // error
This is the type system preventing you from making a mistake. Suppose the above code didn’t throw an error. Then this could happen:
List<Note> notes = new ArrayList<Note>(); List<FavouriteNote> favouriteNotes = new ArrayList<FavouriteNote>(); notes = favouriteNotes; // Invalid code, but pretend compiler allows it. notes.add(new ShortNote("Hello")); // Permitted, a List<Note> is a Note consumer and // a ShortNote is a Note. FavouriteNote aFavouriteNote = favouriteNotes.get(0); // ClassCastException!
Incidentally, Kotlin’s MutableList does not make use of declaration site variance, so you would have the exact same restriction:
var notes: MutableList<Note> = ArrayList<Note>() var favouriteNotes: MutableList<FavoriteNote> = ArrayList<FavoriteNote>() notes = favouriteNotes // error
However, you can use use-site variance to make the cast possible. We can declare the list to be covariant at the use site:
//Java List<? extends Note> notes = new ArrayList<Note>(); List<FavouriteNote> favouriteNotes = new ArrayList<FavouriteNote>(); notes = favouriteNotes; // OK
//Kotlin var notes: MutableList<out Note> = ArrayList<Note>() var favouriteNotes: MutableList<out FavoriteNote> = ArrayList<FavoriteNote>() notes = favouriteNotes// OK
You are protected from the ClassCastException situation above because the compiler prevents you from adding items to a covariant list.
Now getting to declaration site variance. Kotlin declares the read-only List interface’s type to be <out T>
right in the definition of the interface. This means all Lists are automatically assumed to be covariant, even if you don’t bother to declare it to be covariant at the use site.
If you declare variance at a class or interface’s declaration site, the compiler will restrict you from making functions or properties that violate that variance. The read-only List interface doesn’t have any add
functions, so it is fine for it to be a T
producer and not consumer.