Sling Academy
Home/Kotlin/Introduction to Generics in Kotlin

Introduction to Generics in Kotlin

Last updated: November 30, 2024

Kotlin, a modern programming language, provides powerful generic types that help in creating reusable and type-safe codes. Generics allow you to define classes, interfaces, and methods with placeholder types. These placeholders are then replaced by actual types when you instantiate or call these types. This article introduces the concept of generics in Kotlin and shows how to use them effectively.

Understanding Generics

Generics are a feature that allows a class, interface, or function to be more flexible by working with any data type. They serve as a template from which classes or methods can be instantiated.

Here's a simple example of a generic class:

// A simple generic class
class Box(t: T) {
    var value = t
}

fun main() {
    val intBox = Box(1)  // Specifying T as Int
    val stringBox = Box("Hello")  // Specifying T as String
    println(intBox.value)  // Output: 1
    println(stringBox.value)  // Output: Hello
}

In this example, Box<T> is a generic class with a type parameter T. When we create instances of Box, we specify the type we want the class to use with <Int> or <String>.

Generic Functions

Kotlin also allows you to create generic functions. Here's an example:

// A function that can work with any type
fun  printItem(item: T) {
    println(item)
}

fun main() {
    printItem(1)  // Output: 1
    printItem("Kotlin")  // Output: Kotlin
    printItem(12.34)  // Output: 12.34
}

With the <T> before the function name, printItem is declared as a generic function. It can take a parameter of any type and print it.

Type Constraints

Sometimes you may want to restrict the types that can be used as parameters a little further by specifying bounds for the type parameter. You can achieve this using type constraints.

// A generic function with a type constraint
fun  add(a: T, b: T): Double {
    return a.toDouble() + b.toDouble()
}

fun main() {
    println(add(5, 10))  // Output: 15.0
    println(add(3.0, 7.0))  // Output: 10.0
    // The following would cause a compile error, as String is not a subtype of Number
    // println(add("Hello", "World"))
}

In this example, the generic type <T : Number> indicates that this function only accepts parameters of Number type or its subtypes. This provides more control over the types used in a generic function.

Variance: Invariance, Covariance, and Contravariance

Variance is an advanced concept in generics and defines how subtyping between more complex types relates to subtyping between their component types. Kotlin provides three keywords for generics variance:

  • Invariant: Kotlin’s generic types are invariant by default, which means that generic types are invariant to subtyping changes unless declared otherwise.
  • Covariant: Use the out keyword to make a generic type covariant. This means that you can substitute it with a subtype of that type.
  • Contravariant: Use the in keyword to make a generic type contravariant. This allows the utilization of the supertype property.

Example:

// An example of variance
class CoArray(private val values: Array<T>) {
    fun get(index: Int): T {
        return values[index]
    }
    // We cannot include this method as T is marked as 'out'
    // fun set(value: T) {
    //     values[0] = value
    // }
}

fun main() {
    val stringArray = CoArray(arrayOf("Kotlin", "Java"))
    val anyArray: CoArray<Any> = stringArray  // This is allowed because we used 'out' keyword
    println(anyArray.get(0))  // Output: Kotlin
}

The example demonstrates a class CoArray which is covariant. By using the out keyword, the class allows type substitution, meaning you can assign a CoArray<String> to a CoArray<Any>.

Conclusion

Generics are a powerful feature in Kotlin that increases the reusability of the code while keeping it type-safe. This article has given you an overview of how generics work and shown practical examples. Exploring generics more deeply, especially topics like variance or generic constructors, will further enable you to handle complex data structures with simplicity and robust type safety.

Next Article: How to Define Generic Classes in Kotlin

Previous Article: Kotlin Advanced Delegation: Custom Getter and Setter Logic

Series: Advanced Kotlin Features

Kotlin

You May Also Like

  • How to Use Modulo for Cyclic Arithmetic in Kotlin
  • Kotlin: Infinite Loop Detected in Code
  • Fixing Kotlin Error: Index Out of Bounds in List Access
  • Setting Up JDBC in a Kotlin Application
  • Creating a File Explorer App with Kotlin
  • How to Work with APIs in Kotlin
  • What is the `when` Expression in Kotlin?
  • Writing a Script to Rename Multiple Files Programmatically in Kotlin
  • Using Safe Calls (`?.`) to Avoid NullPointerExceptions in Kotlin
  • Chaining Safe Calls for Complex Operations in Kotlin
  • Using the Elvis Operator for Default Values in Kotlin
  • Combining Safe Calls and the Elvis Operator in Kotlin
  • When to Avoid the Null Assertion Operator (`!!`) in Kotlin
  • How to Check for Null Values with `if` Statements in Kotlin
  • Using `let` with Nullable Variables for Scoped Operations in Kotlin
  • Kotlin: How to Handle Nulls in Function Parameters
  • Returning Nullable Values from Functions in Kotlin
  • Safely Accessing Properties of Nullable Objects in Kotlin
  • How to Use `is` for Nullable Type Checking in Kotlin