Generics in Kotlin add flexibility and type safety, but sometimes we don’t need to specify a type. This is where star projection (*
) comes in. In this blog, we’ll explore star projection in Kotlin, its use cases, and practical examples to help you understand how and when to use it.
Understanding Star Projection in Kotlin
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><br>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.
Star Projection vs Wildcards in Java
If you are familiar with Java, you might recognize ?
(wildcard) in generics:
void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
In Kotlin, ?
is used for nullability, so *
is used instead for wildcard-like behavior.
List<?>
in Java ⟶List<*>
in KotlinList<? extends T>
in Java ⟶List<out T>
in KotlinList<? super T>
in Java ⟶List<in T>
in Kotlin
Kotlin’s approach is more concise and expressive, improving readability and reducing boilerplate code.
Limitations of Star Projection
While *
is useful, it has some limitations:
- You cannot add elements to a
List<*>
because the exact type is unknown. - The compiler restricts unsafe operations to maintain type safety.
- Using
*
excessively can make code less readable.
For example, this won’t work:
fun addElement(list: MutableList<*>) {
list.add(42) // Error: Cannot add an element of type Int
}
To modify a generic list, you need a known type parameter:
fun <T> addElement(list: MutableList<T>, element: T) {
list.add(element)
}
When to Use Star Projection in Kotlin
- When you need to access elements from a generic class but don’t care about their exact type.
- When working with APIs that use generics, and you don’t want to specify a concrete type.
- When you want to achieve type safety while maintaining code flexibility.
Conclusion
Star projection in Kotlin simplifies working with generics when the type is unknown or irrelevant. It provides flexibility while ensuring type safety, making it a valuable tool in generic programming.
Next time you’re handling generics, consider whether *
might be the right choice to simplify your code.