Suppose we have a task that we have to perform multiple times but with different data types each time. Making n different functions for those n different data types is one of the ways but is it effective?
No, because it does not adhere to the DRY (Don’t repeat yourself) principle of programming.
Do we have an alternative? Yes, Generics comes to the rescue here.
We make a function/class that does one unique task but can perform that task on whatever data type provided to it. What I mean is that you define a generic by telling your compiler, “this is what I want you to do with the data I put in this function”, but without telling it a lot about the data itself.
For example -
Different API's have different response types, but for each API call we need to perform a common task say checking network availability, we can make a function like -
fun <T> hitSafeApiCall(
data: MutableLiveData<Resource<T>>?,
apiCallBack: APICallBacks<T>?
) {
if (ConnectionManager.instance.hasNetworkAvailable()) {
// handle network availability
} else {
noNetwork(data, apiCallBack)
}
}
And now whenever we call our function, we have to specify the actual type in place of T, in this case, type of API response when hitting multiple Apis.
Some key points regarding generics-
- Not only functions, classes, and interfaces can also be made generic.
- We can make a generic function in a non-generic class.
- We can make functions that can take more generic parameters than the class does.
- We can make generic top-level functions. (I have discussed top-level functions in my previous articles)
- Multiple generic types can be defined in a function with each one separated by a comma.
private fun <T> multipleGenerics(param1: T, param2: T) {
println(param1)
println(param2)
}multipleGenerics(“multiple” , “generics”)
multipleGenerics(10, 20)
multipleGenerics(10, “generics”)
Constraints in Generics
Constraint means to restrict something. While using generics, we can restrict the types that can be used for a generic type parameter, i.e we can specify the data must be of a specific type or a subType/subClass of it.
For example -
private fun <T : Number> genericConstraints(param : T): Int {
return param.toInt().plus(10)
}
Here we have specified that the generic parameter can be of class Number or it’s sub classes.
genericConstraints(20.0)
genericConstraints(20f)
genericConstraints(“20”)
So, it would work fine for double and float values but not for String resulting in type mismatch error.
Variance
One of the basic principles of object-oriented programming is polymorphism which means that an object can take many forms depending on the situation. We can assign a superclass reference with a subclass object.
Consider the following code -
class Library<T: Book>(var book: T, var nCopies: Int, var language: String)open class Book(var attr : String)
class CourseBook(var name: String, var author: String, var course: String) : Book("abc")
class Novel(var name: String, var author: String, var genre: String) : Book("def")val courseBook: Book = CourseBook(“Core Science Biology “, “Geeta Negi”, “Biology — Class 10”)
val novel = Novel(“Jane Eyre”, “Charlotte Bronte”, “Gothic Fiction”)private lateinit var mainBook: Library<Book>var lCourseBook: Library<CourseBook> = Library(courseBook, 20, “English”)
var lNovel: Library<Novel> = Library(novel, 30, “English”)
But, now when we assign mainBook (superclass type) with lCourseBook (subclass type) we get the following error -
Why does that happen despite Kotlin being an object-oriented language?
Because if the above code would have compiled, then we could also assign -
mainBook.book = lNovel
but with this, we have assigned a novel object to a courseBook object and now when we try to access mainBook.book as a courseBook object we will get a ClassCastException. Therefore to avoid such scenarios Kotlin introduced the concept of variance.
Variance is of 2 types -
- Covariance
- Contravariance.
The basic idea of these is to restrict the direction in which we move data in (contravariance) and out (covariance) of the generic object.
Covariance
The main reason Kotlin did not allow assigning mainBook with lCourseBook was to avoid the situation where we can modify mainBook.book with other objects. So a possible solution is to make this generic class read-only!
Koltin allows us to do so by declaring the generic parameter by ‘out’ keyword, which means T is only going to be produced by methods of this class. It can only be used as a return type and not as a parameter type.
class Library<out T : Book>(private var book: T?, var nCopies: Int, var language: String) {
fun getBook() : T? = book
}
Now we can assign our superclass reference with a subclass object.
Contravariance
Conversely, we can declare a generic parameter by ‘in’ keyword, to restrict the class to only consume it. It can only be used as parameter types and no return types.
class Library<in T : Book>(private var book: T?, var nCopies: Int, var language: String) {
fun setBook(book: T?) {
this.book = book
}
}
This enables us to assign the subclass object with the superclass object.
private lateinit var lCourseBook: Library<CourseBook>
var mainBook: Library<Book> = Library(Book(“abc”), 10, “English”)lCourseBook = mainBook
mainBook.setBook(novel)
In the above examples, while creating the generic class itself, we declared whether the parameter will be covariant or contravariant thus it is called declaration-site variance.
Use-site variance or Type projection
There are cases where we cannot decide whether the parameter will be covariant or contravariant because they can be used both as parameter types or return types. We can still use it in a variant way using type projections.
For example -
The library class now produces as well as consumes the generic parameter book -
class Library<T : Book> (private var book: T?, var nCopies: Int, var language: String) {
fun getBook() : T? = book
fun setBook(book: T?) {
this.book = book
}
}
When we want to use only getBook(), we can project it covariantly using out keyword like -
fun <T> useSiteCovariant(from : Library<out T>, to : Library<T>) {
to.setBook(from.getBook())
}
Now we cannot call from.setBook(). We have restricted that the type parameter of from must be a subtype of type parameter of to.
Similarly, using the keyword, we can restrict access to only setBook() and can project it contra-covariantly.
fun <T> useSiteContravariant(from : Library<T>, to : Library<in T>){
to.setBook(from.getBook())
}
We cannot call to.getBook() now. We have restricted that the type parameter of to must be the supertype of the type parameter of from.
Star Projections
There are times when we know nothing about the type of argument but we still want to use it in a safe way.
Going back to our first example where generic parameters did not have a constraint of generic type Book. We can pass Library<Novel>, Library<String>, Library<int> anything to it. So the param type of setBook() must be the subType of all of these!
In Kotlin, Nothing is the subType of every type.
fun setBook(book: Nothing) {
this.book = book
}
Similarly, the return type of getBook() must be the superType of all these.
In Kotlin, Any is the superType of every type, thus changing out getBook() function as -
fun getBook() : Any?
Therefore, for any function that accepts or returns type parameter needs to
- Accept Nothing
- Return Any?
Thus our Library class could be Library<in Nothing> or Library<out Any> which could also be replaced by Library<*>. We use star with a notion of accepting a Library that has any kind of type argument.
Conclusion
In this article, we explored how generics work in Kotlin and implement the concepts of object-oriented programming in different ways to provide a safe way to handle data.
There is a lot more that we will cover in upcoming articles.
If you like this article, don’t forget to clap and to keep up-to-date with all my new articles, you can follow me and my publication on Medium.
Any feedback, suggestions are always welcomed and appreciated!
Keep Learning!