Kotlin’s Underscore (_) Operator for Type Arguments: The Hidden Gem of Generics

Table of Contents

Kotlin is packed with features that make it both powerful and expressive. One such hidden gem is the underscore (_) operator for type arguments in Kotlin. While it may not be as widely known as other features, it provides a concise and efficient way to work with generics.

In this post, we’ll explore what this operator does, why it’s useful, and how you can leverage it to simplify your Kotlin code.

What is the Underscore (_) Operator for Type Arguments in Kotlin?

In Kotlin, when working with generics, you often have to specify the exact type argument. However, sometimes you just want Kotlin to infer the type for you without explicitly providing it. The underscore (_) operator allows you to do just that—it acts as a placeholder for type arguments.

The syntax looks like this:

Kotlin
val list: List<_> = listOf(1, 2, 3)

Here, instead of specifying List<Int>, we use _, and Kotlin automatically infers that the list contains integers.

Understanding Underscore ( _ ) Operator for type arguments with Example

The underscore operator _ in Kotlin is a type inference placeholder that allows the Kotlin compiler to automatically infer the type of an argument based on the context and other explicitly specified types.

Kotlin
abstract class SomeClass<T> {
    abstract fun execute() : T
}

class SomeImplementation : SomeClass<String>() {
    override fun execute(): String = "Test"
}

class OtherImplementation : SomeClass<Int>() {
    override fun execute(): Int = 42
}

object Runner {
    inline fun <reified S: SomeClass<T>, T> run() : T {
        return S::class.java.getDeclaredConstructor().newInstance().execute()
    }
}

fun main() {
    // T is inferred as String because SomeImplementation derives from SomeClass<String>
    val s = Runner.run<SomeImplementation, _>()
    assert(s == "Test")
    // T is inferred as Int because OtherImplementation derives from SomeClass<Int>
    val n = Runner.run<OtherImplementation, _>()
    assert(n == 42)
}

Don’t worry! Let’s break down the code step by step:

  1. In this code, we have an abstract class called SomeClass with a generic type T. It declares an abstract function execute() that returns an object of type T.
  2. We have a class called SomeImplementation which extends SomeClass and specifies the generic type as String. It overrides the execute() function and returns the string value "Test".
  3. Similarly, we have another class called OtherImplementation which extends SomeClass and specifies the generic type as Int. It overrides the execute() function and returns the integer value 42.
  4. Below that, we have an object called Runner with a function run(). This function is generic and has two type parameters S and T. It uses the reified keyword to access the type information at runtime. Inside the function, it creates an instance of the specified class S using reflection (getDeclaredConstructor().newInstance()) and calls the execute() function on it, returning the result of type T.

In the above code, the underscore operator is used in the main() function when calling the Runner.run() function. Let’s take a closer look:

Kotlin
val s = Runner.run<SomeImplementation, _>()

In this line, the type parameter T is explicitly specified as _ for the Runner.run() function. Here, _ acts as a placeholder for the type to be inferred by the compiler. Since SomeImplementation derives from SomeClass<String>, the compiler infers T as String for this invocation. Therefore, the variable s is inferred to be of type String, and the Runner.run() function returns the result of executing SomeImplementation, which is the string "Test".

Kotlin
val n = Runner.run<OtherImplementation, _>()

Similarly, in this line, the type parameter T is specified as _ for the Runner.run() function. Since OtherImplementation derives from SomeClass<Int>, the compiler infers T as Int for this invocation. Consequently, the variable n is inferred to be of type Int, and the Runner.run() function returns the result of executing OtherImplementation, which is the integer 42.

By using the underscore operator _ as a type argument, the compiler can automatically infer the appropriate type based on the context and the explicitly specified types.

Why Use the Underscore (_) Operator for Type Arguments in Kotlin?

Using the underscore operator has several benefits:

  1. Simplifies Type Declarations — You don’t have to explicitly specify type arguments when they can be inferred.
  2. Improves Code Readability — It makes code cleaner and easier to read, especially when dealing with complex generics.
  3. Reduces Boilerplate Code — Less repetitive type annotations mean more concise code.
  4. Works with Generic Functions — It allows you to call generic functions without explicitly passing type arguments.

How to Use the Underscore (_) Operator in Kotlin

1. Using _ with Generic Functions

Kotlin lets you use _ when calling generic functions. Suppose you have a function that takes a generic type parameter:

Kotlin
fun <T> printType(value: T) {
    println("Type: ${value::class.simpleName}")
}

You can call this function without explicitly specifying the type argument:

Kotlin
printType<_>(42) // Kotlin infers the type as Int

2. Using _ with Collections

The underscore operator works well with collections, making them more flexible when type inference is possible.

Kotlin
val numbers: List<_> = listOf(10, 20, 30)
println(numbers) // Output: [10, 20, 30]

Here, Kotlin automatically infers that numbers is a List<Int>.

3. _ in Function Returns

If a function returns a generic type, _ allows Kotlin to infer it without explicit declaration.

Kotlin
fun getList(): List<_> = listOf("Kotlin", "Java", "Swift")

Kotlin infers that getList() returns a List<String> without us specifying it explicitly.

When to Avoid Using the Underscore (_) Operator in Kotlin

While the underscore operator is useful, there are situations where avoiding it can improve code clarity and maintainability:

  • When Type Inference Fails — If Kotlin cannot determine the type of a variable or lambda parameter, using _ will result in a compilation error.
  • In Public APIs — Overusing _ in public functions can make the API less clear, potentially confusing users who rely on explicit parameter names for readability.
  • When Explicit Types Improve Readability — In complex expressions or function signatures, explicitly defining types can enhance code comprehension and maintainability.

Conclusion

The underscore (_) operator for type arguments in Kotlin is a simple yet powerful tool that helps reduce boilerplate code and improve readability. Whether you’re working with collections, or generic functions, it can make your code cleaner and more concise.

Next time you’re dealing with generics, give _ a try and let Kotlin do the heavy lifting for you..!

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!