Varargs, ambiguity and heap pollution

Core Java · lesson 41 of 42 · 6 min read

A convenience that hides an array, and generics make it unsafe.

Open this lesson in the learning hub

Key points

  • Varargs is an array at the call site. The compiler allocates one for every call, which is why a hot loop over a varargs method allocates more than it looks like it does.
  • Passing an existing array works, but passing a null literal is ambiguous: it can be the whole array or one element that happens to be null, and the compiler warns rather than choosing well.
  • Overload resolution prefers a non-varargs method. Adding an overload can therefore silently change which method an existing call binds to, without any error.
  • Generic varargs create an array of a generic type, which the language cannot express soundly. That is heap pollution - the compiler warns and the failure appears later, elsewhere, as a ClassCastException.
  • @SafeVarargs asserts that the method only reads the array and never stores anything into it or lets it escape. It suppresses the warning; it does not verify the claim.
  • The classic trap: Arrays.asList(intArray) produces a single-element list containing the array, because int[] is one object rather than an array of elements.

Example

import java.util.Arrays;
import java.util.List;

public class VarargsTraps {

    static String describe(Object... args) {
        return args == null ? "null array" : "array of " + args.length;
    }

    // Overload resolution prefers the SPECIFIC method over varargs.
    static String pick(int a, int b)   { return "two ints"; }
    static String pick(int... values)  { return "varargs of " + values.length; }

    // Generic varargs: the array type cannot be expressed soundly.
    @SafeVarargs
    static <T> List<T> listOf(T... items) {
        return Arrays.asList(items);       // safe: we only READ the array
    }

    // NOT safe - it lets the array escape, where it can be corrupted.
    static <T> T[] leak(T... items) { return items; }

    public static void main(String[] args) {
        System.out.println(describe("a", "b"));
        System.out.println(describe());
        System.out.println(describe((Object[]) null));   // the whole array is null
        System.out.println(describe((Object) null));     // ONE null element

        System.out.println();
        System.out.println("pick(1, 2)    -> " + pick(1, 2));
        System.out.println("pick(1, 2, 3) -> " + pick(1, 2, 3));

        // The Arrays.asList trap: int[] is ONE object, not many elements.
        int[] primitives = {1, 2, 3};
        List<int[]> wrapped = Arrays.asList(primitives);
        System.out.println();
        System.out.println("asList(int[]).size()     = " + wrapped.size());

        Integer[] boxed = {1, 2, 3};
        System.out.println("asList(Integer[]).size() = " + Arrays.asList(boxed).size());

        // HEAP POLLUTION - the failure lands far from the cause.
        String[] leaked = leak("a", "b");
        Object[] asObjects = leaked;                 // legal: arrays are covariant
        try {
            asObjects[0] = 42;                       // compiles, fails at runtime
        } catch (ArrayStoreException e) {
            System.out.println();
            System.out.println("ArrayStoreException: " + e.getMessage()
                    + "  <- array covariance caught it here");
        }
    }
}

Varargs is an array, generic varargs are unsound, and @SafeVarargs asserts safety rather than checking it.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Core Java course, and every lesson in it is listed on the Core Java contents page.