Skip to main content

Kotlin Star Projections

Introduction

When working with generic types in Kotlin, you might encounter situations where you don't know or don't care about the specific type arguments. For example, you might need to work with a List<T> but don't care about what type T is. This is where star projections come into play.

Star projections in Kotlin are a way to use generic types when the specific type arguments are unknown or irrelevant. They're similar to Java's raw types but with additional type safety.

Understanding Star Projections

In Kotlin, star projections are denoted using the asterisk symbol (*). When you use a star projection, you're essentially telling the compiler: "I know this is a generic type, but I don't know or care about the specific type parameters."

Basic Syntax

kotlin
// Regular generic usage
val list: List<String> = listOf("Kotlin", "Java", "Python")

// Star projection - type argument is unknown
val unknownList: List<*> = list

How Star Projections Work

When you use a star projection (*) for a generic type, it's equivalent to:

  • For a type with variance declaration (out T or in T), the star projection maps to the appropriate upper or lower bound.
  • For invariant types, it maps to out Any? for reading and restricts writing.

Let's break this down:

For Types with out Variance

For a class Box<out T> (where T is covariant):

  • Box<*> is equivalent to Box<out Any?>
  • This means you can read values from it, but they're treated as Any?.
kotlin
class Producer<out T>(val item: T) {
fun get(): T = item
}

fun main() {
val stringProducer = Producer("Hello")
val anyProducer: Producer<*> = stringProducer

// Reading is safe - but type is Any?
val item = anyProducer.get()
println(item) // Output: Hello

// The following would be incorrect:
// val string: String = anyProducer.get() // Type mismatch
}

For Types with in Variance

For a class Box<in T> (where T is contravariant):

  • Box<*> is equivalent to Box<in Nothing>
  • This means you can't safely write values to it.
kotlin
class Consumer<in T> {
fun consume(item: T) {
println("Consuming: $item")
}
}

fun main() {
val stringConsumer = Consumer<String>()
val anyConsumer: Consumer<*> = stringConsumer

// The following would be incorrect:
// anyConsumer.consume("test") // Cannot call 'consume' on Consumer<*>
}

For Invariant Types

For a class Box<T> (where T is invariant):

  • Box<*> allows reading values as Any?
  • Box<*> prohibits writing values

When to Use Star Projections

Star projections are particularly useful in the following scenarios:

  1. When you need to work with generic types but don't care about the specific type arguments
  2. When you only need to read values from a generic container (not write to it)
  3. When implementing generic functions that handle multiple types

Example 1: Checking Container Size

kotlin
fun printContainerSize(container: List<*>) {
println("Container size: ${container.size}")
}

fun main() {
val stringList = listOf("a", "b", "c")
val intList = listOf(1, 2, 3, 4)

printContainerSize(stringList) // Output: Container size: 3
printContainerSize(intList) // Output: Container size: 4
}

Example 2: Type Inspection

kotlin
fun examineCollection(collection: Collection<*>) {
val firstItem = collection.firstOrNull()
when (firstItem) {
is String -> println("Collection of strings, first item: $firstItem")
is Int -> println("Collection of integers, first item: $firstItem")
null -> println("Empty collection")
else -> println("Collection of unknown type, first item: $firstItem")
}
}

fun main() {
examineCollection(listOf("apple", "banana", "cherry"))
// Output: Collection of strings, first item: apple

examineCollection(listOf(1, 2, 3))
// Output: Collection of integers, first item: 1

examineCollection(emptyList<String>())
// Output: Empty collection
}

Practical Applications

Reflection and Generic Classes

Star projections are commonly used when working with reflection, where you might not know the exact type parameters:

kotlin
fun <T> printClassInfo(clazz: Class<*>) {
println("Class name: ${clazz.simpleName}")
println("Is interface: ${clazz.isInterface}")
println("Superclass: ${clazz.superclass?.simpleName ?: "None"}")
}

fun main() {
printClassInfo(String::class.java)
// Output:
// Class name: String
// Is interface: false
// Superclass: Object

printClassInfo(List::class.java)
// Output:
// Class name: List
// Is interface: true
// Superclass: None
}

Generic Function for Type-Agnostic Processing

kotlin
fun processAnyCollection(collection: Collection<*>, action: (Any?) -> Unit) {
collection.forEach { action(it) }
}

fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val words = listOf("apple", "banana", "cherry")

// Process any collection and print elements
processAnyCollection(numbers) { println("Processing: $it") }
// Output:
// Processing: 1
// Processing: 2
// Processing: 3
// Processing: 4
// Processing: 5

// Count characters in strings
processAnyCollection(words) {
if (it is String) println("${it}: ${it.length} characters")
}
// Output:
// apple: 5 characters
// banana: 6 characters
// cherry: 6 characters
}

Star Projections vs. Generic Type Parameters

It's important to understand when to use star projections and when to use proper generic type parameters:

kotlin
// Using star projection - less type safety
fun printFirstElement(list: List<*>) {
val element = list.firstOrNull()
println("First element: $element")
}

// Using generic type parameter - more type safety
fun <T> printFirstElementTyped(list: List<T>): T? {
val element = list.firstOrNull()
println("First element: $element")
return element
}

fun main() {
val strings = listOf("Hello", "World")

printFirstElement(strings)
// Output: First element: Hello

val firstString: String? = printFirstElementTyped(strings)
// Output: First element: Hello
println("Got back: $firstString")
// Output: Got back: Hello
}

The key difference is that with generic type parameters (<T>), you maintain the type information throughout the function, while with star projections (<*>), you lose the specific type information.

Common Pitfalls and Limitations

1. Can't Write to Star-Projected Types

One of the main limitations is that you can't safely write to a star-projected type:

kotlin
fun main() {
val list: MutableList<*> = mutableListOf("a", "b", "c")

// Reading is fine
println(list[0]) // Output: a

// The following would be incorrect:
// list.add("d") // Compile error: Cannot add element to collection with star projection
}

2. Loss of Type Information

With star projections, you lose specific type information:

kotlin
fun main() {
val stringList: List<String> = listOf("a", "b", "c")
val starList: List<*> = stringList

val item = starList[0] // Type of 'item' is Any?

// You need explicit casting to get back the original type
val string: String = item as String
println(string.uppercase()) // Output: A
}

Summary

Star projections in Kotlin provide a way to work with generic types when the specific type arguments are unknown or irrelevant. They offer more type safety than Java's raw types by maintaining some type checking at compile time.

Key points to remember about star projections:

  • Use <*> syntax to represent an unknown type argument
  • They're primarily useful for reading from generic types, not writing to them
  • For covariant types (out T), Box<*> is equivalent to Box<out Any?>
  • For contravariant types (in T), Box<*> is equivalent to Box<in Nothing>
  • Use star projections when you don't care about the specific type, only the structure

Star projections are a powerful feature in Kotlin's type system, but they should be used carefully. When possible, prefer using proper generic type parameters to maintain type safety.

Exercises

  1. Create a function that counts the number of elements in a collection matching a specific condition, using star projections.
  2. Implement a generic cache using a Map<*, *> and explain the limitations of this approach.
  3. Write a function that can print the class names of all elements in any collection.
  4. Create a function that safely converts between different types of collections without knowing the element type.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)