堆污染又是啥

936 阅读3分钟

前言:

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

问题展示:

我们先来看一段代码,然后看下编译器给了什么提示

public static <T> List<T> asList(T... a) {
       return null;
}
warnings: Potential heap pollution via varargs parameter args

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

我们再来看两个例子,帮助理解一下当泛型和可变参数一起的时候可能会带来的问题。

示例一:

第一个例子来自于@SafeVarargs注解中的示例:

static void m(List<String>... stringLists) {
       Object[] array = stringLists;
       List<Integer> 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<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        m(list);
    }

运行程序的话,会报出

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

因为在String s = stringLists[0].get(0)这里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> 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> T[] toArray(T... args) {
      return args;
  }
想象中的执行过程:

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 <T> List<T> flatten(List<? extends T>... lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists) {
            result.addAll(list);
        }
        return result;
    }

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