Varargs methods (Item 53) and generics were both added to the platform in Java 5
The purpose of varargs is to allow clients to pass a variable number of arguments to a method, but it is a leaky abstraction:when you invoke a varargs method, an array is created to hold the varargs parameters; that array, which should be an implementation detail, is visible. As a consequence, you get confusing compiler warnings when varargs parameters have generic or parameterized types.
Recall from Item 28 that a non-reifiable type is one whose runtime representation has less information than its compile-time representation, and that nearly all generic and parameterized types are non-reifiable.
If a method declares its varargs parameter to be of a non-reifiable type, the compiler generates a warning on the declaration.
If the method is invoked on varargs parameters whose inferred type is non-reifiable, the compiler generates a warning on the invocation too. The warnings look something like this:
warning: [unchecked] Possible heap pollution from
parameterized vararg type List<String>
The purpose of varargs is to allow clients to pass a variable number of arguments to a method, but it is a leaky abstraction:when you invoke a varargs method, an array is created to hold the varargs parameters; that array, which should be an implementation detail, is visible. As a consequence, you get confusing compiler warnings when varargs parameters have generic or parameterized types.
Recall from Item 28 that a non-reifiable type is one whose runtime representation has less information than its compile-time representation, and that nearly all generic and parameterized types are non-reifiable.
If a method declares its varargs parameter to be of a non-reifiable type, the compiler generates a warning on the declaration.
If the method is invoked on varargs parameters whose inferred type is non-reifiable, the compiler generates a warning on the invocation too. The warnings look something like this:
warning: [unchecked] Possible heap pollution from
parameterized vararg type List<String>
Heap pollution occurs when a variable of a parameterized type refers to an object that is not of that type [JLS, 4.12.2]. It can cause the compiler’s automatically generated casts to fail, violating the fundamental guarantee of the generic type system.
// Mixing generics and varargs can violate type safety!
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; // Heap pollution
String s = stringLists[0].get(0); // ClassCastException
}
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; // Heap pollution
String s = stringLists[0].get(0); // ClassCastException
}
This method has no visible casts yet throws a
ClassCastException
when invoked with one or more arguments. Its last line has an invisible cast that is generated by the compiler. This cast fails, demonstrating that type safety has been compromised, and it is unsafe to store a value in a generic varargs array parameter.
In fact, the Java libraries export several such methods, including
Arrays.asList(T... a)
, Collections.addAll(Collection<? super T> c, T... elements)
, and EnumSet.of(E first, E... rest)
. Unlike the dangerous
method shown earlier, these library methods are typesafe.
Prior to Java 7, there was nothing the author of a method with a generic varargs parameter could do about the warnings at the call sites. This made these APIs unpleasant to use. Users had to put up with the warnings or, preferably, to eliminate them with
@SuppressWarnings("unchecked")
annotations at every call site (Item 27). This was tedious, harmed readability, and hid warnings that flagged real issues.
In Java 7, the
SafeVarargs
annotation was added to the platform, to allow the author of a method with a generic varargs parameter to suppress client warnings automatically. In essence, the SafeVarargs
annotation constitutes a promise by the author of a method that it is typesafe. In exchange for this promise, the compiler agrees not to warn the users of the method that calls may be unsafe.
// UNSAFE - Exposes a reference to its generic parameter array!
static <T> T[] toArray(T... args) {
return args;
}
static <T> T[] toArray(T... args) {
return args;
}
The method may not look dangerous, but it is! The type of this array is determined by the compile-time types of the arguments passed in to the method, and the compiler may not have enough information to make an accurate determination. Because this method returns its varargs parameter array, it can propagate heap pollution up the call stack.
tatic <T> T[] pickTwo(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError(); // Can't get here
}
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError(); // Can't get here
}
public static void main(String[] args) {
String[] attributes = pickTwo("Good", "Fast", "Cheap");
}
String[] attributes = pickTwo("Good", "Fast", "Cheap");
}
// Safe method with a generic varargs parameter
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
The rule for deciding when to use the
SafeVarargs
annotation is simple: Use@SafeVarargs
on every method with a varargs parameter of a generic or parameterized type, so its users won’t be burdened by needless and confusing compiler warnings. This implies that you should never write unsafe varargs methods like dangerous
or toArray
Every time
he compiler warns you of possible heap pollution from a generic varargs parameter in a method you control, check that the method is safe. As a reminder, a generic varargs methods is safe if:
1. it doesn’t store anything in the varargs parameter array, and
2. it doesn’t make the array (or a clone) visible to untrusted code. If either of these prohibitions is violated, fix it.
Note that the
SafeVarargs
annotation is legal only on methods that can’t be overridden, because it is impossible to guarantee that every possible overriding method will be safe. In Java 8, the annotation was legal only on static methods and final instance methods; in Java 9, it became legal on private instance methods as well.
// List as a typesafe alternative to a generic varargs parameter
static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
This method can then be used in conjunction with the static factory method
List.of
to allow for a variable number of arguments. Note that this approach relies on the fact that the List.of
declaration is annotated with @SafeVarargs
:
audience = flatten(List.of(friends, romans, countrymen));
The advantage of this approach is that the compiler can prove that the method is typesafe. You don’t have to vouch for its safety with a
SafeVarargs
annotation, and you don’t have worry that you might have erred in determining that it was safe. The main disadvantage is that the client code is a bit more verbose and may be a bit slower.
static <T> List<T> pickTwo(T a, T b, T c) {
switch(rnd.nextInt(3)) {
case 0: return List.of(a, b);
case 1: return List.of(a, c);
case 2: return List.of(b, c);
}
throw new AssertionError();
}
switch(rnd.nextInt(3)) {
case 0: return List.of(a, b);
case 1: return List.of(a, c);
case 2: return List.of(b, c);
}
throw new AssertionError();
}
public static void main(String[] args) {
List<String> attributes = pickTwo("Good", "Fast", "Cheap");
}
List<String> attributes = pickTwo("Good", "Fast", "Cheap");
}
The resulting code is typesafe because it uses only generics, and not arrays.
In summary, varargs and generics do not interact well because the varargs facility is a leaky abstraction built atop arrays, and arrays have different type rules from generics. Though generic varargs parameters are not typesafe, they are legal. If you choose to write a method with a generic (or parameterized) varargs parameter, first ensure that the method is typesafe, and then annotate it with
@SafeVarargs
so it is not unpleasant to use.
No comments:
Post a Comment