Generics might seem complicated, but there are ways to make them simpler. In this article, we take a look at Kotlin INs and OUTs and when to use which
Generics might seem complicated, but there are ways to make them simpler. In this article, we take a look at Kotlin INs and OUTs and when to use which
Generics give us the ability to write flexible implementations with the same underlying behavior for many input types. In practice, we can create reusable implementations with the benefit of type safety by eliminating unforeseen outcomes and removing the need for explicit type casts.
Like all modern programming language, Kotlin has supported generics since it was created. Its implementation and supported features are similar to Java’s, but with remarkable improvements that make generics much easier to understand.
One of Kotlin’s notable improvements is its approach to describing wildcard types. It’s worth noting that Kotlin doesn’t actually support wildcard types, but instead has declaration-site and use-site variance (type projections). However, before we delve deeper into these, we should first cover some fundamental concepts required to understand these terms.
Type Parameters & Type Arguments
The primary constructs of generics are type parameters and type arguments. Type parameters serve as placeholders that are substituted with actual type arguments when a generic type is instantiated.
To get a better picture of what this means, consider the following example:
Variance & Java Wildcard Types
Variance explains the inheritance relationship between more complex types and their component types (sub-types).
Put simply, variance describes the relationship between classes that have the same base type but different type arguments. There are 4 types of variance:
1. Invariance
Where neither sub-type nor super-type could be assigned to a type in the same type hierarchy.
In simpler terms, “MyType
By default, all generic types in Java are invariant. This restricts the potential of generics. To overcome this limitation, Java uses a mechanism of upper and lower bound wildcard types.
2. Covariance: Upper Bounded Wildcards
Where we’re allowed to assign sub-types but not super-types.
To do this, we can create a type with a wildcard type argument that can be substituted by a defined type or anything that extends or implements that same type.
This basically defines the upper limit of allowed types in the Hierarchy Tree.
3. Contravariance: Lower Bounded Wildcards
Where we’re allowed to assign supertypes but no subtypes.
Similarly, as with covariance, we can create a type with a wildcard type argument that can be substituted by a specified type and all its parents, essentially defining the lower boundary of allowed items.
4. Bivariance: Unbounded Wildcards
Where we’re allowed to assign both subtypes and supertypes.
Java doesn’t have a specific implementation for bivariance. However, we can attain a comparable outcome with unbound wildcards (star projections in Kotlin).
This mechanism is unique and warrants a dedicated blog post, so we won’t discuss it in further detail here.
Kotlin Mixed-site Variance
Generics were a welcome addition to Java when they were introduced in 2004. Nevertheless, some currently regard them as unsuccessful, primarily due of type erasure, the fact that they are enforced only during compilation, difficult to comprehend, and somewhat awkward to write. For example, every time you want to use a bound wildcard, you must specify what kind of sub-typing behavior you need.
Specifying variance modifiers at usage places is known as use-site variance. It leads to unnecessary code duplication and expressiveness. The team over at JetBrains opted for a different approach. Instead of specifying wildcards every time, you declare them once, where the generic type is declared as “declaration-site variance.”
Declaration Site Variance
In Java, upper bound wildcards have a specific restriction preventing the invocation of any method that “consumes” type parameters. This is why such methods are deemed unsafe. Let’s look at the following example to understand why:
It’s safe to read from these lists because we can just upcast them to Dog, but we can’t add any of the items, since the compiler doesn’t recall the type contained in the list.
We can observe that Java upper bound wildcards only permit us to “produce” values, but not consume them.
With Kotlin declaration site variance, we can achieve the same outcome with greater ease.
Similarly, Java prohibits the usage of anything that returns type parameters for contravariance, but it’s okay to “consume” them.
The IN modifier can be utilized to indicate the type parameters that are permissible to be consumed.
Use Site Variance
In certain cases, you don’t have access to a generic type, or the type might not be either covariant, nor contravariant thereby rendering declaration site variance unusable. For this purpose, Kotlin supports use-site variance, which works similarly to declaration site variance.
Summary
Generics might seem a bit complicated, but luckily, we can follow a couple of rules to make our lives easier.
When To Use OUT?
- When you have a function that’s “producing” a generic type parameter.
- When you want to assign a subtype to a supertype or, in other words, when you want to achieve the same behavior as extend Dog in Java.
- When you want to restrict read-only usage on your type.
When To Use IN?
- When you have a function that’s only “consuming” a generic type parameter.
- When you want to assign a supertype to a subtype when you want to achieve the same behavior as <? super Dog> in Java.
- When you want to restrict write-only usage on your type.
Did you enjoy this article? Be sure to check out the Smallpdf Engineering blog for more!