25.2 Variance
This is really one of the trickiest parts to understand. In Java, there is a problem when we use generic
types. Logic says that
List
should be able to be casted to
List
because it’s less
restrictive. But take a look at this example:
1
List strList = new ArrayList<>();
2
List objList = strList;
3
objList.add(5);
4
String str = objList.get(0);
If the Java compiler let us do this, we could add an
Integer
to an
Object
list, and this would obviously
crash at some point. That’s why wildcards were added to the language. Wildcards will increase
flexibility while limiting this problem.
If we add ‘
? extends Object
’ we are using covariance , which means that we can deal with any
object that uses a type that is more restrictive than
Object
, but we can only do
get
operations safely.
If we want to copy a collection of
String
s into a collection of
Object
s, we should be allowed, right?
Then, if we have:
25 Generics
122
1
List strList = ...;
2
List objList = ...;
3
objList.addAll(strList);
This is possible because the definition of
addAll()
in
Collection
interface is something like:
1
List
2
interface Collection ... {
3
void addAll(Collection extends E> items);
4
}
Otherwise, without the wildcard, we wouldn’t be allowed to use a
String
list with this method. The
opposite, of course, would fail. We can’t use
addAll()
to add a list of
Object
s to a list of
String
s.
As we are only getting the items from the collection we use in that method, it’s a perfect example
of covariance.
On the other hand, we can find contravariance , which is just the opposite situation. Following with
the
Collection
example, if we want to add items to a
Collection
we are passing as parameter, we
could add objects with a more restrictive type into a more generic collection. For instance, we could
add
String
s to an
Object
list:
1
void copyStrings(Collection super String> to, Collection from) {
2
to.addAll(from);
3
}
The only restriction we have to add
String
s to another collection is that the collection accepts
Object
s that are
String
s or parent classes.
But wildcards have their own limitations. Wildcards define use-site variance , which means we
need to declare it where we use it. This implies adding boilerplate every time we declare a more
generic variable.
Let’s see an example. Using a class similar to the one we had before:
1
class TypedClass {
2
public T doSomething(){
3
...
4
}
5
}
This code will not compile:
25 Generics
123
1
TypedClass
t1 = new TypedClass<>();
2
TypedClass t2 = t1;
Though it really doesn’t make sense, because we could still keep calling all the methods of the class,
and nothing would break. We need to specify that the type can have a more flexible definition.
1
TypedClass t1 = new TypedClass<>();
2
TypedClass extends String> t2 = t1;
This makes things more difficult to understand, and adds some extra boilerplate.
On the other hand, Kotlin deals with it in an easier manner by using declaration-site variance .
This means that we specify that we can deal with less restrictive situations when defining the class
or interface, and then we can use it blindly everywhere.
So let’s see how it looks in Kotlin. Instead of long wildcards, Kotlin just uses out for covariance and
in for contravariance. In this case, as our class is producing objects that could be saved into less
restrictive variables, we’ll be using covariance. We can define this in the class declaration directly:
1
class TypedClass() {
2
fun doSomething(): T {
3
...
4
}
5
}
And that’s all we need. Now, the same example that wouldn’t compile in Java is perfectly possible
in Kotlin:
1
val t1 = TypedClass()
2
val t2: TypedClass = t1
If you were already used to these concepts, I’m sure you will easily be able to use in and out in
Kotlin. Otherwise, it just requires a little practice and some concepts understanding.
Do'stlaringiz bilan baham: