Varargs

Varargs let a method accept a variable number of arguments of the same type by writing a single parameter with a trailing ellipsis, which the compiler quietly turns into an array.

Variable-length argument lists (varargs), introduced in Java 5, let you declare a method that accepts zero, one, or many arguments of a given type without writing a family of overloads for every possible count. Internally the compiler packages the arguments into an array, so inside the method a varargs parameter behaves exactly like an array parameter — the syntax at the call site is just sugar over that array.

What Varargs Are and the Syntax

A varargs parameter is declared with three dots (...) after the element type instead of square brackets, and it may only be the last parameter in the method's parameter list.

sum() compiles fine because varargs accepts zero arguments too — this is the key difference from a required array parameter, which cannot be omitted at a call site.

class Summer {
    static int sum(int... numbers) {
        int total = 0;
        for (int n : numbers) {
            total += n;
        }
        return total;
    }

    public static void main(String[] args) {
        System.out.println(sum());           // 0    — zero arguments
        System.out.println(sum(5));          // 5    — one argument
        System.out.println(sum(1, 2, 3, 4)); // 10   — many arguments
    }
}

Varargs Is an Array Under the Hood

Inside the method body, a varargs parameter is a genuine array — you can index it, take its .length, iterate it with a for-each loop, or pass it on to another method expecting int[]. You can also call the method by passing an already-built array directly instead of individual arguments.

Both call styles compile against the same varargs method because the compiler treats T... as equivalent to T[] in the method's actual signature.

static int sum(int... numbers) {
    System.out.println("length: " + numbers.length);
    return java.util.Arrays.stream(numbers).sum();
}

public static void main(String[] args) {
    int[] data = {10, 20, 30};
    System.out.println(sum(data));    // pass the array directly — no repackaging
    System.out.println(sum(1, 2, 3)); // pass individual args — compiler builds the array
}

Zero-Argument Calls and Empty Arrays

Calling a varargs method with no trailing arguments produces a zero-length array, never null. This is a subtle but important guarantee — you can safely call .length on a varargs parameter without a null check as long as no one explicitly passed null as the array argument.

Passing an explicit null — sum((int[]) null) — does make the parameter null, bypassing the zero-length-array guarantee. This requires an explicit cast to disambiguate from a zero-argument call, since bare null is ambiguous between "no arguments" and "a null array argument".

Varargs Combined With Other Parameters

A method may mix ordinary parameters with a trailing varargs parameter, but the varargs parameter must come last — the compiler rejects a varargs parameter anywhere else because it would make the boundary between fixed and variable arguments ambiguous.

class Formatter {
    // Valid — varargs is last
    static String join(String separator, String... parts) {
        return String.join(separator, parts);
    }

    // INVALID — varargs must be the final parameter; won't compile
    // static String bad(String... parts, String separator) { }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Formatter.join(", ", "a", "b", "c")); // a, b, c
    }
}

Overload Interactions

Varargs methods are considered last in overload resolution (Phase 3), after fixed-arity exact/widening matches (Phase 1) and boxing matches (Phase 2). This means a matching fixed-arity overload is always preferred over a varargs overload when both could apply.

Declaring two overloaded varargs methods that could both match the same call (e.g., f(int...) and f(String...) called with no arguments) can produce an ambiguous call — avoid stacking multiple varargs overloads of unrelated element types on the same method name.

class Overloaded {
    static void f(int a, int b)   { System.out.println("fixed two-arg"); }
    static void f(int... a)       { System.out.println("varargs"); }

    public static void main(String[] args) {
        f(1, 2);       // "fixed two-arg" — exact fixed-arity match wins
        f(1, 2, 3);    // "varargs" — only the varargs overload applies
        f();           // "varargs" — zero-length array
    }
}

Type Safety and Heap Pollution With Generic Varargs

A varargs parameter of a generic or parameterized type (e.g., List<String>...) triggers an "unchecked generics array creation" warning, because arrays of generic types are not fully type-safe at runtime (generics use type erasure, so the array's actual runtime component type is erased). The compiler still lets you write it — with a warning — because varargs of generic type are common enough in the JDK itself (e.g., List.of, Arrays.asList).

@SafeVarargs may only be applied to static methods, final instance methods, private instance methods, or constructors — never to a non-final, overridable instance method, since an overriding subclass could break the safety guarantee.

@SafeVarargs // asserts to callers and the compiler that this method doesn't misuse the array
static <T> List<T> listOf(T... items) {
    return Arrays.asList(items);
}

Practical Usage

Varargs shines for APIs that naturally take a flexible number of same-typed items: String.format(String, Object...), List.of(E...), Math.max alternatives, logging methods, or simple aggregation utilities like sum/average/concat. It reads cleanly at the call site — printAll("a", "b", "c") — without forcing callers to construct an array or a collection first.

class Logger {
    static void warn(String template, Object... args) {
        System.out.printf(template + "%n", args);
    }

    public static void main(String[] args) {
        warn("User %s logged in from %s", "alice", "192.168.1.1");
    }
}

Edge Cases

Because a varargs parameter is really an array, in-place mutation of it inside the method is visible to the caller only if the caller passed an actual array (mutating the compiler-synthesized array from individual arguments has no effect the caller can observe, since that array only exists inside the call).

static void zeroOut(int... nums) {
    for (int i = 0; i < nums.length; i++) nums[i] = 0;
}

public static void main(String[] args) {
    int[] data = {1, 2, 3};
    zeroOut(data);
    System.out.println(Arrays.toString(data)); // [0, 0, 0] — same array object was mutated

    zeroOut(4, 5, 6); // fine, but the temporary array vanishes after the call — no observable effect
}

Best Practices

Reserve varargs for methods where "a flexible list of same-typed values" is genuinely the natural shape of the API. Don't overload a varargs method with too many fixed-arity siblings, as this invites ambiguity. Avoid mixing varargs with reference types where autoboxing could silently create surprising array element types (e.g., Object... swallowing an int[] as a single element rather than spreading it).

int[] cannot be widened to Object[] the way Integer[] can, so passing a primitive array to an Object... varargs parameter wraps the whole array as a single element — a classic surprise.

static void show(Object... items) { System.out.println(items.length); }

public static void main(String[] args) {
    int[] arr = {1, 2, 3};
    show(arr);       // prints 1 — int[] is NOT Object[], so it becomes ONE element
    show(1, 2, 3);   // prints 3 — autoboxed Integer values fill separate slots
}

Complete Example

import java.util.Arrays;

public class VarargsDemo {
    static double average(double... values) {
        if (values.length == 0) {
            throw new IllegalArgumentException("average of zero values is undefined");
        }
        double total = 0;
        for (double v : values) total += v;
        return total / values.length;
    }

    static String concatAll(String separator, Object... parts) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < parts.length; i++) {
            if (i > 0) sb.append(separator);
            sb.append(parts[i]);
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        System.out.println(average(2.0, 4.0, 6.0));       // 4.0
        System.out.println(concatAll("-", "a", 1, true));  // a-1-true
        System.out.println(Arrays.toString(new int[]{1, 2})); // demonstrating Arrays utility separately
    }
}

Key Takeaways

A varargs parameter (T...) must be the last parameter in the method, and it is treated internally as a T[] array.

Calling a varargs method with no trailing arguments yields a zero-length array, not null, unless a caller explicitly casts and passes a null array.

Varargs is only considered in the final phase of overload resolution — a matching fixed-arity overload always wins first.

Generic varargs (T... where T is a type parameter) triggers heap-pollution warnings; @SafeVarargs on eligible methods documents that the array is used safely.

A primitive array (like int[]) passed where Object... is expected becomes a single array element, not a spread of individual elements — a common source of bugs.

static void bad(String... parts, String sep) { } // varargs not last static void good(String sep, String... parts) { } // varargs is last

A varargs parameter must be the final parameter in the list — putting anything after it is a compile-time error, since the compiler could not know where the variable-length part ends.

show(new int[]{1,2,3}); // expecting three Object args when show(Object... items) show(1, 2, 3); // or box explicitly: show((Object) 1, 2, 3) int[] does not widen to Object[]; passing a primitive array to an Object... parameter treats the whole array as a single element instead of spreading it into separate arguments.

sum(null); // ambiguous / risks NullPointerException inside the loop sum(); // zero-length array sum((int[]) null); // explicit null array, if truly intended, guarded with a null check inside

Bare null at a varargs call site is ambiguous or silently becomes a null array reference; either call with no arguments for an empty array, or cast explicitly and handle null inside the method.

What does the compiler actually turn a varargs parameter T... into inside the method body?

It becomes an ordinary array T[] — you can use .length, indexing, and for-each on it exactly as with any array parameter.

Where in a method's parameter list must a varargs parameter appear, and why?

It must be the last parameter, because it needs to consume every remaining argument at the call site; allowing it anywhere else would make it impossible for the compiler to know where the variable-length portion ends.

What does calling a varargs method with zero trailing arguments produce inside the method — null or an empty array?

A zero-length array (length 0), never null, unless the caller explicitly passes a null array reference with a cast.

Given overloads f(int, int) and f(int...), which is chosen for the call f(1, 2), and why?

f(int, int) is chosen, because fixed-arity overloads are matched in earlier resolution phases (exact/widening, then boxing) and varargs is only considered last, in Phase 3, after those have been exhausted.

Why does passing an int[] to a method with an Object... parameter not spread its elements as separate arguments?

Because int[] is not a subtype of Object[] (primitive arrays don't widen to Object[] the way reference-type arrays do), so the whole int[] is treated as one Object argument, making items.length equal to 1 rather than the array's element count.

What warning does a varargs parameter of a generic type (e.g., T... where T is a type parameter) typically produce, and what annotation addresses it on eligible methods?

It produces an unchecked/heap-pollution warning because generic array creation isn't fully type-safe under erasure; @SafeVarargs on a static, final, private, or constructor method suppresses the warning and documents that the method uses the array safely.

Can you call a varargs method by passing an already-constructed array of the matching type instead of individual arguments? Give an example.

Yes — sum(new int[]{1,2,3}) is valid for static int sum(int... numbers), because the varargs parameter's actual type is int[], so a matching array argument is accepted directly without repackaging.

Write a varargs method printAll that prints each String argument on its own line, and demonstrate it with zero, one, and several arguments.

public class PrintAllDemo { static void printAll(String... items) { for (String s: items) { System.out.println(s); } } public static void main(String[] args) { printAll(); // prints nothing printAll("solo"); // prints "solo" printAll("a", "b", "c"); // prints a, b, c on separate lines } } printAll() iterates over a zero-length array and prints nothing; printAll("solo") prints one line; printAll("a","b","c") prints three lines, one per element — all handled by the same method thanks to varargs.

What does this program print, and why does the first call not go to the varargs overload?

It prints "fixed" then "varargs". f(1, 2) has an exact fixed-arity match in f(long, long) (int literals widen to long), which is found in resolution Phase 1 and always preferred over varargs. f(1, 2, 3) has no fixed-arity overload with three parameters, so it falls through to the varargs overload in Phase 3.

What does this print, and why do the two calls behave differently?

It prints 1 then 3. int[] is not a subtype of Object[], so primitives is treated as a single Object argument, making items.length equal 1. Integer[] IS a subtype of Object[] (reference array widening), so boxed spreads naturally as an Object[] with length 3.

class T {
    static void f(long a, long b) { System.out.println("fixed"); }
    static void f(long... a)      { System.out.println("varargs"); }

    public static void main(String[] args) {
        f(1, 2);
        f(1, 2, 3);
    }
}
class T {
    static void show(Object... items) {
        System.out.println(items.length);
    }
    public static void main(String[] args) {
        int[] primitives = {10, 20, 30};
        Integer[] boxed = {10, 20, 30};
        show(primitives);
        show(boxed);
    }
}