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

  1. 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.
  2. 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.
  3. If you pass nothing for the vararg parameter, you still get a valid array with size 0, never null. That’s why sum() in Example 1 safely loops zero times instead of crashing.
  4. 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’s Array<out T>.
  5. The out projection means the array is exposed to you as producer-only through the vararg reference: you can read numbers[0], iterate with a for loop, or call read-only members like .size, but you cannot write numbers[0] = x through that same reference.
  6. 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 vararg parameter last whenever possible — it keeps call sites clean and avoids forcing named arguments on everything that follows it.
  • Reach for vararg when callers typically have loose, individual values to pass (like println(a, b, c)-style calls); prefer a List<T> parameter when callers usually already hold a collection, since you can’t spread a List — only an Array supports 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 — its Array<out T> type won’t let you assign into it by index anyway.
  • When a vararg isn’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, or null if no arguments were given (don’t use !!). Test it with largest(3, 7, 2) and largest().
  • Write a function fun joinWithPrefix(prefix: String, vararg words: String): String that returns a single string where prefix is prepended to each word, separated by spaces. For joinWithPrefix("#", "kotlin", "vararg") the expected output is #kotlin #vararg.
  • Given val existing = arrayOf("red", "green"), call your joinWithPrefix function above using the spread operator to pass existing along with one extra literal word, "blue", in the same call.

Summary

  • vararg lets a parameter accept zero or more arguments of one type, collected into an array inside the function.
  • Only one vararg parameter 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 become Array<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.
  • vararg combines cleanly with generics, letting one function accept any element type.
  • A vararg parameter is an Array, not a MutableList — convert with toMutableList() if you need to grow it.