Skip to main content

Command Palette

Search for a command to run...

Getting more concise with Kotlin Generics

A brief introduction to Kotlin Generics

Updated
5 min read
Getting more concise with Kotlin Generics
I

Passionate software engineer skilled in Kotlin, Android, and Ruby, with a focus on creating robust and scalable applications. Proficient in full-stack development, I excel in architecting, developing, and deploying complex software systems that deliver seamless user experiences. With expertise in Kotlin, I have successfully developed and maintained Android applications, leveraging modern frameworks and libraries to ensure high-quality software solutions. Additionally, my proficiency in Ruby enables me to build efficient web applications and contribute to the development of dynamic and interactive websites.

An Algorithmic Analogy

Consider the following scenario, you intend to create an implementation of the Selection Sort algorithm in your application.

fun selectionSort(collection: IntCollection) ...
selectionSort(arrayOf(1,2,5,9,10)) // COMPILES SUCCESSFULLY
selectionSort(arrayOf(0.1, 0.9, 20.1, 12.1)) // WILL NOT COMPILE

With the following example, you will notice that the example only allows you to sort integers independently from any other numerical value. If you think about it, does it mean that all four types of numbers supported in Kotlin; Float, Double, Long, Int, require independent implementation? Well, no, you do not have to. This is a great scenario where Kotlin generics excel.

What are Kotlin Generics?

According to Wikipedia, Generic programming is a style of computer programming in which algorithms are written in terms of data types to be specified later that are then instantiated when needed for specific types provided as parameters.

Generics are supported with the Kotlin language, they allow you to write code that can work with any type or with types that should satisfy some rules (constraints) but are not limited in any other ways.

Generics are written in angle brackets (<T>) and can be thought of as special type parameters or “placeholders” that we assign a specific type to when using generic code (calling a generic function or using a generic type). Inside generic declarations, you can use type parameters as you would use other types.

class Holder<T>(val value: T) { ... }
val isA = Holder<Boolean>(false) // explicitly indicate the type expected
val plate = Holder("plate") // types can be inferred

Constraints

Going back to our Selection Sort example; we may only want to use this sorting mechanism with a specific type of data. For instance, you definitely do not want to be trying to compare values such as * ( $ with numbers. Having a specific set of properties that a given type must have is a good way of avoiding such madness. Kotlin allows us to set upper bounds for generic types. These bounds can be regarded as the constraints of the generic type.

class Holder<T: String>(val value: T) { ... }
val isA = Holder<Boolean>(false) // WILL NOT COMPILE
val plate = Holder("plate") // type is inferred and compiles

What if I do not care?

Maybe you do not have to constrain the type of property that your generic function or class requires you can use star-projection *. This projection specifies the type as either Any? or Nothing.

fun printKeys(map: MutableMap<*, *>) { ... }

Do take note though, working with methods that actually use generic parameters is almost impossible when using star-projection. For example, MutableList<*> will have add(element: Nothing) and get(index: Int): Any? .

Types of Generic Parameters

A type parameter may be one of the following kinds:

  • An invariant type parameter (the default option with no modifier), which can be used as a type you get as output from (produce) and use as input for (consume) your generic type.

  • A covariant type parameter (modifier out), which can be used only as a type you get out from (produce) your generic type.

  • A contravariant type parameter (modifier in), which can be used only as a type you put into (consume) your generic type.

Holder<T> // invariant, can consume and produce elements
Holder<in T> // contravariant, can only consume elements
Holder<out T> // covariant, can only produce elements
Holder<*> // star-projection, does not interact with T

These parameter types belong to a feature known as “declaration-site variance” – when you specify the variance of your type parameters in declarations.

Kotlin also supports “use-site variance” – when you specify the variance of your type parameters in uses.

Star-projection” (*) also belongs to use-site variance.

Erasure

In programming languages, type erasure refers to the method used at load time to get rid of explicit type annotations from a program before it is run.

At runtime, the instances of generic types do not hold any information about their actual type arguments. The type information is said to be erased. The same byte code is used in all usages of the generic as opposed to C++, where each template is compiled separately for each type parameter provided.

  • Any MutableMap<K, V> becomes MutableMap<*, > in the runtime.

  • Any Pilot<T : Movable> becomes Pilot<Movable>.

* Actually, in the Kotlin/JVM runtime we have just java.util.Map to preserve compatibility with Java.

How about nullability?

Contrary to Kotlin conventions, by default all parameter types are nullable. In the event you want to specify a non-nullable parameter type you can set the upper-bound as Any.

/*
EXAMPLE 1
*/
class Holder<T>(val value: T) { ... } // Notice there is no `?`
val holderA: Holder<A?> = Holder(null) // T = A? and that COMPILES
/*
EXAMPLE 2
*/
class Holder<T : Any>(val value: T) { ... }
val holderA: Holder<A?> = Holder(null) // ERROR:

Conclusion

This article delves into the complexities of Kotlin generics and their significance in increasing code quality and reusability. It covers the essentials, such as type parameters and variance modifiers, which allow developers to construct type-safe code that is flexible. The article looks into generic classes and functions, showing how they can be used to create configurable data structures and reusable algorithms. The paper emphasizes the revolutionary influence of Kotlin generics by providing actual examples, helping developers to design powerful, maintainable apps.