Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
146: Star Projections in Generics
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
printCacheDetailsthat takes aMapas 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)
There are no comments for now.