What are the in and out positions in Kotlin Generics?

Viewed 173

I want to start with what I know, or at least I think I know, so what I'm asking would be more clear.

  • First of all, I know that you can declare a variable of a supertype and assign an object of a subtype to take advantage of polymorphism with Inheritence and Interfaces.

  • I know that generics provide type safety because the type parameters are invariant by definition, so where A is a subtype of B, Foo<A> is not necessarily a subtype of Foo<B>, and may not be used in place depending on mutability of the object. With this, possible exceptions that could arise at runtime due to dynamic dispatching can be caught in compile time.

  • They also help to define a generic logic for different types: Like in Lists where you have collections of type A objects, but it doesn't change the implementation for type B objects.

  • Also, I understood why MutableList<String> doesn't count as the subtype of MutableList<Any> because that could result in cases where you create a variable with type MutableList<Any> that holds a reference to a MutableList<String> object, and add an Int element to a List of Strings, which is obviously a problem.

  • I also understood why List version of the previous example works because Lists are immutable so you can't make any modification to the object that could result in type mismatches.

  • Lastly, I know that type parameters with in can only be used as function parameters, being consumed, and the ones with out can be used as the function return types, being produced.


Now to the part what I don't understand:

  • I didn't quite understand what the words consumer and producer actually means in terms of in and out. What does it mean for a type to be in consumed or produced position? Does that mean the object with that type can only be read or write only? Does that have anything to do with the object at all?

  • What would be the behaviour of the object if, let's say, we don't define it using in or out, or, opposite, we define it using in or out, not talking about the subtype-supertype relationship that I explained above.


I spend the last few days looking at different explanations of this, but I found the lack of examples a big problem, especially because that's how I usually learn.
I can use these concepts in code, but the lack of underlying knowledge or the logic greatly disturbs me, so please, if you decide to take the time to write an explanation, provide it with examples and counter examples of why or how a certain idea works.

1 Answers

Just one correction to your first bullet points: List is not immutable; it is read-only. A List could be an up-cast mutable implementation and some other object that references it could be mutating it.

Producer means the generic type appears as a return type in any functions or properties of the object. You can get T’s out of a List, for instance.

Consumer means the generic type appears as a parameter of any functions or as the type of any var properties of the object. You can put T’s into a MutableList, for example.

Since List produces but doesn’t consume (it doesn’t have any functions with T as a parameter), its type is marked as producing-only, aka covariant, aka out right at the declaration site so its type can always be assumed to be out wherever it’s used even if the out keyword is not used.

Since the List type is always covariant out, any List can be safely upcast to a List where the type is a supertype of the originating type. A List<String> can be cast to List<CharSequence> because any item you get out of it (anything it produces) is going to be a String, and therefore also qualifies as the supertype CharSequence.

The reverse logic would apply for something that is purely a consumer with the type marked in, but it’s harder to come up with a simple example where you would actually have a useful object like this.

A MutableList both produces and consumes, so it is invariant by default, but since it is also a List, a MutableList<String> could be safely cast to a List<CharSequence>. If you have a reference to the List<CharSequence>, you can get CharSequences out of it. The underlying object might continue to have new Strings put into it from the original reference.

Related