Exercise 1: Covariance and Contravariance
So similar to how Java uses the keywords extends and super to define covariance and contravariance, Kotlin uses the keywords out and in, respectively. In a way, this makes more sense, since covariance is usually applied when producing a type, and contravariance is applied when consuming a type. In Java, the acronym PECS for "Producer Extends Consumer Super" is usually used to remember this and keep things straight, whereas is Kotlin the association is intuitive. The out keyword is used when producing, and the in keyword is used when consuming. So in Kotlin, we can create the addDogs method like this:fun addDogs(list: MutableList<in Dog>, toAdd: List<out Dog>) { list.addAll(toAdd) }Though note that with this method, many IDEs, like Intellij, will give you a warning on the out keyword that says something like this: "Projection is redundant: the corresponding type parameter of List has the same variance." This is because Kotlin allows for declaration site variance, and since in Kotlin the List type is immutable (which in turn means that it has no methods that consume its type, only methods that produce), the List type is already marked as covariant. If you look at List's definition, you'll see something like this:
public interface List<out E> : Collection<E> { ... }So since the List type already declared the variance, we don't need to, which allows up to remove the out keyword and gives us this:
fun addDogs(list: MutableList<in Dog>, toAdd: List<Dog>) { list.addAll(toAdd) }Do note that if we had declared the toAdd parameter as a MutableList, then it would have required the out keyword. This is because it has both producer methods (like the get method) and consumer methods (like the add method). As such, it is neither covariant nor contravariant, and is instead invariant, which means that only the specific type can be used (if a list of dogs is requested, neither a list of animals, nor a list of poodles, would be acceptable; only a list of dogs).
But of course, just because a type is invariant, it doesn't mean that it can't be used in a covariant or contravariant way, it just means that it requires use site variance, which is why the in keyword is required on the MutableList in our addDogs function.
Now as a final bonus, let's convert the addDogs function to be an extension function. It can be done like this:
fun MutableList<in Dog>.addDogs(toAdd: List<Dog>) { addAll(toAdd) }And it can be used like this:
animals.addDogs(dogs) dogs.addDogs(dogs) animals.addDogs(chihuahuas) dogs.addDogs(chihuahuas) animals.addDogs(poodles) dogs.addDogs(poodles)
Exercise 2: Reified Types
Well the explanations around the last example were a little long winded, but the example with reified types should be pretty straightforward. In short, reified types can be used in ways that a normal type can't, but reified types can only be used in inline functions. So to create our cast method, we'd want to do something like this:inline fun <reified T> Observable<*>.cast() = cast(T::class.java)