As you know, generics enable us to parameterize types when defining classes (or interfaces) and methods. Parameterized types make it possible to reuse the same code while processing different concrete types.
Reusing code with generics
Let's consider a generic class named GenericType that stores a value of "some type".
class GenericType<T>(val t: T) {
fun get(): T {
return t
}
}It is possible to create an object with a concrete type (e.g., String):
val stringInstance: GenericType<String> = GenericType<String>("abc")
val stringField = stringInstance.get()We can also create instances with other types (Int, Char) and then invoke the get method to access the internal field. In this manner, generics allow us to use the same class and methods for processing different types.
Reusing code with Any
However, there is also another way to reuse code. If we declare a field of type Any, we can assign a value of any reference type to it.
The following class demonstrates this concept:
class NonGenericClass(val value: Any) {
fun get(): Any {
return value
}
}Now we can create an instance of this class with the same string as in the previous example (see GenericType).
val anyInstance: NonGenericClass = NonGenericClass("abc")It is also possible to create an instance by passing in a value of type Int, Char, or any other reference type.
Using the Any class this way allows us to reuse the same class to store different data types.
The advantage of generics: from run-time to compile-time
After an invocation of the get() method, we obtain an Any, not a String or an Integer. We cannot get a string directly from the method.
val nonGenericInstance: NonGenericClass = NonGenericClass("abc")
val str: String = nonGenericInstance.get() // Compile-time error: Type mismatchTo get the string back, we must perform an explicit type-cast to the String class.
val str: String = nonGenericInstance.get() as String // "abc"This works because a string was passed into nonGenericInstance. But what if the instance does not store a string? If this is the case, the code throws an exception. Here is an example:
val instance: NonGenericClass = NonGenericClass(123)
val string: String = instance.get() as String // throws java.lang.ClassCastExceptionNow we can see the main advantage of generics over the Any class. Because there is no need to perform an explicit type-cast, we never get a runtime exception. If we do something wrong, we can see it at compile-time.
val stringInstance: GenericType<String> = GenericType<String>("abc")
val str: String = stringInstance.get() // There is no type-casting here
val num: Int = stringInstance.get() // It does not compileA compile-time error will be detected by the programmer, not a user of the program. Because generics let the compiler take care of type casting, generics are both safer and more flexible compared with the Any class.
Multiple Type Parameters
In many situations, it is much more convenient to have several parameters in a class, which can be of arbitrary type. Kotlin allows us to define not only one parameter of arbitrary type but also two, three, etc. In this case, you can specify several type parameters, separating them with a comma.
class MultipleGenerics<T, P>(var valueT: T, var valueP: P)This way is much better and safer than declaring all parameters as type Any. Let's see an example to understand the differences between these two methods of declaring parameterized classes:
We have two classes: a generic class PairGeneric and a non-generic class PairNonGeneric. Both of them have two fields of arbitrary type first and second:
class PairGeneric<T, P>(var first: T, var second: P) {
fun changeFirst(newValue: T) {
first = newValue
}
fun changeSecond(newValue: P) {
second = newValue
}
fun printData() {
println("Value:")
println("first = $first")
println("second = $second")
}
}
class PairNonGeneric(var first: Any, var second: Any) {
fun changeFirst(newValue: Any) {
first = newValue
}
fun changeSecond(newValue: Any) {
second = newValue
}
fun printData() {
println("Value:")
println("first = $first")
println("second = $second")
}
}Now, let's do some wrong actions with the class fields and see the difference:
fun main() {
val genericPair: PairGeneric<String, Int> = PairGeneric("John", 8)
val nonGenericPair: PairNonGeneric = PairNonGeneric("Kate", 18)
genericPair.changeFirst(8) // It does not compile
nonGenericPair.changeSecond("Smith") // It works
nonGenericPair.printData()
}Here is the result for the object of PairNonGeneric class:
Value:
first = Kate
second = SmithAs you can see, if the field type in the class is Any, you can easily and with no errors assign values of different types to the field, which is not good. When you write small programs, this might not be a problem for you, as you will remember what field type you defined. But when you go to code large programs and applications, this can lead to errors that are very hard to find. In this case, it is best to use generics: if you try to assign a value with a different type to a field, it won't compile and you will easily find the problem
Conclusion
Both generics and Any allow you to write generalized code. Using Any, however, may require explicit type-casting, which can lead to error-prone code. Generics provide type-safety by shifting type checking responsibilities to the Kotlin compiler.