Generics in Kotlin

Generics in Kotlin

In software development most modern development languages have generics functionality. Generics allow us to write code that can handle varying data types without having to specify the concrete types in advance. The obvious benefits in using generics are that they make code more re-usable and thus help prevent code duplication. Generics also allow us to make code more type-safe in that we can specify compile-time constraints around generic functions and classes that restrict the type hierarchies they can be used with. Generics also allow us to avoid casting in some scenarios, something that in most languages is expensive and can slow your code down.

Like many of the Kotlin blog pieces I tend to write, we’ll look at how generics in Java and Kotlin differ. I’ll then explain why I believe how Kotlin approaches generics overall can be considered an optimisation of the same feature in Java.

The first notable benefit provided by Kotlin when using generics, in line with many other optimisations across the language, is that we can do the same thing with less code. We can define type parameters for a generic class in its definition but when it comes to instantiating that class we can instantiate and assign to a variable without the burden of having to specify the concrete type for the generic in the variable definition. Overall, this benefit is incurred due to type inference across the language in Kotlin rather than a specific feature of generics. However, I thought it worth mentioning given it makes using generics easier than it is in Java. A simple example of this would be looking at creating a list across the two languages.

Java

                        
                            public class Main {
	public static void main(String[] args) {
		List<String> myList = Arrays.asList("one", "two", "three");
	}
}
                        
                    

Kotlin

                        
                            fun main(args: Array<String>) {
	val myList = listOf("one", "two", "three")
}
                        
                    

An overly complex feature of generics in Java are its wildcard types. Wildcards exist as a means of allowing us to make the API’s we write more flexible and to work around the fact that generics in Java are invariant. Invariance in this context means that a parameterized type with one type argument is not considered a sub-type or a super-type of a parameterized type of the same type with a different type argument. An easy way to understand this is to consider the case of two List instances in Java with different type arguments. If we have a list of type String and a list of type Object they are not sub-types of each other.

Kotlin’s type system is covariant by default and this makes things much easier as in the same scenario as outlined above, the two lists are considered sub-types of each other. We don’t have to use wildcards in our type definitions to achieve this flexibility, we get it out of the box for free.

Java

                        
                            public class Main {
	public static void main(String[] args) {
		List<String> myList = new ArrayList<>();
    	List<Object> objectList = myList;
		// We can see the above line results in a compile-time error as they are not considered subtypes
    
		// To resolve the above error we can use wildcards, they resolve the error, but they are clunky    		
		List<? extends Object> workingObjectList = myList;
	}
}
                        
                    

Kotlin

                        
                            fun main(args: Array<String>) {
	val myList: List<String> = listOf("one", "two", "three")
	val objectList: List<Any> = myList
}
                        
                    

Kotlin has far more comprehensive support in its generics offering for variance in general. To appreciate this, it’s important to firstly understand what variance is in the context of generics. Generally, variance refers to the relationship between the subtyping of parameterized types and the subtyping of their type arguments. Covariance is when the subtyping relationship between the parameterized type and it’s type argument is preserved. For example, with covariance if in the general type hierarchy B is a subtype of A then a generic type like List<B> is also a subtype of List<A>. Contravariance is like covariance, the only difference being that the relationship described in the covariance example is reversed. For example, adhering to the same general type hierarchy when B is a sub-type of A, with covariance the relationship is flipped and List<A> becomes a sub-type of List<B>. Invariance we have already touched on and this is when there is no subtype relationship at all between the types, irrespective of their relationship in the general type system.

Although we can achieve the varying flavours in variance above in both Java and Kotlin, we rely on gymnastics in Java with wildcards and type bounds. This approach is not intuitive, and it requires a significant cognitive load to understand the variance approach taken. Kotlin is far more explicit and utilises declaration-site variance so that the flavour of variance adopted is explicit and staring you in the face.

To achieve covariance in Kotlin we use the ‘out’ keyword at declaration site as in the example below.

                        
                            interface Animal {
	fun speak()
}

class Cat : Animal {
	override fun speak() {
		println("Meow!")
	}
}

class Dog : Animal {
	override fun speak() {
		println("Woof!")
	}
}

class Cage<out T : Animal>(private val animal: T) {
	fun getAnimal(): T {
		return animal
	}
}

fun main() {
	val catCage: Cage<Cat> = Cage(Cat())
	val animalCage: Cage<Animal> = catCage // A covariant assignment in action
	
	animalCage.getAnimal().speak()
}
                        
                    

To achieve contravariance we use the ‘in keyword at declaration site as in the example below.

                        
                            interface Animal {
	fun speak()
}

class Cat : Animal {
	override fun speak() {
		println("Meow!")
	}
}

class Dog : Animal {
	override fun speak() {
		println("Woof!")
	}
}

class Cage<in T : Animal>(private var animal: T) {
	fun setAnimal(newAnimal: T) {
		animal = newAnimal
	}
}

fun main() {	
	val animalCage: Cage<Animal> = Cage(Dog())
	val dogCage: Cage<Dog> = animalCage
        
	// We can do this because dog is a subtype of animal
  	dogCage.setAnimal(Dog()) 
  
  	// We can't do this and get a compile-time error as a cat is not a subtype of dog
  	dogCage.setAnimal(Cat())
}
                        
                    

To achieve invariance we simply omit any variance keyword whatsoever (don’t specify in or out) as in the example below. This is analogous to how Java treats generics out of the box and unlike its variance peers in Kotlin, can be considered more as use-site variance than declaration-site variance.

                        
                            interface Animal {
	fun speak()
}

class Cat : Animal {
	override fun speak() {
		println("Meow!")
	}
}

class Dog : Animal {
	override fun speak() {
		println("Woof!")
	}
}

class Cage<T : Animal>(private var animal: T) {
	fun setAnimal(newAnimal: T) {
		animal = newAnimal
	}
}

fun main() {	
	val animalCage: Cage<Animal> = Cage(Dog())
	val dogCage: Cage<Dog> = Cage(Dog())
        
	dogCage.setAnimal(Dog()) 
    
	// We can't do this because a cat is not a sub-type of a dog
  	dogCage.setAnimal(Cat())
  
  	// We can't do this because the type parameter in the cage class is invariant and each side of the assignment has different type parameters
  	animalCage = dogCage  
}
                        
                    

Kotlin provides another nice feature for generics called star-projections. These are used when we know nothing about the type parameter but still wish to use it in a safe-way. In the example below we use a star-projection at cage instantiation so that we can pass any type of cage to the speakAnimal function as long as it’s an Animal.

                        
                            interface Animal {
	fun speak()
}

class Cat : Animal {
	override fun speak() {
		println("Meow!")
	}
}

class Cage<T : Animal>(private val animal: T) {
	fun getAnimal() : T {
		return animal
	}
}

fun speakAnimal (cage: Cage<out Animal>) {
	val animal: Animal = cage.getAnimal ()
	animal.speak()
}

fun main() {
	val catCage: Cage<Cat> = Cage(Cat())
	
	speakAnimal(catCage)
	speakAnimal(Cage(Cat()))
    
	val anyCage: Cage<*> = Cage(Cat()) // An example of a star projection
	speakAnimal(anyCage)
}
                        
                    

All in all, the feature set when considering generics in both Java and Kotlin is pretty much the same. However, using these features is significantly easier, slicker and more explicit in Kotlin. Like many other features of the language, it feels like they are an optimisation in comparison to Java and we have greater control and flexibility in how we use them.