Skip to Content
Course content

146: Star Projections in Generics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've likely run into a situation where you're writing a utility function—maybe a logger or a validator—that needs to take a generic collection, but you honestly don't care what's actually inside that collection. You just need to know it's a List so you can check its size or iterate over it. Your first instinct is probably to use Any, because in almost every other part of the language, Any is the "I don't care" type.

The "Any" Trap

Let's look at a typical mistake. Suppose we're building a telemetry system. We have different types of events: ClickEvent, HoverEvent, and ScrollEvent. All of them inherit from a base Event class. You want a helper function that prints out how many events are in a batch, regardless of the specific event type.

class Event
class ClickEvent : Event()
class HoverEvent : Event()

fun logBatchSize(events: List<Any>) {
    println("Processing batch of size: ${events.size}")
}

val clicks = listOf(ClickEvent(), ClickEvent())
logBatchSize(clicks) // ❌ Compile Error!

This is where Kotlin's type system catches you. You'll see an error saying List<ClickEvent> is not compatible with List<Any>. It feels wrong, right? A ClickEvent is an Any, so why isn't a list of them a list of Any? The reason is that List is covariant (List<out T>), but the compiler needs to be absolutely sure about the type safety of the reference you're passing. By declaring List<Any>, you're telling Kotlin, "I want a list that can hold any object." But clicks is a list that can only hold ClickEvents. They aren't the same thing.

Letting the Compiler Relax with Star Projections

When you truly don't care about the type—and you don't intend to add anything to the collection—you use a star projection: *. This is you telling the compiler, "I know this is a generic type, but I'm opting out of specifying what that type is."

fun logBatchSize(events: List<*>) {
    println("Processing batch of size: ${events.size}")
}

val clicks = listOf(ClickEvent(), ClickEvent())
logBatchSize(clicks) // ✅ Works perfectly

Now the code compiles. List<*> is effectively a shorthand for List<out Any?>. It tells Kotlin that the list contains some specific type, but we don't know which one. This is the "better way" because it accepts any List regardless of its generic argument, while still maintaining type safety.

The Trade-off: Read-Only Limitations

There is a catch, and it's a big one. When you use a star projection, you lose the ability to write to that collection. Since the compiler doesn't know if the list is a List<String>, a List<Int>, or a List<ClickEvent>, it cannot possibly let you add an item to it. If it let you add a String to a List<*> that happened to actually be a List<Int> at runtime, the app would crash with a ClassCastException.

I like to think of List<*> as a "read-only view of an unknown type." You can pull items out of the list, but they will all be treated as Any? because that's the only thing the compiler can guarantee they are. You can't put anything in (except null, in some specific mutable scenarios, though generally, you should treat it as strictly read-only).

So, the rule of thumb I follow is this: if your function needs to manipulate the contents of a generic collection (adding/replacing), use a type parameter like <T>. If your function only needs to use the methods of the container itself (like size, isEmpty(), or iterating to print values), use a star projection. It makes your API more flexible and removes the need for unnecessary generic declarations in your function signatures.




📋 Practical Task

Implementing a Generic Cache Inspector

You are building a caching system that stores various types of data in different maps. You need to create a utility class called CacheInspector that can analyze any map, regardless of what the keys and values are.

Your requirements:

  • Create a class CacheInspector.
  • Add a method printCacheDetails that takes a Map as an argument.
  • Use star projections for both the key and the value of the map to ensure the method accepts any map (e.g., Map<String, Int>, Map<Int, User>, etc.).
  • Inside the method, print the total number of entries in the map and print each key-value pair using a loop.

Test your implementation with the following:

val userCache: Map<Int, String> = mapOf(1 to "Alice", 2 to "Bob")
val settingCache: Map<String, Boolean> = mapOf("notifications" to true, "darkMode" to false)

val inspector = CacheInspector()
inspector.printCacheDetails(userCache)
inspector.printCacheDetails(settingCache)
Rating
0 0

There are no comments for now.

to be the first to leave a comment.