Baeldung Pro – Kotlin – NPI EA (cat = Baeldung on Kotlin)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

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<T>:

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<T>  works wonders for managing specific treat types, let’s imagine a scenario where we need a function to inspect the next treat in any dispenser, regardless of its type. This is where star projections (*) come into play.

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<Candy>, and Candy isn’t Any. Even though Candy inherits from Any, the compiler enforces strict type matching when using Any in generics.

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.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
1 Comment
Oldest
Newest
Inline Feedbacks
View all comments