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")
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")
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) //
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")
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
}
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
}
}
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
}
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
}
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
}
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
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
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]
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
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
}
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
orMap
and provide type safety.
val intList: List<Int> = listOf(1, 2, 3)
val stringList: List<String> = listOf("one", "two", "three")
- 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
}
- 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
}
- 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()
}
How to Write Clean and Maintainable Generic Code
- Unobtrusive Type Parameter Names: Give type parameters meaningful names.
T
: TypeE
or ElementK
: KeyV
: Value and so on… In more complicated situations, use descriptive names.
class Repository<T>
class Pair<K, V>
- 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()
}
- Flexibility in APIs via Wildcards: Use wildcards to make your APIs less complex. Use
in
andout
variances to describe producer-consumer relationships instead.
fun copy(from: List<out Any>, to: MutableList<Any>) {
for (item in from) {
to.add(item)
}
}
- 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)
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)
}
- 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
}
- 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)