1. Introduction
One of Kotlin’s most versatile abilities is that it allows us to express code syntax concisely yet powerfully to maximize developer productivity. It provides us with a powerful generics system for building reusable and type-safe code.
*By understanding Any and *, we can harness the full potential of Kotlin generics and write more robust and flexible code*.
Let’s now examine their differences and understand how to utilize them effectively.
2. Define Use Case
Let’s imagine we’re building a treat dispenser for a Halloween haunted house. This dispenser needs to handle various types of treats:
- Candy**:** Chocolate bars, lollipops, etc.
- Spooky trinkets**:** Fake spiders, vampire fangs, etc.
Using separate containers for each treat type would be cumbersome and inefficient. Instead, let’s leverage generics to create a versatile TreatDispenser
class TreatDispenser<T>(private val treats: MutableList<T> = mutableListOf()) {
fun dispenseTreat(): T? {
return if (treats.isNotEmpty()) treats.removeFirst() else null
}
fun peekNextTreat(): T? {
return treats.firstOrNull()
}
fun addTreat(treat: T) {
treats.add(treat)
}
}
With the above implementation, we now have the flexibility of creating dispensers for each category:
val candyDispenser = TreatDispenser<Candy>()
val trinketDispenser = TreatDispenser<SpookyTrinket>()
The advantages of this approach are:
- Flexibility: A single TreatDispenser class manages all treat types
- Type safety: The compiler prevents mixing different treats (no gummy bears in the potion dispenser!)
- Extensibility: Adding new treat types is simple; we’ll create a new class and use it with the TreatDispenser
3. Understanding Star Projections (*)
While our TreatDispenser
A star projection (*) essentially represents an unknown type. It allows us to work with generic types without knowing the concrete type argument – it’s a wildcard that matches any type.
Let’s introduce a function to peek at the next treat in any TreatDispenser:
fun peekAtNextTreat(dispenser: TreatDispenser<*>) {
val nextTreat = dispenser.peekNextTreat()
println("The next treat is: $nextTreat")
}
The above code has now given us the magical ability to peek into any type of TreatDispenser.
4. Understanding Any
Although star projections offer great flexibility, they aren’t always the perfect solution.
Enter Any, the root of Kotlin’s type hierarchy. Unlike *, which represents an unknown type, Any specifically represents any non-nullable type. This distinction leads to some crucial differences.
Let’s revisit our peekAtNextTreat() function, but this time, we’ll try to use Any instead of *:
fun peekAtNextTreatAny(dispenser: TreatDispenser<Any>) {
val nextTreat = dispenser.peekNextTreat()
println("The next treat is: $nextTreat")
}
Invoking the above function for our candyDispenser instance results in a type mismatch error:
peekAtNextTreatAny(candyDispenser) // Error: Type mismatch
This is because candyDispenser is of type TreatDispenser
*This highlights a key difference: Any is restrictive and only accepts non-nullable types. Hence, this function call would fail if our Candy class allowed null values (Candy?).*
5. Comparison Summary
In the below table, let’s summarize the usage of Any vs. * as per various criteria:
Criteria
Use Any?
Use *?
Is our type known and Non-Nullable?
Yes
No
Is our type unknown or potentially nullable?
No
Yes
Are we performing read-only operations on a generic type?
No
Yes
6. Conclusion
In essence, Any represents any non-nullable type, allowing for flexibility but potentially sacrificing type safety. Star projections (*), on the other hand, represent an unknown type, offering greater type safety but limiting flexibility.
To summarise, we must always choose between Any and * projections in Kotlin depending on our specific needs. Let’s remember that selecting the right tool ensures our code’s effectiveness.