最近在看《effective java》的时候,看到一个新鲜词,“堆污染”。乍一看好像是个很高端大气上档次,想起来应该跟jvm关系比较大的概念。
后来学习发现是编写代码不规范可能导致的一个异常现象。下面我们一起来学习下,首先这个东西会牵扯到三个东西,第一个是可变参数,第二个是
泛型,第三个是@SafeVarargs注解。前两个咱们知道是java5中加入的新特性。第三个是在java7中加入的新注解。

问题展示:
我们先来看一段代码,然后看下编译器给了什么提示
public static List asList(T… a) {
return null;
}

1
2
3
4
5
6
7
8
9
10
11

可以看到编译器直接提示了这个方法可能导致heap pollution,即使这个方法仅仅就返回一个null。
只要参数列表同时是泛型和可变参数,就会出现这样的警告,提示开发者这里可能出现问题。为了解决这个烦人的提示,所以java7增加了
@SafeVarargs的注解来抑制这个提示。




我们再来看两个例子,帮助理解一下当泛型和可变参数一起的时候可能会带来的问题:
示例一:
第一个例子来自于@SafeVarargs注解中的示例:

static void m(List… stringLists) {
Object[] array = stringLists;
List tmpList = Arrays.asList(42);
array[0] = tmpList; // Semantically invalid, but compiles without warnings
String s = stringLists[0].get(0); // Oh no, ClassCastException at runtime!
}

// 写了一个main方法去调用
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
m(list);
}

1
运行程序的话,会报出

Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

1
因为在

String s = stringLists[0].get(0);

1
2
3
4
5
6
这里stringList[0]已经被array[0] = tmpList修改为了Integer类型的元素,取出来的元素也是Integer类型的,然后因为stringList泛型是String的。
编译器会隐式的加一个强转String,就会报出ClassCastException。

示例二:
这个例子来自于《effective java》 第三版。

public static void main(String[] args) {
String[] attr = pickTwo(“A”, “B”, “C”);
}

static T[] pickTwo(T a, T b, T c) {
switch (ThreadLocalRandom.current().nextInt(3)) {
case 0:
return toArray(a, b);
case 1:
return toArray(b, c);
case 2:
return toArray(a, c);
}
throw new AssertionError();
}

static T[] toArray(T… args) {
return args;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
想象中的执行过程:
pickTwo的参数是String,所以传给toArray的参数也是String,name最终返回的就是一个String[]的数组。
实际过程:
这里注意toArray就是使用了可变参数和泛型,在编译这个方法的时候,编译器会生成代码,创建一个可变参数数组。
为了接收所给的任意泛型的参数,这个toArray的args的数组会是Object[],因此toArray返回的T[],实际上就是
Object[]。所以返回给pickTwo方法的T[]也会是Object[]。最后到了main函数的调用部分会变成Object[],就会抛出Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
所以书中也给出了一个提示:允许一个方法访问一个泛型可变参数数组是不安全的。

正确用法:
首先对于每一个带有泛型可变参数或者参数化类型的方法,使用@SafeVarargs注解。
泛型可变参数方法在以下条件是安全的:
1.它没有在可变参数数组中保存任何值
2.它没有对不信任的程序开放数组(简单来说就是调用泛型可变参数的方法如果使用它的数组,结果是可控的不报异常的)
如果这两个条件有一个是不满足的就不是安全的。
经典使用范例:

@SafeVarargs
static List flatten(List<? extends T>… lists) {
List result = new ArrayList<>();
for (List<? extends T> list : lists) {
result.addAll(list);
}
return result;
}

首先这是一个泛型可变参数列表的方法,然后和上面toArray的区别是,参数列表变成了泛型的List,返回结果也是变成了List。
然后方法中使用for循环将,所有参数列表的可变参数数组组合成了一个。这样使用就是安全,每一个可变参数的类型的上界都是T,返回的参数最终也都是T。