Difference between "*" and "Any" in Kotlin generics
It may be helpful to think of the star projection as a way to represent not just any type, but some fixed type which you don't know what is exactly.
For example, the type MutableList<*>
represents the list of something (you don't know what exactly). So if you try to add something to this list, you won't succeed. It may be a list of String
s, or a list of Int
s, or a list of something else. The compiler won't allow to put any object in this list at all because it cannot verify that the list accepts objects of this type. However, if you attempt to get an element out of such list, you'll surely get an object of type Any?
, because all objects in Kotlin inherit from Any
.
From asco comment below:
Additionally
List<*>
can contain objects of any type, but only that type, so it can contain Strings (but only Strings), whileList<Any>
can contain Strings and Integers and whatnot, all in the same list.
The key to understanding the star projection(*
) is to properly understand the other two type projections in
and out
first. After that, the star projection becomes self-explanatory.
Understanding the problem
Let's say that you have this generic class Crate
that you intend to use for storing fruits. This class is invariant in T
. This means, this class can consume as well as produce T
(fruits). In other words, this class has functions that take T
as argument(consume) as well as return T
(produce). The size()
function is T-independent, it neither takes T
nor does it return T
:
class Crate<T> {
private val items = mutableListOf<T>()
fun produce(): T = items.last()
fun consume(item: T) = items.add(item)
fun size(): Int = items.size
}
But what if you want to use this already existing class just as a producer(out T
) or just as a consumer(in T
) or not want to use T
but just its T-independent functions like size()
? Without worrying about the accidental use of the unwanted functionality?
The solution is, we project the types by using variance modifiers out
, in
and *
at the use-site. Use-site simply means wherever we use the Crate
class.
out
projection is a producer of T
By projecting the Crate
as out
, you are telling the compiler: "give me an error when I accidentally use the Crate
class as a consumer of T
because I just want to safely use that class as a producer T
":
fun useAsProducer(producer: Crate<out Fruit>) {
// T is known to be out Fruit, so produces Fruit and its subtypes.
val fruit = producer.produce() // OK
// Fruit is guaranteed. Can use functions and properties of Fruit.
fruit.getColor() // OK
// Consumer not allowed because you don't want to accidentally add
// oranges, if this is a Crate<Apple>
producer.consume(Orange()) // Error
}
in
projection is a consumer of T
By projecting the Crate
as in
, you are telling the compiler: "give me an error when I accidentally use the Crate
class as a producer of T
because I just want to safely use that class as a consumer of T
":
fun useAsConsumer(consumer: Crate<in Orange>) {
// Produces Any?, no guarantee of Orange because this could
// be a Crate<Fruit> with apples in it.
val anyNullable = consumer.produce() // Not useful
// Not safe to call functions of Orange on the produced items.
anyNullable.getVitaminC() // Error
// T is known to be in Orange, so consumes Orange and its subtypes.
consumer.consume(MandarinOrange()) // OK
}
Star projection is no producer, no consumer of T
By projecting the Crate
as *
, you are telling the compiler: "give me an error when I accidentally use the Crate
class as a producer or consumer of T
because I don't want to use the functions or properties that consume and produce T
. I just want to safely use the T-independent functions and properties like size()
":
fun useAsStar(star: Crate<*>) {
// T is unknown, so the star produces the default supertype Any?.
val anyNullable = star.produce() // Not useful
// T is unknown, cannot access its properties and functions.
anyNullable.getColor() // Error
// Cannot consume because you don't know the type of Crate.
star.consume(Fruit()) // Error
// Only use the T-independent functions and properties.
star.size() // OK
}
Any
is not a projection
When you say Crate<Any>
, you are not projecting, you are simply using the original invariant class Crate<T>
as it is, which can produce as well as consume Any
:
fun useAsAny(any: Crate<Any>) {
// T is known to be Any. So, an invariant produces Any.
val anyNonNull = any.produce() // OK
// T is known to be Any. So, an invariant consumes Any.
any.consume(Fruit()) // OK
// Can use the T-independent functions and properties, of course.
any.size() // OK
}
The same is true for Crate<Apple>
or any other similar type without a variance modifier in
, out
, or *
, it will consume and produce that type (Apple
in that case). It's not a projection. This explains the difference between SomeGeneric<*>
and SomeGeneric<Any>
, you can compare the two code snippets above, side by side.
Star projection for declaration-site producer
So far, we saw the type projections out
, in
and *
for the Crate
class that was invariant at declaration-site: Crate<T>
. From here on, let's find out how the star projection behaves with the classes that are already in
and out
at the declaration-site with type parameter bounds:
Declaration-site
class ProducerCrate<out T : Fruit> {
private val fruits = listOf<T>()
fun produce() : T = fruits.last()
}
Use-site
fun useProducer(star: ProducerCrate<*>) {
// Even though we project * here, it is known to be at least a Fruit
// because it's an upper bound at the declaration-site.
val fruit = star.produce() // OK
// Fruit is guaranteed. Can use functions and properties of Fruit.
fruit.getColor() // OK
}
Star projection for declaration-site consumer
Declaration-site
class ConsumerCrate<in T> {
private val items = mutableListOf<T>()
fun consume(item: T) = items.add(item)
fun size(): Int = items.size
}
Use-site
fun useConsumer(consumer: ConsumerCrate<*>) {
// Cannot consume anything, because the lower bound is not supported
// in Kotlin and T is unknown
consumer.consume(Orange()) // Error
// Only useful for T-independent functions and properties.
consumer.size() // OK
}
Note that the lower bound is not supported in Kotlin. So, in the ConsumerCrate
class above, we cannot have something like in T super Orange
(lower bound) like we have out T : Orange
(upper bound).
Star projection for declaration-site invariant
Declaration-site
class ProducerConsumerCrate<T : Fruit> {
private val fruits = mutableListOf<T>()
fun produce(): T = fruits.last()
fun consume(fruit: T) = fruits.add(fruit)
}
Use-site
fun useProducerConsumer(producerConsumer: ProducerConsumerCrate<*>) {
// Even though we project * here, T is known to be at least a Fruit
// because it's the upper bound at the declaration-site.
val fruit = producerConsumer.produce() // OK
// Fruit is guaranteed. Can use functions and properties of Fruit.
fruit.getColor() // OK
// Consumer not allowed because you don't want to accidentally add
// oranges, if this crate is a Crate<Apple>.
producerConsumer.consume(Fruit()) // Error
}
Conclusion
Type projections for the invariant Crate<T>
:
Projections | Produces | Consumes | Behaviour |
---|---|---|---|
Crate<Fruit> |
Fruit |
Fruit |
Producer and Consumer |
Crate<out Fruit> |
Fruit |
Nothing |
Producer only |
Crate<in Fruit> |
Any? |
Fruit |
Consumer only |
Crate<*> |
Any? |
Nothing |
No Producer and No Consumer |
That's it! Hope that helps.