Kotlin allows you to constrain a type parameter on a generic when you declare it, limiting the range of types that it can accept. Why is this helpful? Well, let’s take a look at an example.
Let’s say you’ve got a few pets at home, and you want to pick a favorite:
fun <T> chooseFavorite(pets: List<T>): T {
val favorite = pets[random.nextInt(pets.size)]
// This next line won't compile - because `name` can't be resolved
println("My favorite pet is ${favorite.name}")
return favorite
}
Blast! The println()
statement won’t compile because you can’t get the value of name
on T
- because T
could be anything that the caller specifies. For example, if you wrote the following, then T
would be an Int
, which doesn’t have a name
property:
chooseFavorite(listOf(1, 2, 3))
Option 1 - Give up on Generics
Well, you could choose to change this function so that it’s no longer a generic:
fun chooseFavorite(pets: List<Pet>): Pet {
val favorite = pets[random.nextInt(pets.size)]
println("My favorite pet is ${favorite.name}")
return favorite
}
That would work, but then we’re stuck with a return type that we might not want. Here’s what happens when we call that function:
val pets: List<Dog> = listOf(Dog("Rover"), Dog("Sheba"))
val favorite: Pet = chooseFavorite(pets)
Even though we start with a List<Dog>
, when we get the favorite back, it’s a Pet
. If we want a reference to Rover or Sheba as a Dog
, we would have to cast it back. Yikes!
Option 2 - Use Type Parameter Constraints
We can limit the type parameter by specifying an upper bound - in other words, the super-most type that you want to accept. In our case, we want this function to work on a List<Dog>
, a List<Cat>
, or a mixed list of both cats and dogs, as List<Pet>
. Easily extended to ferrets and iguanas if that’s your jam.
fun <T : Pet> chooseFavorite(pets: List<T>): T {
val favorite = pets[random.nextInt(pets.size)]
println("My favorite pet is ${favorite.name}")
return favorite
}
The type parameter declaration here is <T : Pet>
, where Pet
is the upper bound. Now that we’ve specified this, the calling code can only pass a List
of Pet
and subtypes of Pet
. Aaaaand… the return type is exactly what we want:
val pets: List<Dog> = listOf(Dog("Rover"), Dog("Sheba"))
val favorite: Dog = chooseFavorite(pets)
Recommendation
You’ll want to use a type parameter constraint when the following characteristics line up:
- You want to invoke a particular function or property on the type.
- You want to preserve the type when it’s returned.
Here’s a quick “cheat sheet” style table to help you decide what to use.
Need to invoke member | No need to invoke member | |
---|---|---|
Need to preserve the type | Use a generic with type parameter constraint | Use a generic without type parameter constraint |
No need to preserve the type | Use a non-generic with proper abstraction | Go use a raw type in Java… ;) |
How to Specify Constraints
There’s a lot more to constraints - you can specify constraints on multiple parameters, and multiple constraints on the same parameter. For all the nitty gritty, go check out the concept article, Type Parameter Constraint. You can also find a quick overview of generic constraints in Kotlin’s official reference documentation.