A type argument is a specific type, specified by the calling code, that a generic will use in place of a type parameter.
If your generic were a light fixture, then the light bulb that’s screwed into that light fixture is the type argument. Just as different kinds of light bulbs can be used in a light fixture’s socket, different call sites can each specify a different type argument to use in instances of a generic.
Examples
Type Declaration
val numbers: Array<Number> = arrayOf(1, 2, 3)
Here we’re specifying the type of numbers
. In this case, the type argument is Number
.
Function Call
Instead of specifying the type argument on the variable’s type declaration, you can specify it on the function call (or constructor invocation) instead. That looks like this:
val numbers = arrayOf<Number>(1, 2, 3)
Type Inference
In some cases, the compiler can infer the type argument based on the context. For example, you can omit the type argument in the examples above, like so:
val numbers = arrayOf(1, 2, 3)
In this case, there’s still a type argument; you just don’t have to explicitly declare it. In this case, the type argument is Int
, because all of the arguments to arrayOf()
are of type Int
.
This might or might not be what you wanted. For instance, you might have wanted an Array<Number>
instead of an Array<Int>
. In that case, you’d have to specify the type argument explicitly like we did in the first two examples.
Type Projection
Normally, generics are invariant, which means that an Array<Number>
is not a supertype of Array<Int>
, even though Number
is a supertype of Int
.
This might be inconvenient for you. For example, if you’re writing a function that takes an Array<Number>
, you might want to accept these types as well:
Array<Int>
Array<Long>
Array<Float>
Array<Double>
In this case, as long as you won’t need to set a value in the array (that is, as long as you’re only going to be reading values from it), you can achieve the subtyping you want by creating a type projection, by specifying a variance annotation on the type argument. For example:
fun toBytes(values: Array<out Number>) =
values.map(Number::toByte).toTypedArray()
We added the modifier out
to the type argument in order to create a type projection. The values
argument can now accept an Array
of any kind of Number
, including Long
and Double
:
val longs: Array<Long> = arrayOf(255L, 256L, 257L)
toBytes(longs).forEach { println(it) }
val doubles: Array<Double> = arrayOf(255.0, 256.0, 257.0)
toBytes(doubles).forEach { println(it) }
Type Argument vs. Type Parameters
Also, keep in mind that type arguments are not the same as type parameters – type arguments are the light bulbs and type parameters are the sockets: