Generics in Kotlin
Basic Introduction to Generics and Types in kotlin programming Language
Photo by eswaran arulkumar on Unsplash
Introduction
In the world of programming, generics provide a powerful way to write code that is not only flexible but also type-safe. We will explore the concept of generics in Kotlin, cover the basics, dive into practical use cases, and discuss best practices. As we delve into this topic, you'll discover how generics in Kotlin can be used to craft elegant solutions that adapt to a wide array of data types. From the fundamental principles of declaring and using generics to advanced topics like generic variance, this blog post will serve as your guide to mastering this pivotal aspect of Kotlin generics.
Kotlin Type System
Before going in-depth in generic it is important to have an overview of kotlin type systems since kotlin generic deals with type parameters.
As you can see in the above diagram the Any? type is a superclass for all types and it is nullable meaning that you can pass in null values to it. The Any is a subtype of Any? and it is not nullable. Nothing and Nothing? are both subtypes of all types.
What is Generics?
Generics are a way to create classes, interfaces, and functions that can work with different data types while maintaining type safety. In Kotlin, generics are implemented through type parameters. This allows you to write code that can operate on a variety of data types without sacrificing compile-time type checking.
Basics of Generics
Declaring Generics
In Kotlin, you declare generics by using angle brackets <>
after the class or function name, followed by the type parameter. For example:
class MyClass<T>(value: T)
Where T is a type parameter that can represent any data type because it is of type Any? .
we can also have a generic function:
fun <T> sum(Item:T): List<T>{TODO()}
Using Generics
Example 1
class MyClass<T:Any>(genericValue: T)
{ var value = genericValue }
fun main() { var myType_0 : MyClass = MyClass(1) var myType_1 = MyClass("Hello")
//Both myType_0 and myType_1 use the MyClass and the generic parameter T
println("myType_0 = ${myType_0.value} ")
println("myType_1 is = ${myType_1.value}")
}
Code Summary
Variance in kotlin
To fully understand how generic types work you have to understand what variances mean.
What is Variance:
Variance tells you the relationship between two objects with the same base class but different type parameters. it refers to how subtyping relationships between generic types are maintained. Variance is important when working with generic classes or interfaces, as it defines whether subtypes of a generic type are considered subtypes of the parameterized type.
Example
var list1: List<String?> = listOf("A", "B", "C")
var list2: List = listOf("X", "Y", "z")
list1=list2 //This will be correct
list2=list1 //This will be incorrect
Code Summary
Variance refers to how a generic class relate to other forms of generic class, it focuses on the relationship of List<String?> and List<String>, and not on the relationship of String? and String.
Type of Variance
Kotlin has three types of variance: Invariant, Covariant, and Contravariant
Invariant:
For invariant relationships, when carrying out an operation between two or more types all types must be of exactly the same types. This means that Subtyping preservation or Supertyping (Inverts of Subtyping) preservation is not allowed. Therefore if "Bird" is a subtype of "Animal", we can't handle LIst<Bird> as a subtype of List<Animal> or vice-versa, only List<Animal> and List<Animal> or LIst<Bird> and List<Bird> can be handled as same types in an Invariant relation.
Covariant:
For covariant relationships subtyping is preserved(it ensures that the behaviour of a program remains consistent when you replace an instance of a supertype with an instance of a subtype.). The Liskov substitution principle ("L" in SOLID principle ) states that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application, simply meaning that the Variance between a Superclass and a Subclass should be covariant in their behaviour. Therefore if "Bird" is a subtype of "Animal", than List<Bird> will be seen as a subtype of List<Animal>.A good example of this can be given for:
Example:
var numberList1: List<Number> = listOf(1, 2.0, 3.5, 4.8)
var intList2: List<Int> = listOf(9, 7, 6, 8)
numberList1=intList2//For a Covariant relationship this code should run
//OR
intList2=numberList1//this should not run if the relationship is Covariant
Contravariant:
Contravariant relationships between two types is the invert of Covariant what I may refer to as SuperTyping(Subtyping is not preserved). Therefore if "Bird" is a subtype of "Animal", then List<Animal> will be seen as a subtype of List<Bird>.
Creating your Generic Classes with given Variance behaviour
To declare your generic classes with a given set of variance there are two main Kotlin keywords needed, "out" Covariant for and "in" for Contravariant, for Invariant declaration, there are no keywords needed because generic classes defined in Kotlin are invariant by default.
out
out-types are type parameters that only occur in returning values of a function(Producer types) or on val properties. out-types can not be passed as function parameters, the out-type in Kotlin helps the compiler understand the variance of the generic class is Covariant and with that, the compiler can ensure the type safety of your code by making sure that the out-type is only used as a returning type.
in
in types are type parameters that only occur as function arguments(Consumer types), they can not occur as returning types of a function. The in-type declaration makes a generic type Contravariant.
Full code Example Explaining all the variance:
open class Animal()
open class Bird: Animal()
class Chicken: Bird()
class Invariant<T>{
fun invariant(t:T):T{
TODO()}
}
//out keyword is used making it Covariant
class Covariant<out T>{
fun covariant():T{
TODO()}
}
//in keyword is used making it Contravariant
class Contravariant<in T>{
fun contravariant(t:T){
TODO()}
}
//Main Function
fun main(args: Array<String>) {
//The Invariant Class can be seen as both a Consumer and Producer of the Generic type T
val type1:Invariant<Animal> = Invariant<Animal>()
val type2:Invariant<Bird> = Invariant<Animal>()//Must be of the exact type since T can be produced and consumed
val type3:Invariant<Chicken> = Invariant<Animal>()//Must be of the exact type since T can be produced and consumed
val type4:Invariant<Animal> = Invariant<Bird>()//Must be of the exact type since T can be produced and consumed
//The Covariant class is a Producer of T generic Type
val type5:Covariant<Animal> = Covariant<Animal>()
val type6:Covariant<Bird> = Covariant<Animal>()//Will not work since Animal is a SuperClass(Interface in this case) of String
val type7:Covariant<Animal> = Covariant<Bird>()//Subtyping is preserved
val type8:Covariant<Animal> = Covariant<Chicken>()//Subtyping is preserved
//The Contravariant class is a Consumer of T generic Type
val type9:Contravariant<Animal> = Contravariant<Animal>()
val type10:Contravariant<Bird> = Contravariant<Animal>()
val type11:Contravariant<Chicken> = Contravariant<Animal>()
val type12:Contravariant<Animal> = Contravariant<Bird>()//Since it is only allowed to consume than Subtyping is not allowed
}
Advantages of Generics
Type Safety: Compile-time type checking ensures that you don't mix incompatible types.
Code Reusability: You can write generic classes and functions that work with various data types.
Cleaner Code: Generics can help reduce code duplication.
Conclusion
In this blog post, we've covered the basics of generics, Kotlin type System, Variance and coding samples to help you get started with this essential Kotlin feature.
Finally, Generics in Kotlin are a powerful feature that enhances code reusability, type safety, and overall maintainability. With this knowledge, you're now prepared to harness the full potential of generics in Kotlin, enhancing your ability to craft resilient, adaptable, and type-safe software.