Type variance in Java and Kotlin

Ivan Ponomarev

ivan

Ivan Ponomarev

  • Staff Engineer @ Synthesized.io

  • Teaching Java @ МФТИ and Mainor

Type variance

  • invariance

  • covariance

  • contravariance

It sounds scary

Do we need to know it?

functors

Type variance

  • 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.

The reason

  • Understanding these concepts allows one to write better APIs.

  • API is a foundation of the building, implementation is the building itself.

How I’m going to explain it

  • I will show the full evolution from historical perspective: from Java typed arrays through Java generics to Kotlin generics, from primitive to complex stuff

evolution

We’ll be using this example of type hierarchy everywhere

hier

Possible outcomes

  • wc won’t compile

  • rt will compile, but runtime exception will occur during execution

  • ok will compile and run normally

  • hp 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)

Let’s get to the roots: Java arrays

Given that the Java array a is not empty, what is the return type of a[0]?

type of a

type of a[0]

Person[]

Employee[]

Manager[]

Object[]

Let’s get to the roots: Java arrays

Given that the Java array a is not empty, what is the return type of a[0]?

type of a

type of a[0]

Person[]

Person (or null)

Employee[]

Employee (or null)

Manager[]

Manager (or null)

Object[]

Object (or null)

Given that Java array is not empty, what is the result of assigning a value to its element?

wc rt ok hp

Person

Employee

Manager

null

Object[]

q

Person[]

q

Employee[]

q

Manager[]

q

Given that Java array is not empty, what is the result of assigning a value to its element?

wc rt ok hp

Person

Employee

Manager

null

Object[]

ok

Person[]

ok

Employee[]

ok

Manager[]

ok

Given that Java array is not empty, what is the result of assigning a value to its element?

wc rt ok hp

Person

Employee

Manager

null

Object[]

ok

Person[]

ok

Employee[]

q

ok

Manager[]

q

q

ok

Given that Java array is not empty, what is the result of assigning a value to its element?

wc rt ok hp

Person

Employee

Manager

null

Object[]

ok

Person[]

ok

Employee[]

wc

ok

Manager[]

wc

wc

ok

Given that Java array is not empty, what is the result of assigning a value to its element?

wc rt ok hp

Person

Employee

Manager

null

Object[]

q

q

q

ok

Person[]

q

q

q

ok

Employee[]

wc

q

q

ok

Manager[]

wc

wc

q

ok

Given that Java array is not empty, what is the result of assigning a value to its element?

WHAT?

Person

Employee

Manager

null

Object[]

ok rt

ok rt

ok rt

ok

Person[]

ok rt

ok rt

ok rt

ok

Employee[]

wc

ok rt

ok rt

ok

Manager[]

wc

wc

ok rt

ok

Can we assign a Java array of a given type to an array of another type?

wc rt ok hp

To →

From ↓

Object[]

Person[]

Employee[]

Manager[]

Object[]

ok

q

q

q

Person[]

ok

q

q

Employee[]

ok

q

Manager[]

ok

Can we assign a Java array of a given type to an array of another type?

wc rt ok hp

To →

From ↓

Object[]

Person[]

Employee[]

Manager[]

Object[]

ok

wc

wc

wc

Person[]

ok

wc

wc

Employee[]

ok

wc

Manager[]

ok

Can we assign a Java array of a given type to an array of another type?

wc rt ok hp

To →

From ↓

Object[]

Person[]

Employee[]

Manager[]

Object[]

ok

wc

wc

wc

Person[]

q

ok

wc

wc

Employee[]

q

q

ok

wc

Manager[]

q

q

q

ok

Can we assign a Java array of a given type to an array of another type?

wc rt ok hp

To →

From ↓

Object[]

Person[]

Employee[]

Manager[]

Object[]

ok

wc

wc

wc

Person[]

ok

ok

wc

wc

Employee[]

ok

ok

ok

wc

Manager[]

ok

ok

ok

ok

What about this code?

wc rt ok hp

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 ?!

What about this code?

rt rt rt rt

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 ?!

rt ArrayStoreException at line 1.

Intermediate conclusions about arrays in Java

  • 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.

Intermediate conclusions about arrays in Java

  • Arrays in Java are covariant — which means the following:

cv
  • 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.

In Java and Kotlin, what is return type of list.get(0), given that list is not empty?

List<Person>

Person

List<Employee>

Employee

List<Manager>

Manager

List<?>

q

List<*>

q

In Java and Kotlin, what is return type of list.get(0), given that list is not empty?

List<Person>

Person

List<Employee>

Employee

List<Manager>

Manager

List<?>

Object

List<*>

Any?

What is the result of list.add(…​) method in Java and Kotlin?

wc rt ok hp

Person

Employee

Manager

null

List<Person>

List<Employee>

List<Manager>

List<?> / List<*>

What is the result of list.add(…​) method in Java and Kotlin?

You’re kidding, there’s no add in Kotlin’s List!

Person

Employee

Manager

null

List<Person>

ha

ha

ha

ha

List<Employee>

ha

ha

ha

ha

List<Manager>

ha

ha

ha

ha

List<?> / List<*>

ha

ha

ha

ha

What is the result of list.add(…​) method in Java and Kotlin?

wc rt ok hp

Person

Employee

Manager

null

List<Person> / MutableList<Person?>

q

q

q

List<Employee> / MutableList<Employee?>

q

q

List<Manager> / MutableList<Manager?>

q

List<?>

MutableList<*>

What is the result of list.add(…​) method in Java and Kotlin?

wc rt ok hp

Person

Employee

Manager

null

List<Person> / MutableList<Person?>

ok

ok

ok

List<Employee> / MutableList<Employee?>

ok

ok

List<Manager> / MutableList<Manager?>

ok

List<?>

MutableList<*>

What is the result of list.add(…​) method in Java and Kotlin?

wc rt ok hp

Person

Employee

Manager

null

List<Person> / MutableList<Person?>

ok

ok

ok

List<Employee> / MutableList<Employee?>

q

ok

ok

List<Manager> / MutableList<Manager?>

q

q

ok

List<?>

MutableList<*>

What is the result of list.add(…​) method in Java and Kotlin?

wc rt ok hp

Person

Employee

Manager

null

List<Person> / MutableList<Person?>

ok

ok

ok

List<Employee> / MutableList<Employee?>

wc

ok

ok

List<Manager> / MutableList<Manager?>

wc

wc

ok

List<?>

MutableList<*>

What is the result of list.add(…​) method in Java and Kotlin?

wc rt ok hp

Person

Employee

Manager

null

List<Person> / MutableList<Person?>

ok

ok

ok

List<Employee> / MutableList<Employee?>

wc

ok

ok

List<Manager> / MutableList<Manager?>

wc

wc

ok

List<?>

q

q

q

MutableList<*>

q

q

q

What is the result of list.add(…​) method in Java and Kotlin?

wc rt ok hp

Person

Employee

Manager

null

List<Person> / MutableList<Person?>

ok

ok

ok

List<Employee> / MutableList<Employee?>

wc

ok

ok

List<Manager> / MutableList<Manager?>

wc

wc

ok

List<?>

wc

wc

wc

MutableList<*>

wc

wc

wc

What is the result of list.add(…​) method in Java and Kotlin?

wc rt ok hp

Person

Employee

Manager

null

List<Person> / MutableList<Person?>

ok

ok

ok

q

List<Employee> / MutableList<Employee?>

wc

ok

ok

q

List<Manager> / MutableList<Manager?>

wc

wc

ok

q

List<?>

wc

wc

wc

q

MutableList<*>

wc

wc

wc

q

What is the result of list.add(…​) method in Java and Kotlin?

wc rt ok hp

Person

Employee

Manager

null

List<Person> / MutableList<Person?>

ok

ok

ok

ok

List<Employee> / MutableList<Employee?>

wc

ok

ok

ok

List<Manager> / MutableList<Manager?>

wc

wc

ok

ok

List<?>

wc

wc

wc

ok

MutableList<*>

wc

wc

wc

wc

Can we assign these lists to each other?

wc rt ok hp

To →

From ↓

List / MutableList <Person>

List / MutableList <Employee>

List / MutableList <Manager>

List<?>/ MutableList<*>

List/MutableList <Person>

ok

wc

wc

ok

List/MutableList <Employee>

q

ok

wc

ok

List/MutableList <Manager>

q

q

ok

ok

List<?> / MutableList<*>

q

q

q

ok

Can we assign these lists to each other?

wc rt ok hp

To →

From ↓

List / MutableList <Person>

List / MutableList <Employee>

List / MutableList <Manager>

List<?>/ MutableList<*>

List/MutableList <Person>

ok

wc

wc

ok

List/MutableList <Employee>

wc

ok

wc

ok

List/MutableList <Manager>

wc

wc

ok

ok

List<?> / MutableList<*>

wc

wc

wc

ok

Intermediate conclusion

  • 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>.

What about immutable lists in Kotlin?

wc rt ok hp

To →

From ↓

List<Person>

List<Employee>

List<Manager>

List<*>

List<Person>

ok

wc

wc

ok

List<Employee>

q

ok

wc

ok

List<Manager>

q

q

ok

ok

List<*>

wc

wc

wc

ok

What about immutable lists in Kotlin?

wc rt ok hp

To →

From ↓

List<Person>

List<Employee>

List<Manager>

List<*>

List<Person>

ok

wc

wc

ok

List<Employee>

ok

ok

wc

ok

List<Manager>

ok

ok

ok

ok

List<*>

wc

wc

wc

ok

And you know why it’s safe!

Unlike Java, we can declare type variance!

Java

Kotlin

public interface
  List<E>
  extends Collection<E>
{...}
public interface
  List<out E> : Collection<E>
{...}
  • 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!

Why runtime check won’t do for mutable lists?

  • 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.

The story behind generics

  • 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

Raw types

Generic Type (source)

Raw Type (compiled)

class Pair<T> {
  private T first;
  private T second;
  Pair(T first,
       T second)
   {this.first = first;
    this.second = second;}
  T getFirst()
   {return first; }
  T getSecond()
   {return second; }
  void setFirst(T newValue)
   {first = newValue;}
  void setSecond(T newValue)
   {second = newValue;}
}
class Pair {
  private Object first;
  private Object second;
  Pair(Object first,
       Object second)
   {this.first = first;
    this.second = second;}
  Object getFirst()
   {return first; }
  Object getSecond()
   {return second; }
  void setFirst(Object newValue)
   {first = newValue;}
  void setSecond(Object newValue)
   {second = newValue;}
}

Boundary types instead of Object

Generic Type (source)

Raw Type (compiled)

class Pair<T extends Employee>{
  private T first;
  private T second;
  Pair(T first,
       T second)
   {this.first = first;
    this.second = second;}
  T getFirst()
   {return first; }
  T getSecond()
   {return second; }
  void setFirst(T newValue)
   {first = newValue;}
  void setSecond(T newValue)
   {second = newValue;}
}
class Pair {
  private Employee first;
  private Employee second;
  Pair(Employee first,
       Employee second)
   {this.first = first;
    this.second = second;}
  Employee getFirst()
   {return first; }
  Employee getSecond()
   {return second; }
  void setFirst(Employee newValue)
   {first = newValue;}
  void setSecond(Employee newValue)
   {second = newValue;}
}

Method calls

Source code

Compiled

Pair<Manager> buddies =
  new Pair<>();

/*type control
in compile time*/
buddies.setFirst(cfo);
buddies.setSecond(cto);

/*type cast is not needed*/
Manager buddy =
  buddies.getFirst();
Pair buddies =
  new Pair();

/*type control is not needed --
everything was checked at compile time!*/
buddies.setFirst(cfo);
buddies.setSecond(cto);

/*type cast inserted by compiler*/
Manager buddy =
  (Manager) buddies.getFirst();

Bridge methods to preserve polymorphism

Source code

Compiled

class DateInterval extends
 Pair<LocalDate> {

 @Override
 void setSecond(
        LocalDate second){
  if (second
   .compareTo(getFirst())>=0){
      super.setSecond(second);
  }
 }
}
class DateInterval extends Pair {

 void setSecond(
        LocalDate second){
  if (second
   .compareTo(getFirst())>=0){
      super.setSecond(second);
  }
 }

 //bridge method!!
 @Override
 void setSecond(Object second){
   setSecond((LocalDate) second);
 }
}

The same with Kotlin

Each lambda is compiled to an anonymous class which inherits from FunctionImpl and implements the corresponding invoke:

Source code

Compiled

{ (s: String): Int
    -> s.length }
object : FunctionImpl(), Function1<String, Int> {
   override fun getArity(): Int = 1

/* bridge */ fun invoke(p1: Any?): Any? = ...

override fun invoke(p1: String): Int = p1.length
}

Because of bridge methods, you cannot implement different parameterizations of the same interface in one class

Source code

Compiled

class Employee implements
  Comparable<Employee>{
  @Override
  int compareTo(Employee e){
    ...
  }
}
class Manager
  extends Employee
  implements
  Comparable<Manager> {
  @Override
  int compareTo(Manager m){
    ...
  }
}
class Manager
  extends Employee
  implements Comparable{

  //bridge method for Employee
  int compareTo(Object m) {
    return compareTo((Manager) m);
  }

  //bridge method for Manager
  int compareTo(Object e) {
    return compareTo((Employee) e);
  }

  //CLASH!!!
}

Summary: how it works

  • 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.

Main Type Erasure limitation

wc rt ok hp

rawtype

Java

Kotlin

if (a instanceof Pair<String>) ...
if (a is Pair<String>) ...

Main Type Erasure limitation

wc wc wc wc

rawtype

Java

Kotlin

if (a instanceof Pair<String>) ...
if (a is Pair<String>) ...

wc We don’t know the type parameter in the runtime!

Main Type Erasure limitation

ok ok ok ok

rawtype

Java

Kotlin

if (a instanceof Pair<?>) ...
if (a is Pair<*>) ...

ok Although we would like to know more than that…​

That’s why generics and arrays in Java are enemies

wc rt ok hp

List<String>[] a = new ArrayList<String>[10];

That’s why generics and arrays in Java are enemies

wc wc wc wc

List<String>[] a = new ArrayList<String>[10];

wc "Generic Array Creation", because such an array will not have the full type information about its elements!

Now look at this code

wc rt ok hp

Java

Kotlin

Pair<Integer> intPair =
       new Pair<>(42, 0);
Pair<?> pair = intPair;
Pair<String> stringPair =
        (Pair<String>) pair;
stringPair.b = "foo";
System.out.println(
        intPair.a * intPair.b);
var intPair = Pair<Int>(42, 0)
var pair: Pair<*> = intPair
var stringPair: Pair<String> =
           pair as Pair<String>
stringPair.b = "foo"
println(intPair.a * intPair.b)

Now look at this code

hp hp hp hp

Java

Kotlin

Pair<Integer> intPair =
       new Pair<>(42, 0);
Pair<?> pair = intPair;
Pair<String> stringPair =
        (Pair<String>) pair;
stringPair.b = "foo";
System.out.println(
        intPair.a * intPair.b);
var intPair = Pair<Int>(42, 0)
var pair: Pair<*> = intPair
var stringPair: Pair<String> =
           pair as Pair<String>
stringPair.b = "foo"
println(intPair.a * intPair.b)

hp intPair is spoiled, it’s not a pair of integer values!

What if we want this?

manempperson
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);

A naive approach in Java…​

wc rt ok hp

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);

A naive approach in Java…​

wc wc wc wc

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);

wc Won’t compile because of type invariance

The same naive approach in Kotlin…​

wc rt ok hp

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)

The same naive approach in Kotlin…​

wc wc wc wc

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)

wc Won’t compile because of type invariance

Java Covariant Wildcard Types

wildext
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);

ok If E is Employee, MyList<Manager> will do as MyList<? extends E>!!

Kotlin Covariant Use-Site Type Projections

outext
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)

ok If E is Employee, MyList<Manager> will do as MyList<out E>!!

Declaration-site covariance in Kotlin for read-only values

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!

Declaration-site covariance in Kotlin for read-only values

declsitecov
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)

ok MyImmutablePair<Manager> is assignable to MyImmutablePair<Employee>

So what can be done with an object typed ? extends in Java?

wc ok

<E> void  doSomething (MyList<? extends E> list) {
  E e1 = list.get(...);

So what can be done with an object typed ? extends in Java?

ok ok

<E> void  doSomething (MyList<? extends E> list) {
  E e1 = list.get(...);
}

ok Of course!

So what can be done with an object typed ? extends in Java?

wc ok

<E> void  doSomething (MyList<? extends E> list) {
  E e2 = ...;
  list.add(e2);
}

So what can be done with an object typed ? extends in Java?

wc wc

<E> void  doSomething (MyList<? extends E> list) {
  E e2 = ...;
  list.add(e2);
}

wc Why?

So what can be done with an object typed ? extends in Java?

wc ok

<E> void  doSomething (MyList<? extends E> list) {
  list.add(null);
}

So what can be done with an object typed ? extends in Java?

ok ok

<E> void  doSomething (MyList<? extends E> list) {
  list.add(null);
}

ok Why?

Unbounded wildcard in Java

  • 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…​

So what can be done with an object typed out in Kotlin?

wc ok

fun <E> doSomething (list: MyList<out E?>) {
    val e1: E? = list.get(...)
}

So what can be done with an object typed out in Kotlin?

ok ok

fun <E> doSomething (list: MyList<out E?>) {
    val e1: E? = list.get(...)
}

ok

So what can be done with an object typed out in Kotlin?

wc ok

fun <E> doSomething (list: MyList<out E?>) {
    val e2: E? = ...
    list.add(e2)
}

So what can be done with an object typed out in Kotlin?

wc wc

fun <E> doSomething (list: MyList<out E?>) {
    val e2: E? = ...
    list.add(e2)
}

wc

So what can be done with an object typed out in Kotlin?

wc ok

fun <E> doSomething (list: MyList<out E?>) {
    list.add(null)
}

So what can be done with an object typed out in Kotlin?

wc wc

fun <E> doSomething (list: MyList<out E?>) {
    list.add(null)
}

wc

We’ve been talking about covariancy so far. What about contravariancy?

Covariant<out E>

covclass

Contravariant<in E>

contravclass

Predicate<Person> can substitute Predicate<Employee> and Predicate<Manager>, and thus it can be considered as their subtype.

Java wildcard contravariant types

wildsup
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);

ok If E is Employee, MyList<Person> will do as MyList<? super E>!

Kotlin Contravariant Use-Site Type Projections

inext
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)

ok If E is Employee, MyList<Person> will do as MyList<in E>!

Declaration-site contravariance in Kotlin for write-only values

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.

Declaration-site contravariance in Kotlin for write-only values

declsitecov
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)

ok MyConsumer<Person> is assignable to MyConsumer<Employee>

So what can be done with an object typed ? super in Java?

wc ok

<E> void  doSomething (MyList<? super E> list) {
  E e1 = ...;
  list.add(e1);
  list.add(null);
}

So what can be done with an object typed ? super in Java?

ok ok

<E> void  doSomething (MyList<? super E> list) {
  E e1 = ...;
  list.add(e1);
  list.add(null);
}

ok

So what can be done with an object typed ? super in Java?

wc ok

<E> void  doSomething (MyList<? super E> list) {
  E e2 = list.get(...);
}

So what can be done with an object typed ? super in Java?

wc wc

<E> void  doSomething (MyList<? super E> list) {
  E e2 = list.get(...);
}

wc

So what can be done with an object typed ? super in Java?

wc ok

<E> void  doSomething (MyList<? super E> list) {
  Object e2 = list.get(...);
}

So what can be done with an object typed ? super in Java?

ok ok

<E> void  doSomething (MyList<? super E> list) {
  Object e2 = list.get(...);
}

ok

So what can be done with an object typed in in Kotlin?

wc ok

fun <E> doSomething (list: MyList<in E>) {
    val e1: E = ...
    list.add(e1)
}

So what can be done with an object typed in in Kotlin?

ok ok

fun <E> doSomething (list: MyList<in E>) {
    val e1: E = ...
    list.add(e1)
}

ok

So what can be done with an object typed in in Kotlin?

wc ok

fun <E> doSomething (list: MyList<in E>) {
    list.add(null)
}

So what can be done with an object typed in in Kotlin?

wc wc

fun <E> doSomething (list: MyList<in E>) {
    list.add(null)
}

wc

So what can be done with an object typed in in Kotlin?

wc ok

fun <E> doSomething (list: MyList<in E?>) {
    list.add(null)
}

So what can be done with an object typed in in Kotlin?

ok ok

fun <E> doSomething (list: MyList<in E?>) {
    list.add(null)
}

ok

So what can be done with an object typed in in Kotlin?

wc ok

fun <E> doSomething (list: MyList<in E>) {
    val first: Any = list.first()
}

So what can be done with an object typed in in Kotlin?

wc wc

fun <E> doSomething (list: MyList<in E>) {
    val first: Any = list.first()
}

wc

So what can be done with an object typed in in Kotlin?

wc ok

fun <E> doSomething (list: MyList<in E>) {
    val first: Any? = list.first()
}

So what can be done with an object typed in in Kotlin?

ok ok

fun <E> doSomething (list: MyList<in E>) {
    val first: Any = list.first()
}

ok

Star projection in Kotlin

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 :-)

Mnemonic rule for Java

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>)

Kotlin

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

Declaration-site variance is not just 'sugar'!

  • 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…​

A real life problem in KStreams API design

/* 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);

wc Although this::transformA will do, this::transformB will fail with "KStream<Employee> is not convertible to KStream<Person>"

What if we try to fix it…​

/* 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);

wc Both lines won’t compile with something like "KStream<capture of ? super Employee> is not convertible to KStream<Employee>"

Meanwhile in Kotlin…​

/* 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)

ok Everything will compile and run as intended!

Conclusions

  • Usage of ready-made generic types is straightforward

  • In order to create your own generic types, you MUST understand the key principles

Conclusions

  • 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 vs. Contravariance

Covariance
? extends
out
read-only

Contravariance
? super
in
write-only

covclass
contravclass

Covariance vs. Invariance vs. Contravariance

ab

Covariance

Invariance

Contravariance

To →

From ↓

C<A>

C<B>

C<A>

ok

wc

C<B>

ok

ok

To →

From ↓

C<A>

C<B>

C<A>

ok

wc

C<B>

wc

ok

To →

From ↓

C<A>

C<B>

C<A>

ok

ok

C<B>

wc

ok

Thanks for listening!

Producer Extends, Consumer Super

Producer Out, Consumer In

@inponomarev