DEV Community

Bugfender
Bugfender

Posted on • Originally published at bugfender.com on

Understanding Kotlin Generics: A Complete Guide for Developers

Introduction to Kotlin Generics

Kotlin Generics are a way to use generics in Kotlin that have type parameters specified to their usage. This powerful tool defines code components so that they will work with any data type in a flexible and reusable manner – and the main advantage of Kotlin Generics is how they are statically-typed.

Kotlin Generics allow you to use three types of parameters – classes, interfaces and methods – which means it’s possible to write code to take any data type and still get compile-time type safety!

Generics help us make type-safe and reusable operations on different types of data.

The benefits of using Generics in Kotlin

i) Generics offer type-safety at the time of compiling the program, meaning we can catch the errors in compile-time rather than runtime.

A List in Kotlin contains objects of any type. This means mixing it up will mostly lead to runtime errors, and if our code is not written to handle such a case, it will crash the application.

val stringLists: List<String> = listOf("Hola", "there")

Enter fullscreen mode Exit fullscreen mode

ii) Generics assist in removing the need to do explicit type-casting, which directly contributes to much cleaner code and improved safety against ClassCastException.

// Without use of generics 
val listObj = listOf(1, 2, 3, 4) as List<Any>

// With the use of generics
val listObj: List<String> = listOf("Hola", "there")

Enter fullscreen mode Exit fullscreen mode

iii) Generics directly contribute to the reusability of code where it’s possible to define the types and functions that can be reused anywhere in the project. You do not need to write the same code again and again for completing the same action.

class Boxx<T>(var value: T) // A generic Boxx class can take any input type and can server the different purpose. 

val intBoxx = Boxx(10) // 

Enter fullscreen mode Exit fullscreen mode

iv) Generics help with writing more flexible and reusable API code. These are used in the libraries and frameworks to create broader, type-safe interfaces.

// Creating a listOf the string items. 
val stringLists: List<String> = listOf("Amma", "Boby", "Charls")
// Mapping of the items.
val mapItem: Map<Int, String> = mapOf(1 to "One", 2 to "Two", 3 to "Three") 

Enter fullscreen mode Exit fullscreen mode

Generic Classes and Interfaces

With Kotlin Generics, you can write the classes and interfaces with type parameters. These type parameters serve as placeholders for an actual type that will be passed to the class or interface when a copy with concrete classes is created.

Generics enable an interface to operate on objects of any type. These are really just placeholders for types; a concrete type will be substituted when the class or interface is being instantiated.

Generic Classes

A generic class is a class that can work with any given type. Type parameter in angle brackets <> and used within the class, as shown.

class Box<T>(var value: T)

fun main() {
    val intBox = Box(10)
    val stringBox = Box("Hello")

    println(intBox.value) // Outputs: 10
    println(stringBox.value) // Outputs: Hello
}

Enter fullscreen mode Exit fullscreen mode

Generic Interfaces

Generic interfaces also have some similarity to a generic method or a generic function, wherein they mandate a better type of parameter for the generic Interface, and can be implemented with specific types by class.

interface Repository<T> {
    fun getById(id: Int): T
    fun getAll(): List<T>
}

class UserRepository : Repository<User> {
    override fun getById(id: Int): User {
        // Implementation here
    }

    override fun getAll(): List<User> {
        // Implementation here
    }
}

Enter fullscreen mode Exit fullscreen mode

Generic Methods

Generic methods allow you to define the methods with their own type parameters. These generic type parameters can be used within the method to ensure the type safety.

fun <T> printArray(array: Array<T>) {
    for (element in array) {
        println(element)
    }
}

fun main() {
    val intArray = arrayOf(1, 2, 3, 4, 5)
    val stringArray = arrayOf("one", "two", "three")

    printArray(intArray) // Outputs: 1 2 3 4 5
    printArray(stringArray) // Outputs: one two three
}

Enter fullscreen mode Exit fullscreen mode

Bounded Type Parameters

Bounded type parameters allow you to restrict the generic types that can be used as type arguments. This can be helpful when we enforce some capabilities or behaviors on the types used with generics.

Upper Bounds

We can also specify the upper bound for these type parameters, using the extends keyword (in Java) or the : symbol (in Kotlin). This requires the type parameter to be a subtype of that specific class.

fun <T : Number> add(a: T, b: T): Double {
    return a.toDouble() + b.toDouble()
}

fun main() {
    println(add(5, 10)) // Outputs: 15.0
    println(add(5.5, 10.1)) // Outputs: 15.6
    // println(add("5", "10")) // Compile-time error: argument not within its bound
}

Enter fullscreen mode Exit fullscreen mode

Multiple Bounds

We could also specify multiple bounds for a type parameter, requiring the type argument to be a subtype of all specified types.

interface Printable {
    fun print()
}

fun <T> printIfPrintable(item: T) where T : Number, T : Printable {
    item.print()
}

class PrintableNumber(val number: Int) : Number(), Printable {
    override fun print() {
        println(number)
    }

    // Other abstract method of Number need to be implemented
    override fun toByte(): Byte = number.toByte()
    override fun toChar(): Char = number.toChar()
    override fun toDouble(): Double = number.toDouble()
    override fun toFloat(): Float = number.toFloat()
    override fun toInt(): Int = number
    override fun toLong(): Long = number.toLong()
    override fun toShort(): Short = number.toShort()
}

fun main() {
    val printableNumber = PrintableNumber(100)
    printIfPrintable(printableNumber) // Outputs: 100
}

Enter fullscreen mode Exit fullscreen mode

Advanced In Generics

Wildcards in Generics

Generics provide a way to specify a type parameter that allows many different types of objects to be passed in. Wildcards are represented by a question mark symbol ? and may indicate unknown types.

Unbounded Wildcards example:

fun printList(list: List<*>) {
    for (item in list) {
        println(item)
    }
}

// Usage
val intList: List<Int> = listOf(1, 2, 3)
val stringList: List<String> = listOf("A", "B", "C")

printList(intList) // Outputs: 1 2 3
printList(stringList) // Outputs: A B C

Enter fullscreen mode Exit fullscreen mode

Upper-bounded wildcard (? extends T) specifies a type parameter that should be a sub-type of a specific type.

fun sumOfNumbers(list: List<out Number>): Double {
    var sum = 0.0
    for (number in list) {
        sum += number.toDouble()
    }
    return sum
}

// Usage
val intList: List<Int> = listOf(1, 2, 3)
val doubleList: List<Double> = listOf(1.1, 2.2, 3.3)

println(sumOfNumbers(intList)) // Outputs: 6.0
println(sumOfNumbers(doubleList)) // Outputs: 6.6

Enter fullscreen mode Exit fullscreen mode

A lower-bounded wildcard (? super T) specifies a type parameter that should be a super-type of a specific type.

fun addNumbers(list: MutableList<in Number>) {
    list.add(1)
    list.add(1.1)
}

// Usage
val numberList: MutableList<Number> = mutableListOf(1, 2, 3)
addNumbers(numberList)
println(numberList) // Outputs: [1, 2, 3, 1, 1.1]

Enter fullscreen mode Exit fullscreen mode

Type Inference & Type Erasure

Type inference allows a compiler of type system generics to deduce the type, such as in cases where it can be determined from context what the parameterized function or class should be.

fun <T> singletonList(item: T): List<T> {
    return listOf(item)
}

// Usage
val intList = singletonList(1) // Compiler infers T as Int
val stringList = singletonList("Hello") // Compiler infers T as String

Enter fullscreen mode Exit fullscreen mode

Type erasure is the removal, by a compiler, of redundant type information at runtime, because we already have guaranteed type-safety.

// Example of type erasure
class Node<T>(val value: T) {
    fun getValue(): T = value
}

fun main() {
    val intNode = Node(1)
    val stringNode = Node("Hello")

    println(intNode.getValue()) // Outputs: 1
    println(stringNode.getValue()) // Outputs: Hello
}

Enter fullscreen mode Exit fullscreen mode

Best Practices for Using Generics in Android Development

When to Use Generics

In Android development, generics should be used whenever you have to write a rider of reuse code that works with multiple types.

  • Kotlin Collections and Data Structures : Use generics to avoid casting while dealing with Kotlin collections like List, Set or Map and provide type safety.
val intList: List<Int> = listOf(1, 2, 3)
val stringList: List<String> = listOf("one", "two", "three")

Enter fullscreen mode Exit fullscreen mode
  • Adapters and View Holders : Create a universal adapter for recycler view to reuse the same logic while handling different kinds of data.
class GenericAdapter<T>(
    private val items: List<T>,
    private val bindView: (item: T, view: View) -> Unit
) : RecyclerView.Adapter<GenericAdapter.GenericViewHolder<T>>() {
    // Implementation
}

Enter fullscreen mode Exit fullscreen mode
  • Repositories and Data Access Objects (DAOs): Use generics to implement a repository pattern, which is even applicable for a wide range of data entities.
interface BaseDao<T> {
    fun insert(entity: T)fun update(entity: T)fun delete(entity: T)fun getById(id: Int): T
}

Enter fullscreen mode Exit fullscreen mode
  • Utility Methods : Write these as generic functions that you can use on any type.
fun <T> convertToList(vararg items: T): List<T> {
    return items.toList()
}

Enter fullscreen mode Exit fullscreen mode

How to Write Clean and Maintainable Generic Code

  • Unobtrusive Type Parameter Names: Give type parameters meaningful names. T : Type E or Element K : Key V: Value and so on… In more complicated situations, use descriptive names.
class Repository<T>
class Pair<K, V>

Enter fullscreen mode Exit fullscreen mode
  • All of these are examples of type bounds – bounded types that ensure the necessary properties and methods for what we want to do with them.
fun <T : Comparable<T>> findMax(items: List<T>): T {
    return items.maxOrNull() ?: throw NoSuchElementException()
}

Enter fullscreen mode Exit fullscreen mode
  • Flexibility in APIs via Wildcards: Use wildcards to make your APIs less complex. Use in and out variances to describe producer-consumer relationships instead.
fun copy(from: List<out Any>, to: MutableList<Any>) {
    for (item in from) {
        to.add(item)
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Overuse Generics: Do not use generics for something that can be done normally. Overuse of generics can complicate the code and lead to a reduced level of readability.
  • Documentation – Document your generic classes and methods to explain how they are supposed to be used.
/*
Give the full explanation of this Generic here, so anyone who works on your project later on should understand the purpose of using it.
*/
class Box<T>(var value: T)

Enter fullscreen mode Exit fullscreen mode

Common Pitfalls and How to Avoid Them

  • Type Erasure: keep in mind that generic type parameters are erased at runtime so you cannot use them for type checks or instance creation.
fun <T> isInstanceOf(value: Any, clazz: Class<T>): Boolean {
    return clazz.isInstance(value)
}

Enter fullscreen mode Exit fullscreen mode
  • Unchecked Casts – Be sure to use these when you know for a fact that it is type-safe, and don’t suppress the warnings!
@Suppress("UNCHECKED_CAST")
fun <T> cast(value: Any): T {
    return value as T
}

Enter fullscreen mode Exit fullscreen mode
  • Complex Generics: Too many little corners of your generics will 8 percent-ify them, so remember to keep it simple. Keep the generics simple, flat and avoid nesting KeyValuePair if possible to avoid complexity.
  • Types should be consistent within a codebase: When using generics, or any type parameter, your aim is to accurately represent what the object is, and do so consistently throughout your application. Don’t use aliases for a type parameter in different places within the code.

Conclusion

Kotlin Generics is a de facto way in which Kotlin provides flexibility, reusability, and type-safety for your code. Generics allow us to define classes, interfaces and methods with type parameters that don’t exist at runtime (it is only used in compile-time).

Generics can catch, for example, typing errors during the compilation process, making code maintenance easier, eliminating some explicit cast between types, and making it possible to write more robust and maintainable code.

Generics are critical in helping with your design of APIs and allow you to do type-checks across the board. This in turn allows you to create safer, more flexible components in your Android projects, resulting in better application performance and an improved development experience. If fully embraced, generics will take your code to a whole new level and massively improve the quality of Android apps that you build. Happy Coding!

Top comments (0)