Generics & Variance in Java vs Kotlin

Prashant
4 min readMay 24, 2020

--

What are Generics?

Generics allows us to declare a type (Integer, String, User-defined types, ..etc) to be a parameter of a class or method.

Example:

Some classes that implement Generics are:

  1. HashMap<String> ✔️
  2. ArrayList<Integer> ✔️
  3. Set<Integer> .. etc ✔️

Defining primitive types to Generics is not possible, We have to pass Objects or Wrappers while defining our classes, methods or interface. For Eg:

  1. HashMap<int> ❌
  2. ArrayList<char> ❌

How to declare Generics?

class Box<T>(t: T) {
var value = t
}

Variance in Java and Kotlin

Have you ever wondered why Java needs those strange wildcards like

  1. ? — This wildcard allows all types of the variable
  2. ? extends T — Wildcard with an upper bound T meaning: Only types that are subtypes of T are all allowed
  3. ? super T — Wildcard with a lower bound meaning: Only types that are supertypes of T are allowed only

I hope Java wildcards are pretty much clear to you.

Generic Types in JAVA

  1. Invariant — Where two objects are not equal. This might not be clear with just a definition. For eg:

List<String> is not a subtype of List<Object>

Suppose this List is not invariant then, It’ll accept both String and Object which will throw a compile-time exception.

List<String> strs = new ArrayList<String>();List<Object> objs = strs; // Java prohibits thisobjs.add(1); // Here we put an Integer into a list of StringsString s = strs.get(0); // !!! ClassCastException: Cannot cast //Integer to String

2. Co-variant — Which accepts objects of T or subtype of T (? extends T)

Collection<String> is a subtype of Collection<Object>

This means that we can safely read T’s from items (elements of this collection are instances of a subclass of T), but cannot write to it since we do not know what objects comply to that unknown subtype of T.

3. Contra-variant — We can only call methods that accept T as an argument.

For Eg:

List<? super String>

We can only call those methods which return String like .add() as it accepts String, but what if we call any returning method that returns String it’ll throw a compile-time error.

Note: Wildcards are only used for type-safety in Java.

Variance in Kotlin

Let’s understand this with the help of an example.

Suppose we have a generic interface Source<T> that does not have any methods that take T as a parameter, only methods that return T

interface Source<T> {   T nextT();}void demo(Source<String> str) {   Source<Object> obj = str; // Java doesn’t allow this}

To fix this, We’ve to change Generic type to ? extends Object, which is sort of useless.

PS: Just java things :)

In Kotlin, There’s a solution for the same, which is declaration-site variance. In which we annotate the type parameter of the Class/Interface to make sure that the object is only returned and not consumed. This is done by defining the type as out (variance annotation). Like:

interface Source<out T> {   fun nextT(): T}fun demo(strs: Source<String>) {   val objects: Source<Any> = strs // ✔, since T is an out-parameter}

Since it is provided at the type parameter declaration site, we talk about declaration-site variance. This is in contrast with Java’s use-site variance where wildcards in the type usages make the types covariant.

Just similar to out variance annotation Kotlin also provides an in variance annotation, It makes type parameter contravariant (? super T)

Different types of type-parameter in Kotlin

There might be some cases where we don’t know about the type argument beforehand.

Here comes Kotlin to our rescue with star projections.

Kotlin provides the so-called star-projection syntax for this:

  1. For Foo<out T : TUpper>, where T is a covariant type parameter with the upper bound TUpper, Foo<*> is equivalent to Foo<out TUpper>. It means that when the T is unknown you can safely read values of TUpper from Foo<*>.
  2. For Foo<in T>, where T is a contravariant type parameter, Foo<*> is equivalent to Foo<in Nothing>. It means there is nothing you can write to Foo<*> in a safe way when T is unknown.
  3. For Foo<T : TUpper>, where T is an invariant type parameter with the upper bound TUpper, Foo<*> is equivalent to Foo<out TUpper> for reading values and to Foo<in Nothing> for writing values.

If a generic type has several type parameters each of them can be projected independently. For example, if the type is declared as interface Function<in T, out U> we can imagine the following star-projections:

Function<*, String> means Function<in Nothing, String>;Function<Int, *> means Function<Int, out Any?>;Function<*, *> means Function<in Nothing, out Any?>.

Note: In Kotlin a star projection MutableList<*> combines both out Any? and in Nothing projections, and the latter means that you cannot pass anything at all to the methods where the type is unknown (Nothing is the type that has no value).

Source: Koltin Doc

Summary

In this article, We discussed Generics and different type of variance

  1. Invariant
  2. CoVariant
  3. ContraVariant

in Java and Kotlin. In addition to this, We checkout in and out type parameters and Star Projections in Kotlin to tackle the problem and make the code short as compared to Java.

--

--

Prashant

SDE - II @FloBiz | Ex-Internshala | Mentor @ hackCBS 2.0 | Full Stack Developer | Freelancer | Open-Source Contributer