This material was written by Aasmund Eldhuset; it is owned by Khan Academy and is licensed for use under CC BY-NC-SA 3.0 US. Please note that this is not a part of Khan Academy's official product offering.
One might think that static typing would make it very impractical to make collection classes or any other class that needs to contain members whose types vary with each usage. Generics to the rescue: they allow you to specify a "placeholder" type in a class or function that must be filled in whenever the class or function is used. For example, a node in a linked list needs to contain data of some type that is not known when we write the class, so we introduce a generic type parameter T
(they are conventionally given single-letter names):
class TreeNode<T>(val value: T?, val next: TreeNode<T>? = null)
Whenever you create an instance of this class, you must specify an actual type in place of T
, unless the compiler can infer it from the constructor parameters: TreeNode("foo")
or TreeNode<String>(null)
. Every use of this instance will act as if it were an instance of a class that looks like this:
class TreeNode<String>(val value: String?, val next: TreeNode<String>? = null)
Member properties and member functions inside a generic class may for the most part use the class' generic type parameters as if they were ordinary types, without having to redeclare them. It is also possible to make functions that take more generic parameters than the class does, and to make generic functions inside nongeneric classes, and to make generic top-level functions (which is what we'll do in the next example). Note the different placement of the generic type parameter in generic function declarations:
fun <T> makeLinkedList(vararg elements: T): TreeNode<T>? {
var node: TreeNode<T>? = null
for (element in elements.reversed()) {
node = TreeNode(element, node)
}
return node
}
You can restrict the types that can be used for a generic type parameter, by specifying that it must be an instance of a specific type or of a subclass thereof. If you've got a class or interface called Vehicle
, you can do:
class TreeNode<T : Vehicle>
Now, you may not create a TreeNode
of a type that is not a subclass/implementor of Vehicle
. Inside the class, whenever you've got a value of type T
, you may access all the public members of Vehicle
on it.
If you want to impose additional constraints, you must use a separate where
clause, in which case the type parameter must be a subclass of the given class (if you specify a class, and you can specify at most one) and implement all the given interfaces. You may then access all the public members of all the given types whenever you've got a value of type T
:
class TreeNode<T> where T : Vehicle, T : HasWheels
Pop quiz: if Apple
is a subtype of Fruit
, and Bowl
is a generic container class, is Bowl<Apple>
a subtype of Bowl<Fruit>
? The answer is - perhaps surprisingly - no. The reason is that if it were a subtype, we would be able to break the type system like this:
fun add(bowl: Bowl<Fruit>, fruit: Fruit) = bowl.add(fruit)
val bowl = Bowl<Apple>()
add(bowl, Pear()) // Doesn't actually compile!
val apple = bowl.get() // Boom!
If the second-to-last line compiled, it would allow us to put a pear into what is ostensibly a bowl of only apples, and your code would explode when it tried to extract the "apple" from the bowl. However, it's frequently useful to be able to let the type hierarchy of a generic type parameter "flow" to the generic class. As we saw above, though, some care must be taken - the solution is to restrict the direction in which you can move data in and out of the generic object.
If you have an instance of Generic<Subtype>
, and you want to refer to it as a Generic<Supertype>
, you can safely get instances of the generic type parameter from it - these will truly be instances of Subtype
(because they come from an instance of Generic<Subtype>
), but they will appear to you as instances of Supertype
(because you've told the compiler that you have a Generic<Supertype>
). This is safe; it is called covariance, and Kotlin lets you do declaration-site covariance by putting out
in front of the generic type parameter. If you do, you may only use that type parameter as a return type, not as a parameter type. Here is the simplest useful covariant interface:
interface Producer<out T> {
fun get(): T
}
It is safe to treat a Producer<Apple>
as if it were a Producer<Fruit>
- the only thing it will ever produce is Apple
instances, but that's okay, because an Apple
is a Fruit
.
Conversely, if you have an instance of Generic<Supertype>
, and you want to refer to it as a Generic<Subtype>
(which you can't do with nongeneric classes), you can safely give instances of the generic type parameter to it - the compiler will require those instances to be of the type Subtype
, which will be acceptable to the real instance because it can handle any Supertype
. This is called contravariance, and Kotlin lets you do declaration-site contravariance by putting in
in front of the generic type parameter. If you do, you may only use that type parameter as a parameter type, not as a return type. Here is the simplest useful contravariant interface:
interface Consumer<in T> {
fun add(item: T)
}
It is safe to treat a Consumer<Fruit>
as a Consumer<Apple>
- you are then restricted to only adding Apple
instances to it, but that's okay, because it is capable of receiving any Fruit
.
With these two interfaces, we can make a more versatile fruit bowl. The bowl itself needs to both produce and consume its generic type, so it can neither be covariant nor contravariant, but it can implement our covariant and contravariant interfaces:
class Bowl<T> : Producer<T>, Consumer<T> {
private val items = mutableListOf<T>()
override fun get(): T = items.removeAt(items.size - 1)
override fun add(item: T) { items.add(item) }
}
Now, you can treat a bowl of T
as a producer of any superclass of T
, and as a consumer of any subclass of T
:
val p: Producer<Fruit> = Bowl<Apple>()
val c: Consumer<Apple> = Bowl<Fruit>()
If the parameters or return types of the members of a variant type are themselves variant, it gets a bit complicated. Function types in parameters and return types make it even more challenging. If you're wondering whether it's safe to use a variant type parameter T
in a particular position, ask yourself:
- If
T
is covariant: is it okay that the user of my class thinks thatT
in this position is aSupertype
, while in reality, it's aSubtype
? - If
T
is contravariant: is it okay that the user of my class thinks thatT
in this position is aSubtype
, while in reality, it's aSupertype
?
These considerations lead to the following rules. A covariant type parameter T
(which the user of an object might think is Fruit
, while the object in reality is tied to Apple
) may be used as:
-
val v: T
A read-only property type (the user is expecting a
Fruit
, and gets anApple
) -
val p: Producer<T>
The covariant type parameter of a read-only property type (the user is expecting a producer of
Fruit
, and gets a producer ofApple
) -
fun f(): T
A return type (as we've already seen)
-
fun f(): Producer<T>
The covariant type parameter of a return type (the user is expecting that the returned value will produce a
Fruit
, so it's okay if it really produces anApple
) -
fun f(consumer: Consumer<T>)
The contravariant type parameter of a parameter type (the user is passing a consumer that can handle any
Fruit
, and it will be given anApple
) -
fun f(function: (T) -> Unit)
The parameter type of a function-typed parameter (the user is passing a function that can handle any
Fruit
, and it will be given anApple
) -
fun f(function: (Producer<T>) -> Unit)
The covariant type parameter of the parameter type of a function-typed parameter (the user is passing a function that can handle any
Fruit
producer, and it will be given anApple
producer) -
fun f(function: () -> Consumer<T>)
The contravariant type parameter of the return type of a function-typed parameter (the user is passing a function that will return a consumer of any
Fruit
, and the returned consumer will be givenApple
instances) -
fun f(): () -> T
The return type of a function-typed return type (the user expects the returned function to return
Fruit
, so it's okay if it really returnsApple
) -
fun f(): () -> Producer<T>
The covariant type parameter of the return type of a function-typed return type (the user expects the returned function to return something that produces
Fruit
, so it's okay if it really producesApple
) -
fun f(): (Consumer<T>) -> Unit
The contravariant type parameter of a parameter of a function-typed return type (the user will call the returned function with something that can consume any
Fruit
, so it's okay to return a function that expects to receive something that can handleApple
)
A contravariant type parameter may be used in the converse situations. It is left as an exercise to the reader to figure out the justifications for why these member signatures are legal:
val c: Consumer<T>
fun f(item: T)
fun f(): Consumer<T>
fun f(producer: Producer<T>)
fun f(function: () -> T)
fun f(function: () -> Producer<T>)
fun f(function: (Consumer<T>) -> Unit)
fun f(): (T) -> Unit
fun f(): (Producer<T>) -> Unit
fun f(): () -> Consumer<T>
If you're using a generic class whose type parameters haven't been declared in a variant way (either because its authors didn't think of it, or because the type parameters can't have either variance kind because they are used both as parameter types and return types), you can still use it in a variant way thanks to type projection. The term "projection" refers to the fact that when you do this, you might restrict yourself to using only some of its members - so you're in a sense only seeing a partial, or "projected" version of the class. Let's look again at our Bowl
class, but without the variant interfaces this time:
class Bowl<T> {
private val items = mutableListOf<T>()
fun get(): T = items.removeAt(items.size - 1)
fun add(item: T) { items.add(item) }
}
Because T
is used as a parameter type, it can't be covariant, and because it's used as a return type, it can't be contravariant. But if we only want to use the get()
function, we can project it covariantly with out
:
fun <T> moveCovariantly(from: Bowl<out T>, to: Bowl<T>) {
to.add(from.get())
}
Here, we're saying that the type parameter of from
must be a subtype of the type parameter of to
. This function will accept e.g. a Bowl<Apple>
as from
and Bowl<Fruit>
as to
. The price we're paying for using the out
projection is that we can't call add()
on from()
, since we don't know its true type parameter and we would therefore risk adding incompatible fruits to it.
We could do a similar thing with contravariant projection by using in
:
fun <T> moveContravariantly(from: Bowl<T>, to: Bowl<in T>) {
to.add(from.get())
}
Now, the type parameter of to
must be a supertype of that of from
. This time, we're losing the ability to call get()
on to
.
The same type parameter can be used in both covariant and contravariant projections (because it's the generic classes that are being projected, not the type parameter):
fun <T> moveContravariantly(from: Bowl<out T>, to: Bowl<in T>) {
to.add(from.get())
}
While doing so was not useful in this particular example, one could get interesting effects by adding an unprojected parameter type via: Bowl<T>
, in which case the generic type parameter of via
would be forced to be "in-between" those of from
and to
.
If you don't have any idea (or don't care) what the generic type might be, you can use a star-projection:
fun printSize(items: List<*>) = println(items.size)
When using a generic type where you have star-projected one or more of its type parameters, you can:
- Use any members that don't mention the star-projected type parameter(s) at all
- Use any members that return the star-projected type parameter(s), but the return type will appear to be
Any?
(unless the type parameter is constrained, in which case you'll get the type mentioned in the constraint) - Not use any members that take a star-projected type as a parameter
Sadly, Kotlin has inherited Java's limitation on generics: they are strictly a compile-time concept - the generic type information is erased at runtime. Therefore, you can not say T()
to construct a new instance of a generic type; you can not at runtime check if an object is an instance of a generic type parameter; and if you try to cast between generic types, the compiler can't guarantee the correctness of it.
Luckily, Kotlin has got reified type parameters, which alleviates some of these problems. By writing reified
in front of a generic type parameter, it does become available at runtime, and you'll get to write T::class
to get the class metadata. You can only do this in inline functions (because an inline function will be compiled into its callsite, where the type information is available at runtime), but it still goes a long way. For example, you can make an inline wrapper function for a big function that has got a less elegant signature.
In the example below, we assume that there is a DbModel
base class, and that every subclass has got a parameterless primary constructor. In the inline function, T
is reified, so we can get the class metadata. We pass this to the function that does the real work of talking to the database.
inline fun <reified T : DbModel> loadFromDb(id: String): T =
loadFromDb(T::class, id)
fun <T : DbModel> loadFromDb(cls: KClass<T>, id: String): T {
val entity = cls.primaryConstructor!!.call()
val tableName = cls.simpleName
// DB magic goes here - load from table `tableName`,
// and use the data to populate `entity`
// (possibly via `memberProperties`)
return entity
}
Now, you can say loadFromDb<Exercise>("x01234567")
to load an object from the Exercise
database table.
← Previous: Objects and companion objects | Next: Extension functions/properties →