Generics in Kotlin vs Java
They are a lot similar to those in the Java language, but the Kotlin language authors added unique keywords like ‘out’ and ‘in’ to make them more natural and intelligible. Let's see an example of Kotlin Generics.
Code:
class myClass<A>(private val ans: A) {
fun getAns(): A {
return ans
}
}
Need of Generics in Kotlin
Let us observe the actual need of the generics with the example given below.
Code:
class myClass(txt: String) {
var ans = txt
init{
println(ans)
}
}
fun main(args: Array<String>){
var employee_name: myClass = myClass("Coding Ninjas")
var employee_salary: myClass = myClass(50000) // code crashes here
}
In this example, we are creating a myClass class with the primary constructor having a single parameter. Now, we try to pass the different types of data in the object of myClass class as String and Integer. The primary constructor of myClass class accepts string type ("Coding Ninjas") as the variable employee_name but gives compile-time error when passes Integer type (50000) as the variable employee_salary.
Output:
The integer literal does not conform to the expected type String
To fix the problem, we utilize a generic type class, which is a user-defined class that accepts a variety of parameter types in a single class.
Let's modify the above example with generic types. A class myClass of type T> is a general-purpose type class that accepts both Int and String parameters.
In other words, the type parameter T> serves as a placeholder for the type argument. When the generic type is instantiated, it will be replaced.
Modified Input:
class myClass<T> (txt: T) {
var ans = txt
init{
println(ans)
}
}
fun main(args: Array<String>){
var employee_name: myClass<String> = myClass<String>("Coding Ninjas")
var employee_salary: myClass<Int> = myClass<Int>(50000)
}
Output:
Coding Ninjas
50000
Variance
Arrays are invariant by default in Kotlin, unlike Java. Generic types, by extension, are invariant in Kotlin. The out and in keywords can help with this. The property of invariance is that a standard generic function/class that has already been created for a certain data type cannot take or return another data type. All additional data types are supertypes of Any.
We have two types of Variance:
Using in and out(Declaration-site variance), Use-site variance(Type projection)
Kotlin in, out and where keywords
Let us see how to use in, out and where keywords with the examples below.
The out Keyword:
The 'out' keyword on the generic type in Kotlin allows us to allocate this reference to any of its supertypes. The supplied class can only produce the out value; it cannot be consumed.
The out modifier is known as a variance annotation, and it offers declaration-site variance because it is provided at the type parameter declaration site.
Here is the basic syntax for using out.
Code:
class myOutClass<out T>(val value: T) {
fun getAns(): T {
return ans } }
Here, We've created a class called myOutClass that can generate T values. Then, for the reference that is a supertype of it, we can assign an instance of the OutClass like this.
Syntax:
val out = myOutClass("Int")
val ref: myOutClass<Any> = out
Wonder what would happen if we did not use the out keyword here?
If the out type was not used in the above class, the following set of statements would result in a compiler error.
The in Keyword:
In addition to out, Kotlin has an additional variance annotation called in. It makes a type parameter contravariant, which means it can only be consumed rather than produced.
Here is an example of the in keyword.
Code:
class myInClass<in T> {
fun findAns(myVal: T): String {
return myVal.toString()
}
}
Note that so far, we've declared a findAns() method that will only accept values of type T. Then, we can give the reference of its subtype – Int: a reference of type Number.
val myInClassObj: myInClass<Number> = myInClass()
val ref<Int> = myInClassObj
Similarly here, just like out keyword, If the in type is not utilized in the aforementioned class, the following sentence will result in a compiler error.
The where Keyword:
The default upper bound (if there was none specified) is Any?. Only one upper bound can be specified inside the angle brackets. If the same type parameter needs more than one upper bound, you need a separate where-clause:
Any? is the default upper bound (if none was given). Within the angle brackets, only one upper bound can be provided. A separate where-clause is required if the same type parameter requires more than one upper bound.
Have a look over the syntax with where clause too.
Code:
fun <T> findAns(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { x > threshold }.map { x.toString() }
}
Here, The passed type must meet all of the where clause's conditions at the same time. The T type must implement both CharSequence and Comparable in the example above.
Covariance
It indicates that replacing/substituting subtypes is permissible but not supertypes, i.e., a generic function/class created for Number can accept Int, while a generic class defined for Int cannot accept Number.
The out keyword in Kotlin can be used to accomplish this:
val x: myClass<Any> = myClass<Int>()
This will give a type mismatch error.
val x: myClass<out Any> = myClass<String>()
This will work since String is a subtype of Any.
val x: myClass<out String> = myClass<Any>()
This will not work since Any is superType of String.
By appending the out keyword to the declaration site, we may immediately allow covariance. So this will do the work.
Code:
fun main(args: Array<String>) {
val x: myClass<Any> = myClass<String>()
}
class myClass<out T>
Type projections
It is feasible to copy all the members in an array of some type into an array of any type, but we must annotate the input argument with the out keyword to allow the compiler to compile our code. As a result, the compiler deduces that the input argument can be of any type that is a subtype of the Any type.
Let us see this with an example where we will be copying the elements of one array into another one.
Code:
fun myCopyClass(from: Array<out Any>, to: Array<Any>) {
for (i in from.indices) // copy content from one array to another
to[i] = from[i]
for (i in to.indices) { // printing content of new array
println(to[i])
}
}
fun main() {
val x: Array<Int> = arrayOf(10, 20, 30, 40, 50, 60, 70, 80)
val y :Array<Any> = Array<Any>(8) { "" }
myCopyClass(x, y)
}
Output:
10
20
30
40
50
60
70
80
Star Projections
It also happens that sometimes you have no knowledge of the type of argument, but you still want to use it safely. The safest approach is to establish a generic type projection so that every concrete instance of that generic type is a subtype of that projection. So, In simple words, we use the star(*) projection when we don't know the precise type of the item and merely wish to publish all the elements of an array.
Here a simple example of using Star-projections is given below.
Input:
fun myAnswer(array: Array<*>) {
array.forEach { print(it) }
}
fun main(args :Array<String>) {
val ans = arrayOf("Coding ","Ninjas ","Blog ", "Writing")
myAnswer(ans)
}
Output:
Coding Ninjas Blog Writing
Advantages of Kotlin Generics
Following are the advantages of using Kotlin Generics:
- Type-safety: Generic permits only one type of object to be stored. Generic does not allow other objects to be stored.
- No need for typecasting: The object does not need to be typecast.
- Checking at compile time: Generics code is checked at compile time to ensure that there are no difficulties at runtime.
Must Read Elvis Operator Kotlin
FAQs
-
In Kotlin, what are Reified types?
When using the Generics idea to send a class as a parameter to a function and needing to retrieve the type of that class, you must use the reified keyword in Kotlin.
-
What is Contracovariance in Kotlin generics?
It can be used to put a supertype value in the subtypes, i.e., a generic function/class defined for Number cannot accept supertypes of the datatype it is already defined for. Furthermore, The in keyword is used to implement it in Kotlin.
-
In Kotlin generics, what is the difference between "*" and "Any"?
List<*> can contain items of any type, but only that kind; for example, it can contain Strings (but only Strings), whereas List<Any> can contain Strings, Integers, and other types of objects in the same list.
Key-Takeaways
In this article, we learned about Kotlin Generics and their types. We also infer from this article about the initialization and implementation of keywords and variance.
However, learning never stops, and there is more to learn. So head over to our Android Development Course on the Coding Ninjas Website to dive deep into Android Development and build future applications.
We hope this article has helped you enhance your knowledge of Kotlin Generics. If you want to learn more, check out our article on environment setup and Competitive Programming articles. Do upvote this article to help other ninjas grow!