Java Varargs: Variable-Length Argument Lists

Varargs (variable-length argument lists) let a method accept any number of arguments of the same type without the caller constructing an array manually. Declared with the ... syntax, a varargs parameter is treated as an array inside the method body. This article covers varargs syntax, how the JVM implements them, ordering rules, overloading interactions, passing existing arrays, and the heap-pollution warning that arises with generic varargs.

What Are Varargs and Why Do They Exist?

Before Java 5, a method that needed to accept an arbitrary number of values of the same type had two options: declare several overloads (log(String), log(String, String), log(String, String, String)…) or require the caller to pass an explicit array. Both approaches are cumbersome: overloads explode in number, and array construction adds noise at every call site.

Varargs, introduced in Java 5, let you write void log(String... messages) and call it as log("a"), log("a", "b"), log("a", "b", "c"), or even log() with zero arguments. The compiler synthesises the array creation at each call site, so the caller never needs to think about it. The JVM itself has no special varargs instruction: String... is just sugar for String[] at the bytecode level.

Familiar APIs that use varargs: String.format(String, Object...), List.of(E...), Arrays.asList(T...), System.out.printf(String, Object...), and JUnit's Assertions.assertAll(Executable...).

Varargs Syntax and the Array Under the Hood

A varargs parameter is declared with three dots immediately after the type name. It must be the last parameter in the method signature: only one varargs parameter is allowed per method, and it must come after all fixed parameters. Inside the method body, the varargs parameter is an ordinary array of the declared type; you can iterate it with a for-each loop, check its length, or pass it to any method that accepts an array.

Java
// Declare a varargs method
public static int sum(int... numbers) {
    // 'numbers' is an int[] inside the method body
    int total = 0;
    for (int n : numbers) {
        total += n;
    }
    return total;
}

// Call with zero, one, many arguments — all valid
System.out.println(sum());           // 0
System.out.println(sum(5));          // 5
System.out.println(sum(1, 2, 3));    // 6
System.out.println(sum(10, 20, 30, 40)); // 100

// Mixing fixed and varargs parameters — varargs must be last
public static String format(String prefix, Object... values) {
    StringBuilder sb = new StringBuilder(prefix).append(": ");
    for (int i = 0; i < values.length; i++) {
        if (i > 0) sb.append(", ");
        sb.append(values[i]);
    }
    return sb.toString();
}

System.out.println(format("Items", "apple", 42, true));
// Items: apple, 42, true
The compiler creates a new array on every call at the call site. Calling a varargs method in a tight loop allocates an array each iteration. For performance-critical hot paths, consider providing a fixed-arity overload alongside the varargs version (as List.of(E e1, E e2, ...) does up to 10 elements).

Passing an Existing Array to a Varargs Method

Because a varargs parameter is just an array at the bytecode level, you can pass an existing array directly: no wrapping required. The compiler detects that the argument type matches the array type and passes it through without creating a new array. This also means you can store varargs arguments in a variable and pass the same array to multiple calls.

Java
int[] existing = {1, 2, 3, 4, 5};
System.out.println(sum(existing)); // 15 — array passed directly, no copy

String[] names = {"Alice", "Bob", "Carol"};
System.out.println(String.join(", ", names)); // Alice, Bob, Carol

// Caution: modifying the array inside the method affects the original
public static void zeroOut(int... nums) {
    Arrays.fill(nums, 0); // modifies the underlying array
}
int[] data = {1, 2, 3};
zeroOut(data);
System.out.println(Arrays.toString(data)); // [0, 0, 0] — original was modified!
When you pass an existing array to a varargs method, the method receives a reference to the same array, not a copy. Mutations inside the method affect the original. If the method must not modify the caller's data, document that contract clearly or copy the array at the start of the method.

Varargs and Method Overloading

Varargs interact with method overloading in a potentially confusing way. The compiler treats a varargs method as a lower-priority candidate during overload resolution. If a fixed-arity overload matches a call exactly, the fixed-arity version is chosen. Only when no fixed-arity overload matches does the compiler consider the varargs option. This means you can provide both a sum(int a, int b) and a sum(int... numbers) and calls with exactly two arguments will always go to the two-parameter version.

Java
public static String describe(Object obj) {
    return "single: " + obj;
}
public static String describe(Object... objs) {
    return "varargs: " + Arrays.toString(objs);
}

System.out.println(describe("hello"));        // single: hello   (exact match)
System.out.println(describe("a", "b"));       // varargs: [a, b]
System.out.println(describe((Object) null));   // single: null   (explicit cast)
System.out.println(describe((Object[]) null)); // varargs called with null array
                                               // — 'objs' will be null inside method!

Note: Calling a varargs method with a null literal without a cast is ambiguous and produces a compiler warning. Cast to (Object[]) null to pass a null array, or (Object) null to pass a single-element array containing null, depending on your intent.

Generic Varargs and @SafeVarargs

When a varargs parameter has a generic type, for example T... items or List<String>... lists, the compiler emits an "unchecked or unsafe operations" warning. This warning exists because of how Java erases generic types at runtime. At the bytecode level, the varargs array becomes Object[], and storing elements of an incompatible type into it can corrupt the array in ways the compiler cannot detect: a condition called heap pollution.

If your generic varargs method does nothing unsafe with the array: specifically, if it only reads from it and does not store foreign-typed values into it or expose the array reference externally: you can suppress the warning with @SafeVarargs. This annotation is a promise to the compiler (and human readers) that the method is safe. It can only be applied to static, final, or private methods, because overridable methods cannot guarantee the safety of every possible override.

Java
// Safe — only reads from the varargs array, never writes foreign types into it
@SafeVarargs
public static <T> List<T> listOf(T... items) {
    return new ArrayList<>(Arrays.asList(items));
}

List<String> names = listOf("Alice", "Bob", "Carol");

// UNSAFE example — do NOT do this (heap pollution)
@SuppressWarnings("unchecked") // needed because @SafeVarargs cannot be used here
static <T> T[] dangerousCombine(T[]... arrays) {
    // Returning a generic array exposes the Object[] to callers
    // who expect T[] — ClassCastException at the call site
    return arrays[0]; // unsafe — do not expose the raw array
}
The standard library uses @SafeVarargs extensively: Collections.addAll, EnumSet.of, and List.of all carry it. If your utility method follows the same read-only access pattern, add @SafeVarargs to keep your API clean and warning-free for callers.