vararg Parameters
Sometimes you don’t know in advance how many arguments a function call needs — printing a variable number of items, summing an unknown count of numbers, or building a message from whatever the caller happens to have on hand. Kotlin’s vararg modifier lets a single parameter accept zero, one, or many arguments of the same type, without forcing the caller to build a collection first.
It’s the same idea behind Java’s Object... varargs, but Kotlin makes it fully type-safe, lets you mix it with named arguments, and gives you the spread operator to unpack an existing array into vararg slots. This lesson covers exactly how it works, how the compiler treats it internally, and the mistakes that trip up almost everyone the first time.
Overview / How It Works
A parameter marked with the vararg keyword tells the compiler that the caller may pass any number of arguments of that type — including none — and they’ll all be collected into a single array-like value inside the function body. You still write ordinary function code; you just iterate over the parameter as if it were an array, because that is exactly what it is.
Only one parameter per function can be marked vararg. By convention it’s placed last, which is what lets you call the function with a clean, comma-separated list of values like sum(1, 2, 3). Kotlin does allow a vararg parameter that isn’t last, but every parameter that comes after it must then be passed by name at the call site — otherwise the compiler has no way to tell where the vararg arguments end and the next parameter begins.
Inside the function body, the type of a vararg parameter depends on what it holds. For the eight primitive types, Kotlin uses a specialized array class to avoid boxing every element: vararg n: Int becomes IntArray inside the function, vararg d: Double becomes DoubleArray, and so on. For any reference type, including generic type parameters, it becomes Array<out T>. The out matters: the array is exposed to you as read-only through this projected type, so you can iterate it, index-read from it, and call functions like .size or .joinToString(), but you can’t assign a new element in place through the vararg reference itself.
This is exactly how core standard-library functions like listOf(1, 2, 3), setOf("a", "b"), and arrayOf(...) work under the hood — they are ordinary functions with a vararg parameter, nothing magic about them.
The Spread Operator
If you already have an array and want to pass its contents into a vararg parameter, you can’t just hand the array over directly — the compiler expects individual elements of the declared type, not a whole array, and will reject it with a type mismatch. Instead, prefix the array with *, the spread operator: greet("Hi", *friends). This tells the compiler to unpack the array’s elements as if you had listed them individually. You can even mix a spread array with individual literal arguments in the same call, and the compiler merges everything into one array at the call site.
Syntax
fun functionName(vararg parameterName: Type): ReturnType {
// parameterName behaves like an array of Type here
}
| Part | Meaning |
|---|---|
vararg |
Marks the parameter as accepting a variable number of arguments (zero or more). |
parameterName |
The name used inside the function body to refer to the collected values. |
Type |
The element type every argument must match — the parameter itself becomes an array of this type. |
*array |
The spread operator, used at the call site to unpack an existing array into the vararg slot. |
| Position | Only one vararg parameter is allowed per function. If it isn’t last, later parameters must be passed by name. |
The array type you actually work with inside the function body depends on the element type:
| vararg element type | Type inside the function body |
|---|---|
Int |
IntArray |
Long |
LongArray |
Double |
DoubleArray |
Float |
FloatArray |
Byte |
ByteArray |
Short |
ShortArray |
Boolean |
BooleanArray |
Char |
CharArray |
Any reference type T |
Array<out T> |
Examples
Example 1: Summing an Unknown Number of Values
The simplest use of vararg: a function that adds up however many integers you give it, including none at all.
fun sum(vararg numbers: Int): Int {
var total = 0
for (n in numbers) {
total += n
}
return total
}
fun main() {
println(sum(1, 2, 3))
println(sum())
println(sum(10))
}
6
0
10
sum(1, 2, 3) packs the three arguments into an IntArray of [1, 2, 3], and the loop adds them up to 6. sum() with no arguments still produces a valid, empty IntArray — not null — so the loop body simply never runs and total stays at its initial value of 0. Notice total is declared with var because it’s reassigned on every iteration, while numbers is never reassigned, only read.
Example 2: Mixing vararg with a Fixed Parameter and the Spread Operator
A vararg parameter can sit alongside ordinary parameters, as long as it comes last. This example also shows unpacking an existing array with *.
fun greet(greeting: String, vararg names: String) {
for (name in names) {
println("$greeting, $name!")
}
}
fun main() {
greet("Hello", "Alice", "Bob", "Charlie")
val friends = arrayOf("Dave", "Eve")
greet("Hi", *friends)
}
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Hi, Dave!
Hi, Eve!
greeting is filled positionally by the first argument, and everything else supplied after it goes into names. friends is a real Array<String> built with arrayOf; passing it as greet("Hi", friends) would fail to compile, since a single Array<String> is not itself a String. Prefixing it with * spreads its two elements into the vararg slot instead.
Example 3: A Generic vararg Function
vararg isn’t limited to one concrete type — combine it with a type parameter and the same function works for any type at all.
fun <T> printAll(vararg items: T) {
for (item in items) {
println(item)
}
}
fun main() {
printAll(1, 2, 3)
printAll("a", "b")
}
1
2
3
a
b
The compiler infers T separately for each call: T is Int in the first call and String in the second. Inside the function, items has type Array<out T> either way — this is exactly the technique kotlin.collections.listOf uses to accept any element type.
How It Works Step by Step
- At the call site, the compiler collects every argument that lines up with the vararg parameter and builds a single array holding all of them, in the order given.
- If a spread argument (
*array) is present, its elements are copied into that same array alongside any individual literal arguments — exactly one array reaches the function, no matter how many spreads or literals you combined at the call site. - If you pass nothing for the vararg parameter, you still get a valid array with size 0, never
null. That’s whysum()in Example 1 safely loops zero times instead of crashing. - Inside the function, the parameter is bound to that array. For primitive element types this is a specialized array class (
IntArray,DoubleArray, and so on); for everything else it’sArray<out T>. - The
outprojection means the array is exposed to you as producer-only through the vararg reference: you can readnumbers[0], iterate with aforloop, or call read-only members like.size, but you cannot writenumbers[0] = xthrough that same reference. - From there the function body runs like any other — the array is just another value in scope, ready to be looped over, transformed, or passed along.
Common Mistakes
Mistake 1: Passing an Array Without the Spread Operator
It’s tempting to assume an array can be handed straight to a vararg parameter, since a vararg becomes an array internally. It can’t — the parameter’s declared type, as seen by the caller, is still the element type, not an array of it.
fun sum(vararg numbers: Int): Int {
var total = 0
for (n in numbers) {
total += n
}
return total
}
fun main() {
val nums = intArrayOf(1, 2, 3)
println(sum(nums)) // Compile error: type mismatch, expected Int
}
sum expects each argument to be an Int; nums is a whole IntArray, so the compiler rejects the call with a type mismatch before it ever runs. The fix is the spread operator:
fun sum(vararg numbers: Int): Int {
var total = 0
for (n in numbers) {
total += n
}
return total
}
fun main() {
val nums = intArrayOf(1, 2, 3)
println(sum(*nums))
}
6
Mistake 2: Parameters After a Non-Last vararg Without Named Arguments
When a vararg parameter isn’t the last one, Kotlin requires every parameter after it to be supplied by name. Calling positionally instead produces confusing errors, because trailing positional arguments are assumed to belong to the vararg.
fun connect(vararg hosts: String, port: Int) {
println("Connecting to ${hosts.joinToString()} on port $port")
}
fun main() {
connect("host1", "host2", 8080) // Compile error: type mismatch and missing 'port'
}
The compiler tries to fold "host1", "host2", and 8080 all into hosts since they’re positional and hosts is a vararg — but 8080 isn’t a String, and port is left with no value at all. Naming port explicitly fixes it:
fun connect(vararg hosts: String, port: Int) {
println("Connecting to ${hosts.joinToString()} on port $port")
}
fun main() {
connect("host1", "host2", port = 8080)
}
Connecting to host1, host2 on port 8080
Mistake 3: Treating the vararg Parameter as a Mutable List
Because a vararg parameter behaves like a collection when you read it, it’s easy to forget it’s an Array, not a MutableList — and arrays in Kotlin have no add function, since their size is fixed once created.
fun collectTags(vararg tags: String) {
tags.add("extra") // Compile error: unresolved reference 'add'
}
tags is an Array<out String>, and Array simply has no add member — that’s a MutableList operation. To grow the collection, convert it first with toMutableList():
fun collectTags(vararg tags: String): List<String> {
val result = tags.toMutableList()
result.add("extra")
return result
}
fun main() {
println(collectTags("a", "b"))
}
[a, b, extra]
Best Practices
- Put the
varargparameter last whenever possible — it keeps call sites clean and avoids forcing named arguments on everything that follows it. - Reach for
varargwhen callers typically have loose, individual values to pass (likeprintln(a, b, c)-style calls); prefer aList<T>parameter when callers usually already hold a collection, since you can’t spread aList— only anArraysupports the*operator. - Remember only one parameter per function may be
vararg; if you need two independent variable-length groups, accept collections instead. - Treat the vararg parameter as read-only inside the function unless you deliberately convert it with
toMutableList()or similar — itsArray<out T>type won’t let you assign into it by index anyway. - When a
varargisn’t last, always call with the trailing parameters named — it also makes the call site more self-documenting. - For a handful of fixed common arities (0, 1, 2 arguments), consider whether plain overloads read better than forcing every call through vararg’s array allocation; the standard library does this internally for some of its own hot paths.
Practice Exercises
- Write a function
fun largest(vararg numbers: Int): Int?that returns the largest value passed in, ornullif no arguments were given (don’t use!!). Test it withlargest(3, 7, 2)andlargest(). - Write a function
fun joinWithPrefix(prefix: String, vararg words: String): Stringthat returns a single string whereprefixis prepended to each word, separated by spaces. ForjoinWithPrefix("#", "kotlin", "vararg")the expected output is#kotlin #vararg. - Given
val existing = arrayOf("red", "green"), call yourjoinWithPrefixfunction above using the spread operator to passexistingalong with one extra literal word,"blue", in the same call.
Summary
vararglets a parameter accept zero or more arguments of one type, collected into an array inside the function.- Only one
varargparameter is allowed per function; if it isn’t last, later parameters must be passed by name. - Primitive element types become specialized arrays (
IntArray,DoubleArray, etc.); reference types becomeArray<out T>, which is read-only through the vararg reference. - The spread operator
*unpacks an existing array into a vararg call — you cannot pass the array directly. - Calling with zero arguments produces a valid, empty array, never
null. varargcombines cleanly with generics, letting one function accept any element type.- A vararg parameter is an
Array, not aMutableList— convert withtoMutableList()if you need to grow it.
