What does in/out actually do in Kotlin when passed as arguments?

Solution 1:

Let me demonstrate what in/out do with the help of an example. Consider the following:

private fun foo(list: ArrayList<Number>) {}

private fun bar(list: ArrayList<Number>) {}

Now we try to pass an ArrayList to each function, each with a different generic type parameter:

// Error: Type Mismatch. Required `ArrayList<Number>` Found `ArrayList<Int>`
foo(arrayListOf<Int>())

// Error: Type Mismatch. Required `ArrayList<Number>` Found `ArrayList<Any>`
bar(arrayListOf<Any>())

But we get errors! How do we solve that? We have to tell the compiler somehow that, for foo the list can also contain elements of a subtype of Number (e.g. Int) and for bar we have to tell the compiler that the list can also contain elements of a basetype of Number (e.g. Any).

private fun foo(list: ArrayList<out Number>) {}

private fun bar(list: ArrayList<in Number>) {}

And now it works!

Further reading

Solution 2:

Type Projection

When the modifier in or out is used in the function parameter, it's called type projection.

Projections Produces Consumes Function Behaviour
ArrayList<out Orange> Orange Nothing Accepts subtypes of ArrayList<Orange>
ArrayList<in Orange> Any? Orange Accepts supertypes of ArrayList<Orange>
ArrayList<Orange> Orange Orange Doesn't accept any subtypes or supertypes

ArrayList in Kotlin is a producer as well as a consumer. This is because it's an invariant generic class defined as ArrayList<T> ,not as ArrayList<out T>(producer) or ArrayList<in T>(consumer). This means the class can have functions that accept T as a function parameter (consume) or return T (produce).

But what if you want to use this already existing class safely just as a consumer(in T) or just as a producer(out T)? Without worrying about the accidental use of the other unwanted functionality?

In that case, we project the types by using variance modifiers in and out at the use-site. Use-site simply means wherever we use the ArrayList<T> class.


out produces T and the function accepts subtypes

When you are using the ArrayList as a producer(out), the function can accept the subtypes of ArrayList<Orange>, that is, ArrayList<MandarinOrange>, ArrayList<BloodOrange>, since MandarinOrange and BloodOrange are subtypes of Orange. Because the subtyping is preserved:

fun useAsProducer(producer: ArrayList<out Orange>) {

    // Producer produces Orange and its subtypes
    val orange = producer.get(1)            // OK

    // Can use functions and properties of Orange
    orange.getVitaminC()                    // OK

    // Consumer functions not allowed
    producer.add(BloodOrange())             // Error
}

The producer produces Orange and its subtypes. Here producer.get(1) can return MandarinOrange, BloodOrange etc. but we don't care as long as we get an Orange. Because we are only interested in using the properties and functions of Orange at use-site.

Compiler doesn't allow calling the add() function (consumer) because we don't know which type of Orange it contains. You don't want to accidentally add BloodOrange, if this is an ArrayList<MandarinOrange>.


in consumes T and the function accepts supertypes

When you are using ArrayList as a consumer(in), the function can accept the supertypes of ArrayList<Orange>, that is, ArrayList<Fruit> because now the subtyping is reversed. This means ArrayList<Fruit> is a subtype of ArrayList<Orange> when Orange is a subtype of Fruit:

fun useAsConsumer(consumer: ArrayList<in Orange>) {

    // Produces Any?, no guarantee of Orange because this could
    // be an ArrayList<Fruit> with apples in it
    val anyNullable = consumer.get(1)       // Not useful

    // Not safe to call functions of Orange on the produced items.
    anyNullable.getVitaminC()               // Error

    // Consumer consumes Orange and its subtypes
    consumer.add(MandarinOrange())          // OK
}

The consumer consumes Orange and its subtypes. It doesn't matter whether it's a MandarinOrange or a BloodOrange as long as it's an Orange. Because the consumer is only interested in the properties and functions of Orange at its declaration-site.

The compiler does allow the call to get() function (producer) but it produces Any? which is not useful for us. The compiler flags an error when you use that Any? as an Orange at use-site.


Invariant produces and consumes T, the function doesn't accept subtypes or supertypes

When you are using ArrayList as a producer and consumer (without in or out), the function can accept only the exact type ArrayList<Orange>, no other subtypes like ArrayList<MandarinOrange> or supertypes like ArrayList<Fruit>. Because the subtyping is not allowed for invariants:

fun useAsProducerConsumer(producerConsumer: ArrayList<Orange>) {
    // Produces Orange and its subtypes
    val orange = producerConsumer.get(1)    // OK

    // Orange is guaranteed
    orange.getVitaminC()                    // OK

    // Consumes Orange and its subtypes
    producerConsumer.add(Orange())          // OK
}

The invariant produces and consumes Orange and its subtypes.


That's it! Type projection is all about telling the compiler how you are using that class in that particular function, so it can help you by flagging an error if you call unintended functions accidentally. Hope that helps.

Solution 3:

For in generic, we could assign a class of super-type to class of sub-type. But for out generic, we could assign a class of sub-type to class of super-type