Kotlin provides a powerful type system that includes declaration-site variance, making generics more flexible and safe. If you’ve ever worked with generics in Java, you might recall the ? extends
and ? super
keywords. Kotlin simplifies this with in
and out
modifiers.
In this blog, we’ll break down declaration-site variance in Kotlin, explore how in
and out
work, and compare them to Java wildcards to clarify these concepts.
What is Declaration-Site Variance in Kotlin?
In Kotlin, the ability to specify variance modifiers on class declarations provides convenience and consistency because these modifiers apply to all places where the class is used. This concept is known as a declaration-site variance.
Declaration-site variance in Kotlin is achieved by using variance modifiers on type parameters when defining a class. As you already knows there are two main variance modifiers:
out
(covariant): Denoted by theout
keyword, it allows the type parameter to be used as a return type or read-only property. It specifies that the type parameter can only occur in the “out” position, meaning it can only be returned from functions or accessed in a read-only manner.in
(contravariant): Denoted by thein
keyword, it allows the type parameter to be used as a parameter type. It specifies that the type parameter can only occur in the “in” position, meaning it can only be passed as a parameter to functions.
By specifying these variance modifiers on type parameters, you define the variance behavior of the class, and it remains consistent across all usages of the class.
On the other hand, Java handles variance differently through use-site variance. In Java, each usage of a type with a type parameter can specify whether the type parameter can be replaced with its subtypes or supertypes using wildcard types (? extends
and ? super
). This means that at each usage point of the type, you can decide the variance behavior.
It’s important to note that while Kotlin supports declaration-site variance with the out
and in
modifiers, it also provides a certain level of use-site variance through the out
and in
projection syntax (out T
and in T
). These projections allow you to control the variance behavior in specific usage points within the code.
Declaration-site variance in Kotlin Vs. Java wildcards
In Kotlin, declaration-site variance allows for more concise code because variance modifiers are specified once on the declaration of a class or interface. This means that clients of the class or interface don’t have to think about the variance modifiers. The convenience of declaration-site variance is that the variance behavior is determined at the point of declaration and remains consistent throughout the codebase.
On the other hand, in Java, wildcards are used to handle variance at the use site. To create APIs that behave according to users’ expectations, the library writer has to use wildcards extensively. For example, in the Java 8 standard library, wildcards are used on every use of the Function interface. This can lead to code like Function<? super T, ? extends R>
in method signatures.
To illustrate the declaration of the map
method in the Stream
interface in Java :
/* Java */
public interface Stream<T> {
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
}
In the Java code, wildcards are used in the declaration of the map
method to handle the variance of the function argument. This can make the code less readable and more cumbersome, especially when dealing with complex type hierarchies.
In contrast, the Kotlin code uses declaration-site variance, specifying the variance once on the declaration makes the code much more concise and elegant.
Benefits of Declaration-Site Variance in Kotlin
- Stronger Type Safety — Prevents incorrect type assignments.
- More Readable Code — Clearly defines how a generic class is used.
- No Wildcards Needed — Unlike Java’s
? extends
and? super
, Kotlin’s variance is built-in.
Conclusion
Understanding declaration-site variance in Kotlin with in
and out
is crucial for writing type-safe and maintainable generic classes. The out
modifier allows a class to only produce values, making it covariant, while in
allows a class to only consume values, making it contravariant.
By leveraging these concepts, you can write more flexible, safe, and concise Kotlin code without the complexity of Java’s wildcards.