They enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods/functions. Much like the more familiar formal parameters used in the method declarations, type parameters provide a way for you to re-use the same code with different inputs.
Generic classes and methods combine reusability, type safety, and efficiency in a way that their non-generic counterparts with collections and the methods that operate on them.
Maybe when doing a project or trying to type better code you have noticed
- The code written for two different features is way too similler.
- When adding new functionality you find yourself repeating same code with mionr differences.
- Dome features you have developed could be refactored with much less boilerplate code.
Let’s do some {code}
Suppose we’re making a game, in this game, there are going to be 2 kinds of weapons, knife, and sniperRifle(normally there should be more than that but just to keep it simple it will be 2)
abstract class Weapon
class Knife: Weapon()
open class Rifle: Weapon()
class SniperRifle: Weapon()
So, if you want to get a weapon you will have to check boxes and find them, but a box can have only 1 kind of weapon so a box of knifes will have just knifes inside and box of rifles or sniperRifles will just have of its own type too, one thing to do here is making a class for every weapon(here is not that bad because we have just 2 weapons but imagine if the game world has 50 different weapons it will not be very efficient to make the expect same class for every type of weapon) or we could use generics.
//Do This
class Box<T>(weaponInside: T)
//Insted of doing this
class Box(weaponInside: SniperRifle)
class Box(weaponInside: Knife)
You have already seen those <>
angle brackets before, with collections!, yes, List<String>
, ArrayList<String>
, HashMap<String, Any>()
, Set<Int>
, those are collections and those allow you to use them with any type you want, like strings, or cars, or knifes, weapon, etc. So 1 class and you can go ahead with any type, but we still have a problem in our Box class. Look below….
This is what we want to do with our generics
val knifeBox = Box(Knife()) //here we see type inference
//here we explicitly tell kotlin what type we are storing
val sniperRifelBox: Box<SniperRifle> = Box(SniperRifle())
But we can do as well as below, this one also legal
val knifeBox = Box("you can put here a String along with Weapon()")
//or below one
val sniperRifelBox: Box(3.0) //Any type
So what to do to fix that? We just need to restrict T
it to be a subclass of Weapon()
class Box<T: Weapon>(weaponInside: T)
Now, we don’t have to worry about putting different types of data by mistake because the compiler/IDE will complain or show errors and won’t receive any data type that is not a child of weapon class.
By the way using T
as the name parameter in generics is not mandatory but it is a convention to use either E
or T
so you should normally follow it and name it like that too.
Now we will talk about in and out modifiers
in and out are variance modifiers and they help us allow subtyping when using generics.
Suppose we have a class called Case that will indicate if we either consume a weapon or produce it.
class <T: Weapon>{
private val contents = mutableListOf<T>()
fun produce(): T = contents.list()
fun consume(item: T) = contents.add(item)
}
You might think that this is indeed possible thanks to polymorphism.
val sniperRifle: Case<SniperRifle> = Case<SniperRifle>()
var rifel = Case<Rifle>()
rifle = sniperRifle
Normally assigning a child instance to a parent instance is possible, but this time it isn’t because generics don’t allow subtyping by default, and by that I mean we have to use keywords like in
and out
to be able of using subtyping.
If we declare T
with an out
modifier, it will be covariant, so now we preserve subtyping but we cannot consume(take T
as parameters). T
, we can just produce(return
) it.
class Case<out T: Weapon>{
private val contents = mutableListOf<T>()
fun produce(): T = contents.last()
//this is no longer possible
// fun consume(item: T) = contents.add(item)
}
/*
* (Below)This is now possible
* */
val sniperRifle: Case<SniperRifle> = Case<SniperRifle>()
var rifle = Case<Rifle>()
rifle = sniperRifle
rifle.produce()
Now, if we declare T
with in
modifier. It will be contravariant, think of it as the other way around (sort of), because we can now consume T
but we cannot produce T
.
class Case<in T: Weapon>{
private val contents = mutableListOf<T>()
//this is no longer possible
// fun produce(): T = contents.last()
fun consume(item: T) = contents.add(item)
}
But here is where things get a little bit weird, now a parent class will be the child of its child class. So, rifle, is now a subclass of sniperRifle so you treated it as if rifle is extending sniperRifle or as if weapon is extending knife and rifle.
var sniperRifle: Case<SniperRifle> = Case<SniperRifle>()
var rifle = Case<Rifle>()
sniperRifle = rifle
sniperRifle.consume(Rifle())
Finally, we will talk about were
keyword.
What is it all about with the where
keyword? We use it when we want to extend from several interfaces and not just one, this is also called an upper bound.
Suppose we have a function to sell weapons and we want to sell just weapons and usable ones(note the new interface I will make usable). If the data type we passed is a weapon and is usable we can proceed to use the function, otherwise, we can’t, this is only possible with functions.
fun <T> sellWeapon(weapon: T): String where T: Weapon, T: Usable{
print("$weapon was just sold")
return "success"
}
Thank you.