Kotlin is a powerful programming language that simplifies development while maintaining strong type safety. One of the essential concepts in Kotlin is variance, which helps us understand how generics and subtyping work. If you’ve ever been confused by out
, in
, or *
, and how generics behave in Kotlin, this guide is for you.
Variance: generics and subtyping
The concept of variance describes how types with the same base type and different type arguments relate to each other: for example, List<String> and List<Any>. It’s important to understand variance when working with generic classes or functions because it helps ensure the safety and consistency of your code.
Why variance exists: passing an argument to a function
To illustrate why variance is important, let’s consider passing arguments to functions. Suppose we have a function that takes a List<Any>
as an argument. Is it safe to pass a variable of type List<String>
to this function?
In the case of a function that prints the contents of the list, such as:
fun printContents(list: List<Any>) {
println(list.joinToString())
}
You can safely pass a list of strings (List<String>
) to this function. Each element in the list is treated as an Any
, and since String
is a subtype of Any
, it is considered safe.
However, let’s consider another function that modifies the list:
fun addAnswer(list: MutableList<Any>) {
list.add(42)
}
If you attempt to pass a list of strings (MutableList<String>
) to this function, like so:
val strings = mutableListOf("abc", "bac")
addAnswer(strings)
println(strings.maxBy { it.length })
You will encounter a ClassCastException
at runtime. This occurs because the function addAnswer
tries to add an integer (42
) to a list of strings. If the compiler allowed this, it would lead to a type inconsistency when accessing the contents of the list as strings. To prevent such issues, the Kotlin compiler correctly forbids passing a MutableList<String>
as an argument when a MutableList<Any>
is expected.
So, the answer to whether it’s safe to pass a list of strings to a function expecting a list of Any
objects depends on whether the function modifies the list. If the function only reads the list, it is safe to pass a List
with a more specific element type. However, if the list is mutable and the function adds or replaces elements, it is not safe.
Kotlin provides different interfaces, such as List
and MutableList
, to control safety based on mutability. If a function accepts a read-only list, you can pass a List
with a more specific element type. However, if the list is mutable, you cannot do that.
In the upcoming sections, we’ll explore these concepts in the context of generic classes. We’ll also examine why List
and MutableList
differ regarding their type arguments. But before that, let’s discuss the concepts of type and subtype.
Difference between Classes, types, and subtypes
In Kotlin, the type of a variable specifies the possible values it can hold. The terms “type” and “class” are sometimes used interchangeably, but they have distinct meanings. In the case of a non-generic class, the class name can be used directly as a type. For example, var x: String
declares a variable that can hold instances of the String
class. However, the same class name can also be used to declare a nullable type, such as var x: String?
which indicates that the variable can hold either a String
or null
. So each Kotlin class can be used to construct at least two types.
When it comes to generic classes, things get more complex. To form a valid type, you need to substitute a specific type as a type argument for the class’s type parameter. For example, List
is a class, not a type itself, but the following substitutions are valid types: List<Int>
, List<String?>
, List<List<String>>
, and so on. Each generic class can generate an infinite number of potential types.
To discuss the relationship between types, it’s important to understand the concept of subtyping. Type B is considered a subtype of type A if you can use a value of type B wherever a value of type A is expected. For example, Int
is a subtype of Number
, but Int
is not a subtype of String
. Note that a type is considered a subtype of itself. The term “supertype” is the opposite of subtype: if A is a subtype of B, then B is a supertype of A.

Understanding subtype relationships is crucial because the compiler performs checks whenever you assign a value to a variable or pass an argument to a function. For example:
fun test(i: Int) {
val n: Number = i
fun f(s: String) { /*...*/ }
f(i)
}
Storing a value in a variable is only allowed if the value’s type is a subtype of the variable’s type. In this case, since Int
is a subtype of Number
, the declaration val n: Number = i
is valid. Similarly, passing an expression to a function is only allowed if the expression’s type is a subtype of the function’s parameter type. In the example, the type Int
of the argument i
is not a subtype of the function parameter type String
, so the invocation of the f
function does not compile.
In simpler cases, subtype is essentially the same as subclass. For example, Int
is a subclass of Number
, so the Int
type is a subtype of the Number
type. If a class implements an interface, its type is a subtype of the interface type. For instance, String
is a subtype of CharSequence
.
Nullable types introduce a scenario where subtype and subclass differ. A non-null type is a subtype of its corresponding nullable type, but they both correspond to the same class.

You can store the value of a non-null type in a variable of a nullable type, but not vice versa. For example:
val s: String = "abc"
val t: String? = s
In this case, the value of the non-null type String
can be stored in a variable of the nullable type String?
. However, you cannot assign a nullable type to a non-null type because null
is not an acceptable value for a non-null type.
The distinction between subclasses and subtypes becomes particularly important when dealing with generic types. This brings us back to the question from the previous section: is it safe to pass a variable of type List<String>
to a function expecting List<Any>
? We’ve already seen that treating MutableList<String>
as a subtype of MutableList<Any>
is not safe. Similarly, MutableList<Any>
is not a subtype of MutableList<String>
either.
A generic class, such as MutableList
, is called invariant on the type parameter if, for any two different types A
and B
, MutableList<A>
is neither a subtype nor a supertype of MutableList<B>
. In Java, all classes are invariant, although specific uses of those classes can be marked as non-invariant (as you’ll see soon).
In the previous section, we encountered a class, List
, where the subtyping rules are different. The List
interface in Kotlin represents a read-only collection. If type A
is a subtype of type B
, then List<A>
is a subtype of List<B>
. Such classes or interfaces are called covariant. In the upcoming sections, we’ll explore the concept of covariance in more detail and explain when it’s possible to declare a class or interface as covariant.
Covariance: preserved subtyping relation
Covariance refers to preserving the subtyping relation between generic classes. In Kotlin, you can declare a class to be covariant on a specific type parameter by using the out
keyword before the type parameter’s name.
A covariant class is a generic class (we’ll use Producer as an example) for which the following holds: Producer<A> is a subtype of Producer<B> if A is a subtype of B. We say that the subtyping is preserved. For example, Producer<Cat> is a subtype of Producer<Animal> because Cat is a subtype of Animal.
Here’s an example of the Producer
interface using the out
keyword:
interface Producer<out T> {
fun produce(): T
}
Flexible Function Argument and Return Value Passing
Covariance in Kotlin allows you to pass values of a class as function arguments and return values, even when the type arguments don’t exactly match the function definition. This means that you can use a more specific type as a substitute for a more generic type.
Suppose we have a hierarchy of classes involving Animal
, where Cat
is a subclass of Animal
. We also have a generic interface called Producer
, which represents a producer of objects of type T
. We’ll make the Producer
interface covariant by using the out
keyword on the type parameter.
interface Producer<out T> {
fun produce(): T
}
Now, let’s define a class AnimalProducer
that implements the Producer
interface for the Animal
type:
class AnimalProducer : Producer<Animal> {
override fun produce(): Animal {
return Animal()
}
}
Since Animal
is a subtype of Animal
, we can use AnimalProducer
wherever a Producer<Animal>
is expected.
Now, let’s define another class CatProducer
that implements the Producer
interface for the Cat
type:
class CatProducer : Producer<Cat> {
override fun produce(): Cat {
return Cat()
}
}
Since Cat
is a subtype of Animal
, we can also use CatProducer
wherever a Producer<Animal>
is expected. This is possible because we declared the Producer
interface as covariant.
Now, let’s see how covariance allows us to pass these producers as function arguments and return values:
fun feedAnimal(producer: Producer<Animal>) {
val animal = producer.produce()
animal.feed()
fun main() {
val animalProducer = AnimalProducer()
val catProducer = CatProducer()
feedAnimal(animalProducer) // Passes an AnimalProducer, which is a Producer<Animal>
feedAnimal(catProducer) // Passes a CatProducer, which is also a Producer<Animal>
}
In the feedAnimal
function, we expect a Producer<Animal>
as an argument. With covariance, we can pass both AnimalProducer
and CatProducer
instances because Producer<Cat>
is a subtype of Producer<Animal>
due to the covariance declaration.
This demonstrates how covariance allows you to treat more specific types (Producer<Cat>
) as if they were more generic types (Producer<Animal>
) when it comes to function arguments and return values.
BTW, How covariance guarantees type safety?
Suppose we have a class hierarchy involving Animal
, where Cat
is a subclass of Animal
. We also have a Herd
class that represents a group of animals.
open class Animal {
fun feed() { /* feeding logic */ }
}
class Herd<T : Animal> { // The type parameter isn’t declared as covariant
val size: Int get() = /* calculate the size of the herd */
operator fun get(i: Int): T { /* get the animal at index i */ }
}
fun feedAll(animals: Herd<Animal>) {
for (i in 0 until animals.size) {
animals[i].feed()
}
}
Now, suppose you have a function called takeCareOfCats
, which takes a Herd<Cat>
as a parameter and performs some operations specific to cats.
class Cat : Animal() {
fun cleanLitter() { /* clean litter logic */ }
}
fun takeCareOfCats(cats: Herd<Cat>) {
for (i in 0 until cats.size) {
cats[i].cleanLitter()
// feedAll(cats) // This line would cause a type-mismatch error, Error: inferred type is Herd<Cat>, but Herd<Animal> was expected
}
}
In this case, if you try to pass the cats
herd to the feedAll
function, you’ll get a type-mismatch error during compilation. This happens because you didn’t use any variance modifier on the type parameter T
in the Herd
class, making the Herd<Cat>
incompatible with Herd<Animal>
. Although you could use an explicit cast to overcome this issue, it is not a recommended approach.
To make it work correctly, you can make the Herd
class covariant by using the out
keyword on the type parameter:
class Herd<out T : Animal> { // The T parameter is now covariant.
// ...
}
fun takeCareOfCats(cats: Herd<Cat>) {
for (i in 0 until cats.size) {
cats[i].cleanLitter()
}
feedAll(cats) // Now this line works because of covariance, You don’t need a cast.
By marking the type parameter as covariant, you ensure that the subtyping relation is preserved, and T
can only be used in \”out\” positions. This guarantees type safety and allows you to pass a Herd<Cat>
where a Herd<Animal>
is expected.
Usage of covariance
Covariance in Kotlin allows you to make a class covariant on a type parameter, but it also imposes certain constraints to ensure type safety. The type parameter can only be used in “out” positions, which means it can produce values of that type but not consume them.
You can’t make any class covariant: it would be unsafe. Making the class covariant on a certain type parameter constrains the possible uses of this type parameter in the class. To guarantee type safety, it can be used only in so-called out positions, meaning the class can produce values of type T but not consume them. Uses of a type parameter in declarations of class members can be divided into “in” and “out” positions.
Let’s consider a class that declares a type parameter T and contains a function that uses T. We say that if T is used as the return type of a function, it’s in the out position. In this case, the function produces values of type T. If T is used as the type of a function parameter, it’s in the in position. Such a function consumes values of type T.

The out keyword on a type parameter of the class requires that all methods using T have T only in “out” positions and not in “in” positions. This keyword constrains possible use of T, which guarantees safety of the corresponding subtype relation.
Let’s understand this with some examples. Consider the Herd
class, which is declared as Herd<out T : Animal>
. The type parameter T
is used only in the return value of the get
method. This is an “out” position, and it is safe to declare the class as covariant. For instance, Herd<Cat>
is considered a subtype of Herd<Animal>
because Cat
is a subtype of Animal
.
class Herd<out T : Animal> {
val size: Int = ...
operator fun get(i: Int): T { ... } // Uses T as the return type
}
Similarly, the List<T>
interface in Kotlin is covariant because it only defines a get
method that returns an element of type T
. Since there are no methods that store values of type T
, it is safe to declare the class as covariant.
interface List<out T> : Collection<T> {
operator fun get(index: Int): T // Read-only interface that defines only methods that return T (so T is in the “out” position)
// ...
}
You can also use the type parameter T
as a type argument in another type. For example, the subList
method in the List
interface returns a List<T>
, and T
is used in the “out” position.
interface List<out T> : Collection<T> {
fun subList(fromIndex: Int, toIndex: Int): List<T> // Here T is in the “out” position as well.
// ...
}
However, you cannot declare MutableList<T>
as covariant on its type parameter because it contains methods that both consume and produce values of type T
. Therefore, T
appears in both “in” and “out” positions, and making it covariant would be unsafe.
interface MutableList<T> : List<T>, MutableCollection<T> { //MutableList can’t be declared as covariant on T …
override fun add(element: T): Boolean // … because T is used in the “in” position.
}
The compiler enforces this restriction. It would report an error if the class was declared as covariant: Type parameter T is declared as ‘out’ but occurs in ‘in’ position.
Constructor Parameters and Variance
In Kotlin, constructor parameters are not considered to be in the “in” or “out” position when it comes to variance. This means that even if a type parameter is declared as “out,” you can still use it in a constructor parameter declaration without any restrictions.
For example:
class Herd<out T: Animal>(vararg animals: T) { ... }
The type parameter T
is declared as “out” but it can still be used in the constructor parameter vararg animals: T
without any issues. The variance protection is not applicable to the constructor because it is not a method that can be called later, so there are no potentially dangerous method calls that need to be restricted.
However, if you use the val
or var
keyword with a constructor parameter, it declares a property with a getter and setter (if the property is mutable). In this case, the type parameter T
is used in the “out” position for a read-only property and in both “out” and “in” positions for a mutable property.
For example:
class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) { ... }
Here, the type parameter T
cannot be marked as “out” because the class contains a setter for the leadAnimal
property, which uses T
in the “in” position. The presence of a setter makes it necessary to consider both “out” and “in” positions for the type parameter.
It’s important to note that the position rules for variance in Kotlin only apply to the externally visible API of a class, such as public, protected, and internal members. Parameters of private methods are not subject to the “in” or “out” position rules. The variance rules are in place to protect a class from misuse by external clients and do not affect the implementation of the class itself.
For instance:
class Herd<out T: Animal>(private var leadAnimal: T, vararg animals: T) { ... }
In this case, the Herd
class can safely be made covariant on T
because the leadAnimal
property has been made private. The private visibility means that the property is not accessible from external clients, so the variance rules for the public API do not apply.
Contravariance: reversed subtyping relation
Contravariance is the opposite of covariance and it can be understood as a mirror image of covariance. When a class is contravariant, the subtyping relationship between its type arguments is the reverse of the subtyping relationship between the classes themselves.
To illustrate this concept, let’s consider the example of the Comparator interface. This interface has a single method called compare, which takes two objects and compares them:
interface Comparator<in T> {
fun compare(e1: T, e2: T): Int { ... }
}
In this case, you’ll notice that the compare method only consumes values of type T. This means that the type parameter T is used in “in” positions only, indicating that it is a contravariant type. To indicate contravariance, the “in” keyword is placed before the declaration of T.
A comparator defined for values of a certain type can, of course, compare the values of any subtype of that type. For example, if you have a Comparator, you can use it to compare values of any specific type.
val anyComparator = Comparator<Any> { e1, e2 -> e1.hashCode() - e2.hashCode() }
val strings: List<String> = listOf("abc","xyz")
strings.sortedWith(anyComparator) // You can use the comparator for any objects to compare specific objects, such as strings.
Here, the sortedWith function expects a Comparator (a comparator that can compare strings), and it’s safe to pass one that can compare more general types. If you need to perform comparisons on objects of a certain type, you can use a comparator that handles either that type or any of its supertypes. This means Comparator<Any> is a subtype of Comparator<String>, where Any is a supertype of String. The subtyping relation between comparators for two different types goes in the opposite direction of the subtyping relation between those types.
What is contravariance?
A class that is contravariant on the type parameter is a generic class (let’s consider Consumer<T> as an example) for which the following holds: Consumer<A> is a subtype of Consumer<B> if B is a subtype of A. The type arguments A and B changed places, so we say the subtyping is reversed. For example, Consumer<Animal> is a subtype of Consumer<Cat>.
In simple words, contravariance in Kotlin means that the subtyping relationship between two generic types is reversed compared to the normal inheritance hierarchy. If B
is a subtype of A
, then a generic class or interface that is contravariant on its type parameter T
will have the relationship ClassName<A>
is a subtype of ClassName<B>
.

Here, we see the difference between the subtyping relation for classes that are covariant and contravariant on a type parameter. You can see that for the Producer class, the subtyping relation replicates the subtyping relation for its type arguments, whereas for the Consumer class, the relation is reversed.
The “in” keyword means values of the corresponding type are passed in to methods of this class and consumed by those methods. Similar to the covariant case, constraining use of the type parameter leads to the specific subtyping relation. The “in” keyword on the type parameter T means the subtyping is reversed and T can be used only in “in” positions.
Covariance and Contravariance in Kotlin’s Function Types
In Kotlin, a class or interface can be covariant on one type parameter and contravariant on another. One of the classic examples of this is the Function
interface. Let’s take a look at the declaration of the Function1
interface, which represents a one-parameter function:
interface Function1<in P, out R> {
operator fun invoke(p: P): R
}
To make the notation more readable, Kotlin provides an alternative syntax (P) -> R
to represent Function1<P, R>
. In this syntax, you’ll notice that P
(the parameter type) is used only in the in
position and is marked with the in
keyword, while R
(the return type) is used only in the out
position and is marked with the out
keyword.
This means that the subtyping relationship for the function type is reversed for the first type argument (P
) and preserved for the second type argument (R
).
For example, let’s say you have a higher-order function called enumerateCats
that accepts a lambda function taking a Cat
parameter and returning a Number
:
fun enumerateCats(f: (Cat) -> Number) { ... }
Now, suppose you have a function called getIndex
defined in the Animal
class that returns an Int
. You can pass Animal::getIndex
as an argument to enumerateCats
:
fun Animal.getIndex(): Int = ...
enumerateCats(Animal::getIndex) // This code is legal in Kotlin. Animal is a supertype of Cat, and Int is a subtype of Number
In this case, the Animal::getIndex
function is accepted because Animal
is a supertype of Cat
, and Int is a subtype of Number, the function type’s subtyping relationship allows it.

This illustration demonstrates how subtyping works for function types. The arrows indicate the subtyping relationship.
Use-site variance: specifying variance for type occurrences
To understand use-site variance better, you first need to understand declaration-site variance. 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.
BTW, How does use-site variance work in Kotlin?
Kotlin supports use-site variance, you can specify variance at the use site, which means you can indicate the variance for a specific occurrence of a type parameter, even if it can’t be declared as covariant or contravariant in the class declaration. Let’s break down the concepts and see how use-site works.
In Kotlin, many interfaces, like MutableList
, are not covariant or contravariant by default because they can both produce and consume values of the types specified by their type parameters. However, in certain situations, a variable of that type may be used only as a producer or only as a consumer.
Consider the function copyData
that copies elements from one collection to another:
fun <T> copyData(source: MutableList<T>, destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
In this function, both the source
and destination
collections have an invariant type. However, the source
collection is only used for reading, and the destination
collection is only used for writing. In this case, the element types of the collections don’t need to match exactly.
To make this function work with lists of different types, you can introduce a second generic parameter:
fun <T : R, R> copyData(source: MutableList<T>, destination: MutableList<R>) {
for (item in source) {
destination.add(item)
}
}
In this modified version, you declare two generic parameters representing the element types in the source and destination lists. The source element type (T
) should be a subtype of the elements in the destination list (R
).
However, Kotlin provides a more elegant way to express this using use-site variance. If the implementation of a function only calls methods that have the type parameter in the “out” position (as a producer) or only in the “in” position (as a consumer), you can add variance modifiers to the particular usages of the type parameter in the function definition.
For example, you can modify the copyData
function as follows:
fun <T> copyData(source: MutableList<out T>, destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
In this version, you specify the out
modifier for the source
parameter, which means it’s a projected (restricted) MutableList
. You can only call methods that return the generic type parameter (T
) or use it in the “out” position. The compiler prohibits calling methods where the type parameter is used as an argument (“in” position).
Here’s an example usage:
val ints = mutableListOf(1, 2, 3)
val anyItems = mutableListOf<Any>()
copyData(ints, anyItems)
println(anyItems) // [1, 2, 3]
When using use-site variance in Kotlin, there are limitations on the methods that can be called on a projected type. If you are using a projected type, you may not be able to call certain methods that require the type parameter to be used as an argument (“in” position) :
val list: MutableList<out Number> = ..
list.add(42) // Error: Out-projected type 'MutableList<out Number>' prohibits the use of 'fun add(element: E): Boolean'
Here, list
is declared as a MutableList<out Number>
, which is an out-projected type. The out
projection restricts the type parameter Number
to only be used in the “out” position, meaning it can only be used as a return type or read from. You cannot call the add
method because it requires the type parameter to be used as an argument (“in” position).
If you need to call methods that are prohibited by the projection, you should use a regular type instead of a projection. In this case, you can use
MutableList<Number>
instead ofMutableList<out Number>
. By using the regular type, you can access all the methods available for that type.
Regarding the concept of using the in
modifier, it indicates that in a particular location, the corresponding value acts as a consumer, and the type parameter can be substituted with any of its supertypes. This is similar to the contravariant position in Java’s bounded wildcards.
For example, the copyData
function can be rewritten using an in-projection:
fun <T> copyData(source: MutableList<T>, destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
In this version, the destination
parameter is projected with the in
modifier, indicating that it can consume elements of type T
or any of its supertypes. This allows you to copy elements from the source
list to a destination list with a broader type.
It’s important to note that use-site variance declarations in Kotlin correspond directly to Java’s bounded wildcards.
MutableList<out T>
in Kotlin is equivalent toMutableList<? extends T>
in Java, while the in-projectedMutableList<in T>
corresponds to Java’sMutableList<? super T>
.
Use-site projections in Kotlin can help widen the range of acceptable types and provide more flexibility when working with generic types, without the need for separate covariant or contravariant interfaces.
Star projection: using * instead of a type argument
In Kotlin, star projection is a syntax that allows you to indicate that you have no information about a generic argument. It is represented by the asterisk (*) symbol. Let’s explore the semantics of star projections in more detail.
When you use star projection, such as List<*>
, it means you have a list of elements of an unknown type. It’s important to note that MutableList<*>
is not the same as MutableList<Any?>
. The former represents a list that contains elements of a specific type, but you don’t know what type it is. You can’t put any values into the list because it may violate the expectations of the calling code. However, you can retrieve elements from the list because you know they will match the type Any?
, which is the supertype of all Kotlin types.
Here’s an example to illustrate this:
val list: MutableList<Any?> = mutableListOf('a', 1, "qwe")
val chars = mutableListOf('a', 'b', 'c')
val unknownElements: MutableList<*> = if (Random().nextBoolean()) list else chars
unknownElements.add(42) // Error: Adding elements to a MutableList<*> is not allowed
println(unknownElements.first()) // You can retrieve elements from unknownElements
In this example, unknownElements
can be either list
or chars
based on a random condition. You can’t add any values to unknownElements
because its type is unknown, but you can retrieve elements from it using the first()
function.
unknownElements.add(42)
// Error: Out-projected type 'MutableList<*>' prohibits//the use of 'fun add(element: E): Boolean'
The term “out-projected type” refers to the fact that MutableList<*>
is projected to act as MutableList<out Any?>
. It means you can safely get elements of type Any?
from the list but cannot put elements into it.
For contravariant type parameters, like Consumer<in T>
, a star projection is equivalent to <in Nothing>
. In this case, you can’t call any methods that have T
in the signature on a star projection because you don’t know exactly what it can consume. This is similar to Java’s wildcards (MyType<?>
in Java corresponds to MyType<*>
in Kotlin).
You can use star projections when the specific information about type arguments is not important. For example, if you only need to read the data from a list or use methods that produce values without caring about their specific types. Here’s an example of a function that takes List<*>
as a parameter:
fun printFirst(list: List<*>) {
if (list.isNotEmpty()) {
println(list.first())
}
}
printFirst(listOf("softAai", "Apps")) // Output: softAai
In this case, the printFirst
function only reads the first element of the list and doesn’t care about its specific type. Alternatively, you can introduce a generic type parameter if you need more control over the type:
fun <T> printFirst(list: List<T>) {
if (list.isNotEmpty()) {
println(list.first())
}
}
The syntax with star projection is more concise, but it works only when you don’t need to access the exact value of the generic type parameter.
Now let’s consider an example using a type with a star projection and common traps that you may encounter. Suppose you want to validate user input using an interface called FieldValidator
. It has a type parameter declared as contravariant (in T
). You also have two validators for String
and Int
inputs.
interface FieldValidator<in T> {
fun validate(input: T): Boolean
}
object DefaultStringValidator : FieldValidator<String> {
override fun validate(input: String) = input.isNotEmpty()
}
object DefaultIntValidator : FieldValidator<Int> {
override fun validate(input: Int) = input >= 0
}
If you want to store all validators in the same container and retrieve the right validator based on the input type, you might try using a map. However, using FieldValidator<*>
as the value type in the map can lead to difficulties. You won’t be able to validate a string with a validator of type FieldValidator<*>
because the compiler doesn’t know the specific type of the validator.
val validators = mutableMapOf<KClass<*>, FieldValidator<*>>()
validators[String::class] = DefaultStringValidator
validators[Int::class] = DefaultIntValidator
validators[String::class]!!.validate("") // Error: Cannot call validate() on FieldValidator<*>
In this case, you will encounter a similar error as before, indicating that it’s unsafe to call a method with the type parameter on a star projection. One way to work around this is by explicitly casting the validator to the desired type, but this is not recommended as it is not type-safe.
val stringValidator = validators[String::class] as FieldValidator<String>
println(stringValidator.validate("")) // Output: false
This code compiles, but it’s not safe because the cast is unchecked and may fail at runtime if the generic type information is erased.
A safer approach is to encapsulate the access to the map and provide type-safe methods for registration and retrieval. This ensures that only the correct validators can be registered and retrieved. Here’s an example using an object called Validators
:
object Validators {
private val validators = mutableMapOf<KClass<*>, FieldValidator<*>>()
fun <T : Any> registerValidator(kClass: KClass<T>, fieldValidator: FieldValidator<T>) {
validators[kClass] = fieldValidator
}
@Suppress("UNCHECKED_CAST")
operator fun <T : Any> get(kClass: KClass<T>): FieldValidator<T> =
validators[kClass] as? FieldValidator<T>
?: throw IllegalArgumentException("No validator for ${kClass.simpleName}")
}
Validators.registerValidator(String::class, DefaultStringValidator)
Validators.registerValidator(Int::class, DefaultIntValidator)
println(Validators[String::class].validate("softAai Apps")) // Output: true
println(Validators[Int::class].validate(42)) // Output: true
In this example, the Validators
object controls all access to the map, ensuring that only correct validators can be registered and retrieved. The code emits a warning about the unchecked cast, but the guarantees provided by the Validators
object make sure that no incorrect use can occur.
This pattern of encapsulating unsafe code in a separate place helps prevent misuse and makes the usage of a container safe. It’s worth noting that this pattern is not specific to Kotlin and can be applied in Java as well.
Conclusion
Understanding Kotlin variance helps you write safer, more flexible code. By using out
for producers, in
for consumers, and keeping generics invariant when necessary, you ensure your programs remain type-safe and efficient.
Next time you see out
, in
, or *
, you’ll know exactly what’s happening and why!