In our earlier post, we talked about the Generics in Kotlin at a basic Level, If you missed it, I will suggest you can go ahead and read it through here.
In this blog post, we will be covering Invariance, Co-variance and Contra-variance and how these help us to code in Kotlin. At this time, It is very imperative that we are clear with the terms like Super class, Sub class and Sub type.
Let’s take this simple example:
Implementation 1:
open class Color(val color: String) { fun printColorName() { println("Color is $color") } } class Black(blackColor: String) : Color(blackColor) { } class Red(redColor: String) : Color(redColor) { }
In the above example, Color is my Superclass for the classes Black and Red and just for a check, Any is a superclass for Color and any other class defined in Kotlin. Black and Red are subclasses of Color class.
When we talk of subtypes, we generally refer it to the properties and behavior associated with the object of that class. By this definition, I may infer that Color class will have a single subtype, the instance given by its own self, but here is the twist.
It is not single but it will have two subType. One given by self and one inherited from Any class. For Black and Red class, they will have 3 subtypes, one from itself, one from Color class and one from Any class.
Let’s look at the below code and try to clear some more doubts:
Implementation 2:
fun main(){ val color: Color = Red("343434") }
If you will compile above code, it succeeds, that means we are able to assign a child class instance where a Super class instance was required. In OOPs Paradigm, it is possible because of the Liskov Substitution Principle, which emphasizes on Open/Closed Principle for a class.
Let’s look at another implementation:
Implementation 3:
val redArray: Array<Red> = arrayOf(Red("#FF0000"), Red(" #B22222")); //val colorArray: Array<Color> = redArray [will not compile, why?]
So, this is the tricky part that the second line will not compile, however for a fact we know that Color is the Superclass of the Red Class and our implementation 2(based on the above fact) worked like a charm. Before pulling off the curtains over this suspense let’s look into one more example:
Implementation 4:
val redList: List<Red> = listOf(Red("#FF00FF"),Red("#B22222")); val colorList: List<Color> =redList [Compiles successfully]
Awesome, Our code just compiled! Why? So, the reason is that the Arrays in Kotlin are by default defined as In-variant and Lists are defined as Co-variant and that is the reason our Implementation 3 failed and Implementation 4 succeeds.
When we are working with Generics in Kotlin, we always have to remember that by default there is no relationship between the types until defined explicitly i.e the relationship is In-variant. In kotlin, we use two keywords to develop a relationship which can be co-variant or contra-variant. Let’s have a look at each one of it:
- IN (To make the relationship contra-variant)
- OUT ( To make the relationship co-variant)
Let’s have a look at Co-variant(Out) type relationship:
Implementation 5:
class School<T> { } open class Department(val departmentName: String, val count: Int, val totalSalary: Long) { fun EmployeeCount() { println("Employee count in Department $departmentName is $count") } fun Salary() { println("Total salary for Employee in Department $departmentName is $totalSalary") } } class FinanceDepartment(departmentName: String, count: Int, totalSalary: Long) : Department(departmentName, count, totalSalary) { } class TeachingDepartment(departmentName: String, count: Int, totalSalary: Long) : Department(departmentName, count, totalSalary) { }
Let’s look at the main function for above classes:
Implementation 6:
fun main() { val financeDepartment: FinanceDepartment = FinanceDepartment("Finance", 5, 2000) val teachingDepartment: TeachingDepartment = TeachingDepartment("Teaching", 3, 1900) // val school: School<Department> = School<FinanceDepartment>() [This code will not compile] }
You might be aware now, why the above code, the last line in the Implementation 6 will not compile? Yes, because right now our Generic class School does not have any type of relationship i.e it has an invariant relationship. To make it co-variant let’s modify our implementation 5 first
class School<out T> { } open class Department(val departmentName: String, val count: Int, val totalSalary: Long) { fun EmployeeCount() { println("Employee count in Department $departmentName is $count") } fun Salary() { println("Total salary for Employee in Department $departmentName is $totalSalary") } } class FinanceDepartment(departmentName: String, count: Int, totalSalary: Long) : Department(departmentName, count, totalSalary) { } class TeachingDepartment(departmentName: String, count: Int, totalSalary: Long) : Department(departmentName, count, totalSalary) { }
So, we just added out keyword when compared with Implementation 5, rest everything remains the same. So with our change, we have defined a relationship where School<FinanceDepartment> has become subtype of School<Department>
So the following code works fine now:
fun main() { val financeDepartment: FinanceDepartment = FinanceDepartment("Finance", 5, 2000) val teachingDepartment: TeachingDepartment = TeachingDepartment("Teaching", 3, 1900) val school: School<Department> = School<FinanceDepartment>() //[This compiles successfully] }
At the same time, if we go back and have a look at Implementation 4, it is the very same reason that the List in Kotlin has been defined with out keyword hence a co-variant relationship is shared. Let’s have a quick sneak peek into List implementation as well:
public interface List<out E> : Collection<E> { override val size: Int override fun isEmpty(): Boolean override fun contains(element: @UnsafeVariance E): Boolean override fun iterator(): Iterator<E> override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean public operator fun get(index: Int): E public fun indexOf(element: @UnsafeVariance E): Int public fun lastIndexOf(element: @UnsafeVariance E): Int public fun listIterator(): ListIterator<E> public fun listIterator(index: Int): ListIterator<E> public fun subList(fromIndex: Int, toIndex: Int): List<E> }
Now, let’s have a look at the contra-variance ( In ) relationship. Firstly, let’s go through the following a set of lines:
fun main() { val financeDepartment: FinanceDepartment = FinanceDepartment("Finance", 5, 2000) val teachingDepartment: TeachingDepartment = TeachingDepartment("Teaching", 3, 1900) //val school: School<FinanceDepartment> = School<Department>()[This will not work because we are trying to assign super type to a subtype }
In order to make the above code work, we will need to add the out keyword definition to our School definition. So let’s modify our implementation 5:
class School<in T> { } open class Department(val departmentName: String, val count: Int, val totalSalary: Long) { fun EmployeeCount() { println("Employee count in Department $departmentName is $count") } fun Salary() { println("Total salary for Employee in Department $departmentName is $totalSalary") } } class FinanceDepartment(departmentName: String, count: Int, totalSalary: Long) : Department(departmentName, count, totalSalary) { } class TeachingDepartment(departmentName: String, count: Int, totalSalary: Long) : Department(departmentName, count, totalSalary) { }
After modifying the above code, we will be able to execute the following set of lines successfully:
fun main() { val financeDepartment: FinanceDepartment = FinanceDepartment("Finance", 5, 2000) val teachingDepartment: TeachingDepartment = TeachingDepartment("Teaching", 3, 1900) val school: School<FinanceDepartment> = School<Department>()//[Works like a charm] }
So, finally we can draw a conclusion from the following simple diagram, when you want to assign a subtype to a super type i.e make a relationship co-variant use IN Keyword and when you want to assign a super type to a subtype i.e make the relationship a contra-variant use OUT keyword.
That’s all for this post, until the next one, stay tuned…
Very nice blog in generic,very well explained.