As in Java, classes in Kotlin may have type parameters:
In general, to create an instance of such a class, one needs to provide the type parameters:
But if the parameters may be inferred, e.g. from the constructor arguments or by some other means, one is allowed to omit the type parameters:
One of the most tricky parts of Java's type system is wildcard types (see Java Generics FAQ). And Kotlin doesn't have any. Instead, it has two other things: declaration-site variance and type projections.
First, let's think about why Java needs those mysterious wildcards. The problem is explained in Effective Java, Item 28: Use bounded wildcards to increase API flexibility. First, generic types in Java are invariant, meaning that List<String> is not a subtype of List<Object>. Why so? If List was not invariant, it would have been no better than Java's arrays, cause the following code would have compiled and cause an exception at runtime:
So, Java prohibits such things in order to guarantee run-time safety. But this has some implications. For example, consider the addAll() method from Collection interface. What's the signature of this method? Intuitively, we'd put it this way:
But then, we would not be able to do the following simple thing (which is perfectly safe):
That's why the actual signature of addAll() is the following:
The wildcard type argument ? extends T indicates that this method accepts a collection of objects of some subtype of T, not T itself. 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. In return for this limitation, we have the desired behaviour: Collection<String> is a subtype of Collection<? extends Object>. In "clever words", the wildcard with an extends-bound (upper bound) makes the type covariant.
The key to understanding why this trick works is rather simple: if you can only take items from a collection, then a collection of String's and reading Object's from it is fine. Conversely, if you can only put items into the collection, it's OK to take a collection of Object's and put String's into it: in Java we have List<? super String> a supertype of List<Object>. The latter is called contravariance, and you can only call methods that take String as an argument on List<? super String> (e.g., you can call add(String) or set(int, String)), while if you call something that returns T in List<T>, you don't get a String, but an Object.
Joshua Bloch calls those objects you only read from producers, and those you only write to – consumers. He recommends: "For maximum flexibility, use wildcard types on input parameters that represent producers or consumers", and proposes the following mnemonic:
PECS stands for producer-extends, consumer-super.
NOTE: if you use a producer-object, say, List<? extends Foo>, you are not allowed to call add() or set() on this object, but this does not mean that this object is immutable: for example, nothing prevents you to call clear() to remove all items from the list, since clear() does not take any parameters at all. The only thing guaranteed by wildcards (or other types of variance) is type safety. Immutability is a completely different story.
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:
Then, it would be perfectly safe to store a reference to an instance of Source<String> in a variable of type Source<Object> – there're no consumer-methods to call. But Java does not know this, and still prohibits:
To fix this, we have to declare objects of type Source<? extends Object> that is sort of meaningless, because we can call all the same methods on such a variable, as before, so there's no value added by the more complex type. But the compiler does not know that.
In Kotlin, there is a way to explain this sort of thing to the compiler. This is called declaration-site variance: we can annotate the type parameter T of Source to make sure that it is only returned (produced) from members of Source<T>, and never consumed. To do this we provide the out modifier:
The general rule is: when a type parameter T of a class C is declared out, it may occur only in out-position in the members of C, but in return C<Base> can safely be a supertype of C<Derived>.
In "clever words" they say that the class C is covariant in the parameter T, or that T is a covariant type parameter. You can think of C as being a producer of T's, and NOT a consumer of T's.
The out modifier is called a variance annotation, and 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.
In addition to out, Kotlin provides a complementary variance annotation: in. It makes a type parameter contravariant: it can only be consumed and never produced. A good example of a contravariant class is Comparable:
We believe that the word in and out are self-explainig (as they were successfully used in C# for quite some time already), thus the mnemonic mentioned above is not really needed, and one can rephrase it for a higher purpose:
The Existential Transformation: Consumer in, Producer out!
It is very convenient to declare a type parameter T as out and have no trouble with subtyping on the use site. Yes, it is, when the class in question can actually be restricted to only return T's, but what if it can't? A good example of this is Array:
This class cannot be either co- or contravariant in T. And this imposes certain inflexibilities. Consider the following function:
This function is supposed to copy item from one array to another. Let's try to apply it in practice:
Here we run into the same familiar problem: Array<T> is invariant in T, thus neither of Array<Int> and Array<Any> is a subtype of the other. Why? Again, because copy might be doing bad things, i.e. it might attempt to write, say, a String to from, and if we actually passed an array of Int there, a ClassCastException would have been thrown sometime later...
Then, the only thing we want to ensure is that copy() does not do any bad things. We want to prohibit it to write to from, and we can:
What has happened here is called type projection: we said that from is not simply an array, but a restricted (projected) one: we can only call those methods that return the type parameter T, in this case it means that we can only call get(). This is our approach to use-site variance, and corresponds to Java's Array<? extends Object>, but in a little simpler way.
You can project a type with in as well:
Array<in String> corresponds to Java's Array<? super String>, i.e. you can pass an array of CharSequence or an array of Object to the fill() function.
Sometimes you want to say that you know nothing about the type argument, but still want to use it in a safe way. The safe way here is to say that we are dealing with an out-projection (the object does not consume any values of unknown types), and that this projection is with the upper-bound of the corresponding parameter, i.e. out Any? for most cases. Kotlin provides a shortahnd syntax for this, that we call a star-projection: Foo<*> means Foo<out Bar> where Bar is the upperbound for Foo's type parameter.
Note: star-projections are very much like Java's raw types, but safe.
Not only classes can have type parameters. Functions can, too. Usually, one places the type parameters in angle brackets after the name of the function:
If type parameters are passed explicitly at the call site, they can be only specified after the name of the function:
The set of all possible types that can be substituted for a given type parameter may be restricted by generic constraints.
The most common type a constraint is an upper bound that corresponds to Java's extends keyword:
The type specified after a colon is the upper bound: only subtype of Comparable<T> may be substituted for T. For example
The default upper bound (if none specified) is Any?. There can be not more than one upper bound specified directly inside the angle brackets. If same type parameter needs more than one upper bound, we need a separate where-clause:
Another type of generic constraints are class object constraints. They restrict the properties of a class object of the root class of a type being substituted for T.
Consider the following example. Suppose, we have a class Default that has a property default that holds a default value to be used for this type:
For example, the class Int could extend Default in the following way:
Now, let's consider a function that takes a list of nullable T's, i.e. T?, and replaces all the null's with the default values:
For this function to compile, we need to specify a type constraint that requires a class object of T to be of a subtype of Default<T>:
Now the compiler knows that T (as a class object reference) has the default property, and we can access it.
|Class object bounds are not supported yet|
See the corresponding issue.