Ivan Ponomarev
Ivan Ponomarev
|
invariance
covariance
contravariance
Do we need to know it?
Even experienced Java/Kotlin developers sometimes fail to understand this topic.
This leads to poor design of internal APIs: generics are either not used at all, or used incorrectly.
Understanding these concepts allows one to write better APIs.
API is a foundation of the building, implementation is the building itself.
I will show the full evolution from historical perspective: from Java typed arrays through Java generics to Kotlin generics, from primitive to complex stuff
won’t compile
will compile, but runtime exception will occur during execution
will compile and run normally
will compile and run, but heap pollution will occur
(a situation when a variable of a certain type refers to an object that is not of that type)
Given that the Java array a
is not empty, what is the return type of a[0]
?
type of | type of |
| |
| |
| |
|
Given that the Java array a
is not empty, what is the return type of a[0]
?
type of | type of |
|
|
|
|
|
|
|
|
|
|
|
| |
| ||||
| ||||
| ||||
|
|
|
|
| |
| ||||
| ||||
| ||||
|
|
|
|
| |
| ||||
| ||||
| ||||
|
|
|
|
| |
| ||||
| ||||
| ||||
|
|
|
|
| |
| ||||
| ||||
| ||||
|
WHAT?
|
|
|
| |
|
|
|
| |
|
|
|
| |
|
|
| ||
|
|
To → From ↓ |
|
|
|
|
| ||||
| ||||
| ||||
|
To → From ↓ |
|
|
|
|
| ||||
| ||||
| ||||
|
To → From ↓ |
|
|
|
|
| ||||
| ||||
| ||||
|
To → From ↓ |
|
|
|
|
| ||||
| ||||
| ||||
|
Manager[] managers = new Manager[10];
Person[] persons = managers; //this should compile and run
persons[0] = new Person(); //line 1 ??
Manager m = managers[0]; //line 2 ?!
Manager[] managers = new Manager[10];
Person[] persons = managers; //this should compile and run
persons[0] = new Person(); //line 1 ??
Manager m = managers[0]; //line 2 ?!
ArrayStoreException
at line 1.
Arrays in Java are reified — that is they store the information about the types of their elements in run time, and they validate types in runtime.
Arrays in Java are covariant — which means the following:
Covariancy is safe when we read values, but it is unsafe when we write values. For Java arrays, the problem is partially solved by type checking in runtime due to reification of arrays.
list.get(0)
, given that list is not empty?
|
|
|
|
|
|
| |
|
list.get(0)
, given that list is not empty?
|
|
|
|
|
|
|
|
|
|
list.add(…)
method in Java and Kotlin?
|
|
|
| |
| ||||
| ||||
| ||||
|
list.add(…)
method in Java and Kotlin?You’re kidding, there’s no add
in Kotlin’s List
!
|
|
|
| |
| ||||
| ||||
| ||||
|
list.add(…)
method in Java and Kotlin?
|
|
|
| |
| ||||
| ||||
| ||||
| ||||
|
list.add(…)
method in Java and Kotlin?
|
|
|
| |
| ||||
| ||||
| ||||
| ||||
|
list.add(…)
method in Java and Kotlin?
|
|
|
| |
| ||||
| ||||
| ||||
| ||||
|
list.add(…)
method in Java and Kotlin?
|
|
|
| |
| ||||
| ||||
| ||||
| ||||
|
list.add(…)
method in Java and Kotlin?
|
|
|
| |
| ||||
| ||||
| ||||
| ||||
|
list.add(…)
method in Java and Kotlin?
|
|
|
| |
| ||||
| ||||
| ||||
| ||||
|
list.add(…)
method in Java and Kotlin?
|
|
|
| |
| ||||
| ||||
| ||||
| ||||
|
list.add(…)
method in Java and Kotlin?
|
|
|
| |
| ||||
| ||||
| ||||
| ||||
|
To → From ↓ |
|
|
|
|
| ||||
| ||||
| ||||
|
To → From ↓ |
|
|
|
|
| ||||
| ||||
| ||||
|
In Java language, generic types are invariant, that is assignable only to the types of the exactly same type.
Otherwise:
List<Manager> managers = new ArrayList<>();
List<Person> persons = managers; //won't compile
persons.add(new Person()); //no runtime check is possible
In Kotlin, MutableList
is still invariant.
So is Array<T>
, which is a wrapper around native Java array. Thus, you cannot assign e. g. Array<String>
to Array<Any>
.
To → From ↓ |
|
|
|
|
| ||||
| ||||
| ||||
|
To → From ↓ |
|
|
|
|
| ||||
| ||||
| ||||
|
And you know why it’s safe!
Java | Kotlin |
|
|
List<E>
in Kotlin is a covariant type.
But it’s safe, because we only read values from Kotlin’s list and never write to it!
First, we want to avoid run time exceptions
Java and Kotlin generics use type erasure.
This means that in run time, List<T>
does not know the T
and thus cannot perform run time check.
Appeared in Java 5
There was a problem of backward compatibility
Generics were implemented as a language capability, not a platform capability
Here comes Type Erasure
Generic Type (source) | Raw Type (compiled) |
|
|
Generic Type (source) | Raw Type (compiled) |
|
|
Source code | Compiled |
|
|
Source code | Compiled |
|
|
Each lambda is compiled to an anonymous class which inherits from FunctionImpl
and implements the corresponding invoke
:
Source code | Compiled |
|
|
Source code | Compiled |
|
|
There are no parameterized classes in the JVM, only regular classes and methods.
Type parameters are replaced with Object or with boundary type.
Bridge methods are added to preserve polymorphism.
Type cast is added as needed.
Java | Kotlin |
|
|
Java | Kotlin |
|
|
We don’t know the type parameter in the runtime!
Java | Kotlin |
|
|
Although we would like to know more than that…
List<String>[] a = new ArrayList<String>[10];
List<String>[] a = new ArrayList<String>[10];
"Generic Array Creation", because such an array will not have the full type information about its elements!
Java | Kotlin |
|
|
Java | Kotlin |
|
|
intPair
is spoiled, it’s not a pair of integer values!
MyList<Manager> managers = ...
MyList<Employee> employees = ...
//Valid options, we want these to be compilable!
employees.addAllFrom(managers);
managers.addAllTo(employees);
//Invalid options, we don't want these to be compilable!
managers.addAllFrom(employees);
employees.addAllTo(managers);
class MyList<E> implements Iterable<E> {
void add(E item) { ... }
void addAllFrom(MyList<E> list) {
for (E item : list) this.add(item);
}
void addAllTo(MyList<E> list) {
for (E item : this) list.add(item);
}
...}
MyList<Manager> managers = ...; MyList<Employee> employees = ...;
employees.addAllFrom(managers); managers.addAllTo(employees);
class MyList<E> implements Iterable<E> {
void add(E item) { ... }
void addAllFrom(MyList<E> list) {
for (E item : list) this.add(item);
}
void addAllTo(MyList<E> list) {
for (E item : this) list.add(item);
}
...}
MyList<Manager> managers = ...; MyList<Employee> employees = ...;
employees.addAllFrom(managers); managers.addAllTo(employees);
Won’t compile because of type invariance
class MyList<E> : Iterable<E> {
fun add(item: E) {...}
fun addAllFrom(list: MyList<E>) {
for (item in list) add(item)
}
fun addAllTo(list: MyList<E>) {
for (item in this) list.add(item)
}
...}
val managers: MyList<Manager> = ...; val employees: MyList<Employee> = ...
employees.addAllFrom(managers); managers.addAllTo(employees)
class MyList<E> : Iterable<E> {
fun add(item: E) {...}
fun addAllFrom(list: MyList<E>) {
for (item in list) add(item)
}
fun addAllTo(list: MyList<E>) {
for (item in this) list.add(item)
}
...}
val managers: MyList<Manager> = ...; val employees: MyList<Employee> = ...
employees.addAllFrom(managers); managers.addAllTo(employees)
Won’t compile because of type invariance
class MyList<E> implements Iterable<E> {
void addAllFrom (List<? extends E> list){
for (Е item: list) add(item); }
}
MyList<Manager> managers = ...; MyList<Employee> employees = ...
employees.addAllFrom(managers);
If E
is Employee
, MyList<Manager>
will do as MyList<? extends E>
!!
class MyList<E> : Iterable<E> {
fun addAllFrom(list: MyList<out E>) {
for (item in list) add(item) }
}
val managers: MyList<Manager> = ... ; val employees: MyList<Employee> = ...
employees.addAllFrom(managers)
If E
is Employee
, MyList<Manager>
will do as MyList<out E>
!!
This is what Java doesn’t have:
class MyImmutablePair<out E>(val a: E, val b: E)
In this class, we can only declare methods that return something of type E, but not accessible methods that will have E-typed arguments.
Constructor parameters and private methods with E-typed arguments are OK!
class MyList<E> : Iterable<E> {
//Don't bother about use-site type variance!
fun addAllFrom(pair: MyImmutablePair<E>){
add(pair.a); add(pair.b) }
...
}
val twoManagers: MyImmutablePair<Manager> = ...
employees.addAllFrom(twoManagers)
MyImmutablePair<Manager>
is assignable to MyImmutablePair<Employee>
? extends
in Java?
<E> void doSomething (MyList<? extends E> list) {
E e1 = list.get(...);
? extends
in Java?
<E> void doSomething (MyList<? extends E> list) {
E e1 = list.get(...);
}
Of course!
? extends
in Java?
<E> void doSomething (MyList<? extends E> list) {
E e2 = ...;
list.add(e2);
}
? extends
in Java?
<E> void doSomething (MyList<? extends E> list) {
E e2 = ...;
list.add(e2);
}
Why?
? extends
in Java?
<E> void doSomething (MyList<? extends E> list) {
list.add(null);
}
? extends
in Java?
<E> void doSomething (MyList<? extends E> list) {
list.add(null);
}
Why?
If Foo<T extends Bound>
, then Foo<?>
is the same as Foo<? extends Bound>
.
We can read elements, but only as Bound
(or Object
, if no Bound
is given).
If we’re using intersection types Foo<T extends Bound1 & Bound2>
, any of the bound types will do.
We can put only null
values.
Star-projection Foo<*>
in Kotlin looks similar, but we’ll cover it later…
out
in Kotlin?
fun <E> doSomething (list: MyList<out E?>) {
val e1: E? = list.get(...)
}
out
in Kotlin?
fun <E> doSomething (list: MyList<out E?>) {
val e1: E? = list.get(...)
}
out
in Kotlin?
fun <E> doSomething (list: MyList<out E?>) {
val e2: E? = ...
list.add(e2)
}
out
in Kotlin?
fun <E> doSomething (list: MyList<out E?>) {
val e2: E? = ...
list.add(e2)
}
out
in Kotlin?
fun <E> doSomething (list: MyList<out E?>) {
list.add(null)
}
out
in Kotlin?
fun <E> doSomething (list: MyList<out E?>) {
list.add(null)
}
|
|
Predicate<Person>
can substitute Predicate<Employee>
and Predicate<Manager>
, and thus it can be considered as their subtype.
class MyList<E> implements Iterable<E> {
void addAllTo (List<? super E> list) {
for (Е item: this) list.add(item); }
}
MyList<Employee> employees = ...; MyList<Person> people = ...;
employees.addAllTo(people);
If E
is Employee
, MyList<Person>
will do as MyList<? super E>
!
class MyList<E> : Iterable<E> {
fun addAllTo(list: MyList<in E>) {
for (item in this) list.add(item) }
}
val employees: MyList<Employee> = ... ; val people: MyList<Person> = ...
employees.addAllTo(people)
If E
is Employee
, MyList<Person>
will do as MyList<in E>
!
class MyConsumer<in E> {
fun consume(p: E){
...
}
}
Now we can define methods that have E-typed arguments, but we cannot expose anything of type E.
We can have private class variables of type E, and even private methods that return E, though.
class MyList<E> : Iterable<E> {
//Don't bother about use-site type variance!
fun addAllTo(consumer: MyConsumer<E>){
for (item in this) consumer.consume(item)
}
...
}
val employees: MyList<Employee> = ...
val personConsumer: MyConsumer<Person> = ...
employees.addAllTo(personConsumer)
MyConsumer<Person>
is assignable to MyConsumer<Employee>
? super
in Java?
<E> void doSomething (MyList<? super E> list) {
E e1 = ...;
list.add(e1);
list.add(null);
}
? super
in Java?
<E> void doSomething (MyList<? super E> list) {
E e1 = ...;
list.add(e1);
list.add(null);
}
? super
in Java?
<E> void doSomething (MyList<? super E> list) {
E e2 = list.get(...);
}
? super
in Java?
<E> void doSomething (MyList<? super E> list) {
E e2 = list.get(...);
}
? super
in Java?
<E> void doSomething (MyList<? super E> list) {
Object e2 = list.get(...);
}
? super
in Java?
<E> void doSomething (MyList<? super E> list) {
Object e2 = list.get(...);
}
in
in Kotlin?
fun <E> doSomething (list: MyList<in E>) {
val e1: E = ...
list.add(e1)
}
in
in Kotlin?
fun <E> doSomething (list: MyList<in E>) {
val e1: E = ...
list.add(e1)
}
in
in Kotlin?
fun <E> doSomething (list: MyList<in E>) {
list.add(null)
}
in
in Kotlin?
fun <E> doSomething (list: MyList<in E>) {
list.add(null)
}
in
in Kotlin?
fun <E> doSomething (list: MyList<in E?>) {
list.add(null)
}
in
in Kotlin?
fun <E> doSomething (list: MyList<in E?>) {
list.add(null)
}
in
in Kotlin?
fun <E> doSomething (list: MyList<in E>) {
val first: Any = list.first()
}
in
in Kotlin?
fun <E> doSomething (list: MyList<in E>) {
val first: Any = list.first()
}
in
in Kotlin?
fun <E> doSomething (list: MyList<in E>) {
val first: Any? = list.first()
}
in
in Kotlin?
fun <E> doSomething (list: MyList<in E>) {
val first: Any = list.first()
}
Foo<*>
Foo<T : TUpper>
(invariant):
you can read values as TUpper
you cannot write anything (even null)
Foo<out T : TUpper>
(covariant):
you can read values as TUpper
Foo<in T: TUpper>
(contravariant):
you still cannot write anything :-)
PECS
Producer — Extends, Consumer — Super
public static <T> T max (Collection<? extends T> coll,
Comparator<? super T> comp)
Collections.max(List<Integer>, Comparator<Number>)
Collections.max(List<String>, Comparator<Object>)
Producer — Out, Consumer — In
Rule of thumb:
if you only read values of T
, make it out
on declaration site
if you only write values of T
, make it in
on declaration site
Kafka Streams KStream<K, V>
, KTable<K, V>
classes are semantically covariant: stream of Employee
can be safely considered as a stream of Person
!!
This cannot be fixed even by adding ? extends
everywhere…
/* LIBRARY CODE */
class KStream<E> { ... }
class Processor<E> {
void withFunction(Function<? super KStream<E>,
? extends KStream<E>> chain) {...}
}
/* USER'S CODE */
KStream<Employee> transformA(KStream<Employee> s) {...}
KStream<Manager> transformB(KStream<Person> s) {...}
/* We want to use method references! */
Processor<Employee> processor = ...
processor.withFunction(this::transformA);
processor.withFunction(this::transformB);
Although this::transformA
will do, this::transformB
will fail with "KStream<Employee> is not convertible to KStream<Person>"
/* LIBRARY CODE */
class KStream<E> { ... }
class Processor<E> {
void withFunction(Function<? super KStream<? super E>,
? extends KStream<? extends E>> chain) {...}
}
/* USER'S CODE */
KStream<Employee> transformA(KStream<Employee> s) { ... }
KStream<Manager> transformB(KStream<Person> s) { ... }
Processor<Employee> processor = new Processor<>();
processor.withFunction(this::transformA);
processor.withFunction(this::transformB);
Both lines won’t compile with something like "KStream<capture of ? super Employee> is not convertible to KStream<Employee>"
/* LIBRARY CODE */
class KStream<out E>
class Processor<E> {
fun withFunction(chain: (KStream<E>) -> KStream<E>) {}
}
/* USER'S CODE */
fun transformA(s: KStream<Employee>): KStream<Employee> { ... }
fun transformB(s: KStream<Person>): KStream<Manager> { ... }
val processor: Processor<Employee> = Processor()
processor.withFunction(this::transformA)
processor.withFunction(this::transformB)
Everything will compile and run as intended!
Usage of ready-made generic types is straightforward
In order to create your own generic types, you MUST understand the key principles
Kotlin offers great enhancements for Java Generics, making usage of ready-made generic types even more straightforward
But in order to create your own generic types in Kotlin, it’s even more important to understand the key principles!
Covariance | Contravariance |
Covariance | Invariance | Contravariance | |||||||||||||||||||||||||||
|
|
|
Producer Extends, Consumer Super
Producer Out, Consumer In
@inponomarev