The java.lang.SafeVarargs annotation

Marcio Endo
August 22, 2022

In a previous blog post I mentioned the @SafeVarargs annotation. That post was about wildcard type arguments. For this reason, the SafeVarargs annotation was beyond the scope of that post.

In this blog post, I want to take a deeper look at the annotation. To be precise, we will look into all of the topics related to the annotation. In particular:

So let's begin.

An unsafe varargs example

If the name of the annotation is "safe varargs" it must mean that, in some situations, varargs can be unsafe.

So let's see an example.

Before we do, please keep in mind the purpose of the following code is to illustrate an use of unsafe varargs. In other words, it is written in that way specifically to produce bad results. It shouldn't be considered an example for production code.

A server that configures and starts two services

Suppose we have the following Server:

public class Server {
  @SuppressWarnings("unchecked")
  public static void main(String[] args) {
    var serviceA = new HashMap<String, Object>();
    serviceA.put("name", "Service A");

    var serviceB = new HashMap<String, Object>();
    serviceB.put("name", "Service B");

    Library.defaults(serviceA, serviceB);

    serviceB.put("max", 512);

    start(serviceA);
    start(serviceB);
  }

  private static void start(Map<String, Object> config) {
    System.out.println("Starting...");

    String name = (String) config.get("name");
    System.out.format("name=%s%n", name);

    int max = (int) config.get("max");
    System.out.format("max=%d%n", max);

    System.out.println();
  }
}

It creates the configuration for two services named:

The service configuration is given by a Map<String, Object>. The mapped key is the configuration's name and the mapped value is the configuration's actual value.

The Server calls a Library function which populates each specified configuration with the default values. We will see the Library code in a moment.

Then the Server updates the B service max configuration with the value 512. What this configuration means and what it does is not important to our example.

Finally, the Server "starts" the two services. The starting process is represented by printing the configuration values to the standard output.

Let's now look at the Library code:

final class Library {
  private Library() {}

  public static void defaults(ConfigBuilder... configs) {
    Object[] values = configs;
    addDefaultValues(values);
  }

  @SuppressWarnings("unchecked")
  public static void defaults(Map<String, Object>... configs) {
    Object[] values = configs;
    addDefaultValues(values);
  }

  @SuppressWarnings({"rawtypes", "unchecked"})
  private static void addDefaultValues(Object[] values) {
    for (Object input : values) {
      if (input instanceof Map map) {
        map.put("max", 256);
      } else if (input instanceof ConfigBuilder b) {
        b.integer("max", 256);
      } else {
        throw new IllegalArgumentException("Invalid type " + input.getClass());
      }
    }
  }
}

We can see that the Library supports the configuration in two forms:

It delegates to a private method where it sets a single default configuration value:

Notice that both public methods of our library declare varargs parameters. Additionally, and before invoking the private method, both public methods do the following:

Object[] values = configs;

Since in Java arrays are covariant the statement is valid.

Finally, notice that SuppressWarnings were added to two of the Library methods.

When we execute our Server it runs successfully producing the following output:

Starting...
name=Service A
max=256

Starting...
name=Service B
max=512

Great! But let's change our server configuration to make it a little more "type-safe".

Introducing a ConfigValue interface

Suppose now we decided that a Map<String, Object> is a poor representation for our service's configuration. We will model our configuration using a Map<String, ConfigValue> instead.

Remember, the purpose of this example is to illustrate an unsafe varargs usage.

The ConfigValue interface is given by the following:

sealed interface ConfigValue {
  record IntValue(int value) implements ConfigValue {
    @Override
    public void apply(String key) {
      System.out.format("%s=%d%n", key, value);
    }
  }

  record StringValue(String value) implements ConfigValue {
    @Override
    public void apply(String key) {
      System.out.format("%s=%s%n", key, value);
    }
  }

  void apply(String key);
}

It supports integer and string values which is enough for our example. As we have introduced this class, we need to change our Server class:

public class Server {
  @SuppressWarnings("unchecked")
  public static void main(String[] args) {
    var serviceA = new HashMap<String, ConfigValue>();
    serviceA.put("name", new StringValue("Service A"));

    var serviceB = new HashMap<String, ConfigValue>();
    serviceB.put("name", new StringValue("Service B"));

    Library.defaults(serviceA, serviceB);

    serviceB.put("max", new IntValue(512));

    start(serviceA);
    start(serviceB);
  }

  private static void start(Map<String, ConfigValue> config) {
    System.out.println("Starting...");

    for (var entry : config.entrySet()) {
      var key = entry.getKey();
      var value = entry.getValue();
      value.apply(key);
    }
    
    System.out.println();
  }
}

Notice that there are no more casts in our start method. So, in theory, we have succeeded in our improvement. Or did we?

We have to update our Library class as well. Let's update the defaults method signature to:

@SuppressWarnings("unchecked")
public static void defaults(Map<String, ConfigValue>... configs)

As all of our code compiles, we assume no more changes are required.

Let's try to execute our server:

Starting...
Exception in thread "main" java.lang.ClassCastException: 
  class java.lang.Integer cannot be cast to class iter2.ConfigValue 
  (java.lang.Integer is in module java.base of loader 'bootstrap';
  iter2.ConfigValue is in unnamed module of loader 'app')
	at iter2.Server.start(Server.java:36)
	at iter2.Server.main(Server.java:26)

I formatted the output slightly so it is easier to read. The important part is:

java.lang.ClassCastException: 
  class java.lang.Integer cannot be cast to class iter2.ConfigValue

What happened? And how can we fix it?

We will see that in a moment. For now, it seems that the "safe" term in the SafeVarargs name refers to type-safety.

Heap pollution

The ClassCastException we have experienced is the "symptom" of a condition known as heap pollution. The term heap pollution is used in section 4.12.2 of the Java language specification:

It is possible that a variable of a parameterized type will refer to an object that is not of that parameterized type. This situation is known as heap pollution.

This is what happened in our last example.

Our configuration map is a parameterized type. In the start method, we expected the config map to contain only ConfigValue values. In reality it had an Integer value mapped to the max key as indicated by the ClassCastException message.

Continuing with the section 4.12.2 of the Java language specification, we find (emphasis mine):

Heap pollution can only occur if the program performed some operation involving a raw type that would give rise to a compile-time unchecked warning (...), or if the program aliases an array variable of non-reifiable element type through an array variable of a supertype which is either raw or non-generic.

While only one of the conditions is required, the Library class does both of them.

First, in the addDefaultValues method, it "performed some operation involving a raw type":

for (Object input : values) {
  if (input instanceof Map map) {
    map.put("max", 256);
  } else ...
  ...
}

As it uses an instance of the Map raw type.

Second, in the defaults(Map) method, it "aliases an array variable of non-reifiable element type through an array variable of a supertype which is either raw or non-generic":

public static void defaults(Map<String, ConfigValue>... configs) {
  Object[] values = configs;
  addDefaultValues(values);
}

Where:

How to make our unsafe example safe?

To make our unsafe example safe we have to fix our Library class.

From the previous section, we have to fix:

One possible solution is:

final class Library {
  private static final int MAX_DEFAULT_VALUE = 256;

  private Library() {}

  public static void defaults(ConfigBuilder... configs) {
    for (var config : configs) {
      config.integer("max", MAX_DEFAULT_VALUE);
    }
  }

  public static void defaults(Map<String, ConfigValue>... configs) {
    for (var config : configs) {
      config.put("max", new IntValue(MAX_DEFAULT_VALUE));
    }
  }
}

Running our Server now produces the following output:

Starting...
max=256
name=Service A

Starting...
max=512
name=Service B

Great! It works as expected.

But what about the SafeVarargs annotation? Is our example really safe?

The compiler warnings

Our last example runs. But the compilation raises two warnings:

$ javac -d /tmp -Xlint:all src/main/java/iter3/* src/main/java/shared/*
src/main/java/iter3/Library.java:33: 
	warning: [unchecked]
	Possible heap pollution 
	from parameterized vararg type Map<String,ConfigValue>
  public static void defaults(Map<String, ConfigValue>... configs) {
                                                          ^
src/main/java/iter3/Server.java:31:
	warning: [unchecked]
	unchecked generic array creation 
	for varargs parameter of type Map<String,ConfigValue>[]
    Library.defaults(serviceA, serviceB);
                    ^
2 warnings

I formatted the output slightly so it is easier to read.

The first warning comes from the Library class defaults(Map) method.

The second warning comes from the Server class. And, worth noting, it is at the call-site of the method from the first warning.

Two methods with varargs. But warning in only one.

Our Library class defines two methods with varargs parameters. Their signatures are:

public static void defaults(ConfigBuilder... configs)

public static void defaults(Map<String, ConfigValue>... configs)

But compilation only raised a warning for the second one. Why didn't the first method trigger the same warning?

The SafeVarargs annotation is actually defined by the Java language specification. Section 9.6.4.7 states:

A variable arity parameter with a non-reifiable element type (§4.7) can cause heap pollution (§4.12.2) and give rise to compile-time unchecked warnings (§5.1.9).

In other words, a varargs parameter can only be unsafe if it is of a non-reifiable type.

So the question becomes: what are reifiable and non-reifiable types?

Reifiable types

Reifiable types are defined in section 4.7 of the Java language specification:

Because some type information is erased during compilation, not all types are available at run time. Types that are completely available at run time are known as reifiable types.

A type is reifiable if and only if one of the following holds:

  • It refers to a non-generic class or interface type declaration.

  • It is a parameterized type in which all type arguments are unbounded wildcards (§4.5.1).

  • It is a raw type (§4.8).

  • It is a primitive type (§4.2).

  • It is an array type (§10.1) whose element type is reifiable.

  • It is a nested type where, for each type T separated by a ".", T itself is reifiable.

We can translate the definition to the following Java code. Each of the parameters of the following methods is of a type that is reifiable:

// non-generic class or interface
foo(String s, IOException ex, Runnable r);

// parameterized type, unbounded wildcards
foo(List<?> l, Map<?,?> m);

// raw type
foo(List l, Map m);

// primitive types
foo(int i, boolean b);

// array, element type is reifiable
foo(int[] values, Path... files);

// nested type, each T is reifiable
foo(System.Logger logger, StackWalker.StackFrame frame);

Note that the return type of each method was not listed.

Varargs and reifiable types

Getting back to our Library class, we have the defaults(ConfigBuilder) method:

public static void defaults(ConfigBuilder... configs)

The varargs configs parameter is of a type that is reifiable. As ConfigBuilder is reifiable, an array of ConfigBuilder is also reifiable.

That is the reason the compiler did not issue a warning for this method.

Therefore, and regarding heap pollution, the combination:

is always safe.

Non-reifiable types

The previous section listed all of the possible reifiable types. So the complement of that must contain all of the non-reifiable types.

Joshua Bloch, in the Effective Java (second edition) book, says the following about non-reifiable types:

Intuitively speaking, a non-reifiable type is one whose runtime representation contains less information than its compile-time representation.

Let's see some examples. Each parameter of the following methods is of a type that is non-reifiable:

// type variables
foo(E e, T t);

// parameterized types
foo(List<String> l, Map<String, Object> m, Set<E> s);

// parameterized types, bounded wildcards
foo(List<? extends String> l, Function<? super T, ? extends R> f);

// array, element is non-reifiable
foo(E[] values, List<String>... lists);

Once again, the return type of each method was not listed. Additionally, the type parameters are implied to be defined either at the enclosing class or at the method itself.

Varargs and non-reifiable types

Going back to our Library class once again, we have the defaults(Map) method:

public static void defaults(Map<String, ConfigValue>... configs)

The varargs configs parameter is of a type that is non-reifiable. A parameterized type, Map<String, ConfigValue> in this case, is non-reifiable. Since it is non-reifiable, an array of those is also non-reifiable.

This is the reason the compiler issued a warning for this method. Remember the SafeVarargs section of the Java language specification (emphasis mine):

A variable arity parameter with a non-reifiable element type (§4.7) can cause heap pollution (§4.12.2)

Therefore, and regarding heap pollution, the combination:

is possibly unsafe. In other words, the combination can be safe under certain conditions.

Suppressing our first compiler warning

So the combination varargs and non-reifiable types can be safe under certain conditions. What are those conditions?

Remember the heap pollution section of the Java language specification (emphasis mine):

Heap pollution can only occur if the program performed some operation involving a raw type (...) or if the program aliases an array variable of non-reifiable element type through an array variable of a supertype which is either raw or non-generic.

In the last modification to the Library class, we removed both of those unsafe operations.

Now, since:

We can conclude that our Library class is safe regarding heap pollution. Threfore, it is now safe to suppress the first compiler warning. Let's annotate the default method in the Library class:

@SuppressWarnings("unchecked")
public static void defaults(Map<String, ConfigValue>... configs) {
  for (var config : configs) {
    config.put("max", new IntValue(MAX_DEFAULT_VALUE));
  }
}

Let's compile our program:

$ javac -d /tmp -Xlint:all src/main/java/iter3/* src/main/java/shared/*
src/main/java/iter3/Server.java:31: 
	warning: [unchecked] 
	unchecked generic array creation 
	for varargs parameter of type Map<String,ConfigValue>[]
    Library.defaults(serviceA, serviceB);
                    ^
1 warning

Nice. We have successfully suppressed the first warning.

Suppressing our second compiler warning

Before we suppress our second warning, let's try to understand it. In order to do it, we will have to look into a series of concepts.

A new array is created at the varargs method call-site

Imagine we have the following method:

private static Set<String> asSet(String... strings) {
  var set = new HashSet<String>();
  for (String s : strings) {
    set.add(s);
  }
  return set;
}

It accepts an array of strings (as varargs) and adds each element to a Set instance. An invocation of the form:

var set = asSet("A", "B", "B", "C", "C");

is actually compiled as:

var set = asSet(new String[] {"A", "B", "B", "C", "C"});

We can use the javap tool to confirm. It produces the following output for both versions:

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: (0x0009) ACC_PUBLIC, ACC_STATIC
  Code:
    stack=4, locals=2, args_size=1
       0: iconst_5
       1: anewarray     #16                 // class java/lang/String
       4: dup
       5: iconst_0
       6: ldc           #18                 // String A
       8: aastore
(...)
      24: dup
      25: iconst_4
      26: ldc           #22                 // String C
      28: aastore
      29: invokestatic  #24                 // Method asSet:([Ljava/lang/String;)Ljava/util/Set;

It creates a new array of String, adds the string values to the array and invokes the asSet method with the created array.

Arrays and non-reifiable types

It is not possible to create arrays of non-reifiable types.

For example, the following array creation expressions do not compile:

Map<String, ConfigValue>[] maps;
// does not compile!
maps = new Map<String, ConfigValue>[2];

class Container<E> {
  // does not compile!
  final E[] values = new E[10];
}

Why is it not possible? We will see that in a moment.

The java.lang.ArrayStoreException

In Java, arrays are reifiable. In other words, arrays know, at runtime, their component type.

The following program illustrates this fact. It creates a String[] and tries to add two elements to it:

public class TheArrayStoreException {
  public static void main(String[] args) {
    Object[] a = new String[2];

    var c = a.getClass();
    if (c.isArray()) {
      var componentType = c.getComponentType();

      System.out.println("Component type is " + componentType);
    }

    System.out.println("Index 0");
    a[0] = "abc";
    System.out.println("Index 1");
    a[1] = 123;
  }
}

Running this program produces the following output:

Component type is class java.lang.String
Index 0
Index 1
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
	at iter4.TheArrayStoreException.main(TheArrayStoreException.java:22)

It fails at the last statement with an uncaught ArrayStoreException. So what is happening?

In Java, arrays are covariant. We have seen this before. The first statement in the program is valid:

Object[] a = new String[2];

Since our a variable is of type Object[] both assignments at the end of the program are also valid:

a[0] = "abc";
a[1] = 123;

However, as mentioned, arrays in Java are reifiable. So, while the two assignments are valid at compile-time, the last one fails at runtime:

the array knows at runtime it is only allowed to accept String values.

So it rejects the last assignment with a java.lang.ArrayStoreException.

Erasure: generics do not have a ArrayStoreException equivalent

From the previous example, we conclude that arrays behave substantially different than generics.

In Java, generics are invariant. In other words, unlike arrays, parameterized types are not covariant. Additionally, parameterized types (excluding the ones with unbounded wildcards) are non-reifiable.

Let's try to create a version of the previous example using a parameterized types:

public class TheArrayStoreExceptionCounter {
  @SuppressWarnings({"rawtypes", "unchecked"})
  public static void main(String[] args) {
    List a = new ArrayList<String>(2);

    var c = a.getClass();
    var typeParameters = c.getTypeParameters();
    for (var typeVar : typeParameters) {
      System.out.println(typeVar);
    }

    System.out.println("Index 0");
    a.add("abc");
    System.out.println("Index 1");
    a.add(123);
  }
}

This program runs successfully producing the following output:

E
Index 0
Index 1

Therefore, in this example, the last statement did not fail.

Granted, this is a rather poor example. It uses raw types and it suppresses unchecked warnings. In other words, it would very likely fail with a ClassCastException at some later point.

However, a proper example would not have compiled at all. Which is another major difference to the previous array example.

The point I am trying to make:

Arrays and non-reifiable types (revisited)

Let's now combine the information of the two previous sections.

Suppose the following compiles (remember, it does not compile):

// does not compile!
Object[] a = new List<String>[2];

Then this assignment should be allowed:

a[0] = new ArrayList<String>();

While we would expect the following to throw an ArrayStoreException:

a[1] = new ArrayList<Integer>();

Except, as we have seen in the previous section, the Integer information is erased at runtime.

If the Integer information is erased, how can our hypothetical List<String>[] know it should not allow a List<Integer>?

Well, it can't.

Therefore, it is not possible to create arrays of non-reifiable types.

Varargs and non-reifiable types

So how does the compiler work around these two opposing facts:

In other words, consider a method having the following signature:

private static Set<String> asSet(List<String>... lists);

If we were to invoke like so:

var set = asSet(
  Arrays.asList("A", "B", "C"),
  Arrays.asList("B", "C", "D"));

As it is not possible to create arrays of non-reifiable types, the compiler cannot generate the following:

// does not compile!
var set = asSet(new List<String>[] {
  Arrays.asList("A", "B", "C"),
  Arrays.asList("B", "C", "D")});

So what is the solution? The compiler, instead, generates the following code:

var set = asSet(new List[] {
    Arrays.asList("A", "B", "C"),
    Arrays.asList("B", "C", "D")});

In other words, the compiler:

  1. creates an array of the raw type List; and

  2. does an unchecked conversion from List[] to List<String>[].

Let's see this two steps in action. The following program does just that:

public class ArraysAndNonReifiableTypes {
  public static void main(String[] args) {
    List<String>[] lists = new List[2];
    System.out.println(lists.length);
  }
}

When we try to compile it the compiler gives two warnings:

$ javac -d /tmp -Xlint:all src/main/java/iter4/ArraysAndNonReifiableTypes.java 
src/main/java/iter4/ArraysAndNonReifiableTypes.java:18: 
    warning: [rawtypes] found raw type: List
    List<String>[] lists = new List[2];
                               ^
  missing type arguments for generic class List<E>
  where E is a type-variable:
    E extends Object declared in interface List
    
src/main/java/iter4/ArraysAndNonReifiableTypes.java:18:
    warning: [unchecked] unchecked conversion
    List<String>[] lists = new List[2];
                           ^
  required: List<String>[]
  found:    List[]
2 warnings

So the warnings are, in order, about:

As we have mentioned.

Finally suppressing the second compiler warning

Let's recap the second compiler warning. It was issued at the Server class at the call-site of the Library.defaults varargs method:

src/main/java/iter3/Server.java:31: 
	warning: [unchecked] 
	unchecked generic array creation 
	for varargs parameter of type Map<String,ConfigValue>[]
    Library.defaults(serviceA, serviceB);
                    ^
1 warning

The compiler is warning us that, in order to invoke that method, it will have to:

Both are potentially unsafe operations; depending on what the invoked method will do to the created array.

However, when we suppressed the first compiler warning, we concluded that the method is safe.

Therefore, we can now safely suppress the second compiler warning.

We cannot annotate an expression, so we must annotate the whole main method, like so:

@SuppressWarnings("unchecked")
public static void main(String[] args) {
  var serviceA = new HashMap<String, ConfigValue>();
  serviceA.put("name", new StringValue("Service A"));

  var serviceB = new HashMap<String, ConfigValue>();
  serviceB.put("name", new StringValue("Service B"));

  Library.defaults(serviceA, serviceB);

  serviceB.put("max", new IntValue(512));

  start(serviceA);
  start(serviceB);
}

This iteration of our Server now compiles without warning:

$ javac -d /tmp -Xlint:all src/main/java/iter3/* src/main/java/shared/*
[no warnings emitted]

Suppressing both warnings with SafeVarargs

There are two problems with our previous suppressions:

  1. we had to suppress warnings for the whole main method.
    This means that we might have inadvertently suppressed an unchecked warning coming from another source.

  2. we have to suppress warnings for each and every invocation of our Library.defaults method.
    In fact, the suppression is required each time any varargs method invocation implies the creation of an array of non-reifiable types.

Therefore, in order to mitigate these problems, the SafeVarargs annotation was introduced in Java 7.

Remember that both generics and varargs were introduced in Java 5.

Let's apply the annotation to our running example. We first annotate our defaults method with the annotation:

@SafeVarargs
public static void defaults(Map<String, ConfigValue>... configs) {
  for (var config : configs) {
    config.put("max", new IntValue(MAX_DEFAULT_VALUE));
  }
}

And we can remove the SuppressWarnings annotation from the main method:

public class Server {
  public static void main(String[] args) {
    ...
  }

  private static void start(Map<String, ConfigValue> config) {
    ...
  }
}

And it compiles without warnings:

$ javac -d /tmp -Xlint:all src/main/java/iter5/* src/main/java/shared/*
[no warnings emitted]

A final note on SafeVarargs

The SafeVarargs annotation can only be added to methods that cannot be overridden.

The annotation javadocs states (emphasis mine):

it is a compile-time error if a method or constructor declaration is annotated with a @SafeVarargs annotation, and either:

  • the declaration is a fixed arity method or constructor

  • the declaration is a variable arity method that is neither static nor final nor private.

In other words:

The reason is that a safe method can be made unsafe by overriding it.

A final note on heap pollution

Consider the following method:

// not safe!!!
private static <T> T[] genericArray(T... values) {
  return values;
}

The genericArray method simply returns the varargs argument. Apart from that, it does not do any operation at all to the array. Can we annotate this method with SafeVarargs?

The answer is no. This is definitely not a safe method.

To illustrate it, consider the following usage of the method:

public class FinalNote {
  public static void main(String[] args) {
    List<String>[] generic = genericArray(
      Arrays.asList("A", "B", "C"),
      Arrays.asList("D", "E", "F"));

    Object[] alias = generic;

    alias[0] = Arrays.asList(1, 2, 3);

    String a = generic[0].get(0);
    System.out.println(a);
  }

  private static <T> T[] genericArray(T... values) {
    return values;
  }
}

It fails with a ClassCastException:

Exception in thread "main" java.lang.ClassCastException: 
	class java.lang.Integer cannot be cast to class java.lang.String 
	(java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
	at iter6.FinalNote.main(FinalNote.java:21)

Remember section 4.12.2 of the Java language specification:

Heap pollution can only occur (...) if the program aliases an array variable of non-reifiable element type through an array variable of a supertype which is either raw or non-generic.

By returning the array as it is we allowed another part of the program to "alias the array variable".

Conclusion

In this blog post we took a deep look at the SafeVarargs annotation.

We started with an example containing an unsafe use of a varargs argument. It caused a situation known as heap pollution.

We fixed our example so it was type-safe. But the compiler would issue two warnings.

In the process of suppressing those two warnings we looked into:

Finally we suppresed the warnings with a SafeVarargs annotation.

The source code for all of the examples can be found at this GitHub repository.

References

Apart from the Java language specification and the javadocs for the SafeVarargs annotation, I also used the following as reading material for this blog post:

Additionally, I believe this is the tracker bug for SafeVarargs: