龙空技术网

effective java 3 - 第5章 泛型[32]谨慎并用泛型和可变参数

CC躺平混吃 46

前言:

而今各位老铁们对“java创建泛型数组”大致比较注重,看官们都想要学习一些“java创建泛型数组”的相关知识。那么小编同时在网络上收集了一些对于“java创建泛型数组””的相关文章,希望朋友们能喜欢,各位老铁们一起来学习一下吧!

原文:

可变参数(varargs)方法(详见第53条)和泛型都是在Java5中就有了,因此你可能会期待它们可以良好地相互作用;遗憾的是,它们不能。可变参数的作用在于让客户端能够将可变数量的参数传给方法,但这是个技术露底(leaky abstration):当调用一个可变参数方法时,会创建一个数组用来存放可变参数;这个数组应该是一个实现细节,它是可见的。因此,当可变参数有泛型或者参数化类型时,编译警告信息就会产生混乱。

回顾一下第28条(列表优于数组),非具体化(non-reifiable)类型是指其运行时代码信息比编译时少,并且显然所有的泛型和参数类型都是非具体化的。如果一个方法声明其可变参数为non-reifiable 类型,编译器就会在声明中产生一条警告。如果方法是在类型为non-reifiable的可变参数上调用,编译器也会在调用时发出一条警告信息。这个警告信息类似于:

warning:[unchecked] Possible heap pollution from parameterized vararg type List<String>

当一个参数化类型的变量指向一个不是该类型的对象时,会产生堆污染(heap pollution)[JLS, 4.12.2]。它导致编辑器的自动生成转换失败,破坏了泛型系统的基本保证。

举个例子。下面的代码是对第28条种的代码片段稍加修改而得:

// 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}

这个方法没有可见的转换,但是在调用一个或者多个参数时会抛出ClassCastException异常。上述最后一行代码中有一个不可见的转换,这是由编译器生成的。这个转换失败证明类型安全已经受到了危及,因此将值保存在泛型可变参数数组参数中是不安全的。

这个例子引出了一个有趣的问题:为什么显式创建泛型数组是非法的,用泛型可变参数声明方法却是合法的呢?换言之,为什么之前展示的方法只产生一条警告,而28条中的代码片段却产生一个错误呢?答案在于,带有泛型可变参数或者参数化类型的方法在实践中用处很大,因此Java语言的设计者选择容忍这一矛盾的存在。事实上,Java类库导出了好几个这样的方法,包括 Arrays.asList(T... a)、 Collections.addAll(Collection<? super T> C, T... elements) , 以及 EnumSet.of(E first,E ... rest)。 与前面提到的危险方法不一样,这些类库方法是类型安全的。

在Java7中,增加了 SafeVarargs注解,它让带泛型vararg 参数的方法的设计者能够自动禁止客户端的警告。本质上,SafeVarargs注解是通过方法的设计者做出承诺,声明这是类型安全的。作为对该承诺的交换,编译器同意不再向该方法的用户发出警告说这些调用可能不安全。

重要的是,不要随意用 @SafeVarargs 对方法进行注解,除非它真正是安全的。那么它凭什么确保安全呢?回顾一下,泛型数组是在调用方法的时候创建的,用来保存可变参数。如果该方法没有在数组中保存任何值,也不允许对数组的引用转义(这可能导致不被信任的代码访问数组),那么它就是安全的。换言之,如果可变参数数组只用来将数量可变的参数从调用程序传到方法(毕竟这才是可变参数的目的),那么该方法就是安全的。

值得注意的是,从来不在可变参数的数组中保存任何值,这可能破坏类型安全性。以下面的泛型可变参数方法为例,它返回了一个包含其参数的数组。乍看执行,这似乎是一个方便的小工具:

// UNSAFE - Expose a reference to its generic paramter array !

static<T> T[] toArray(T ... args){return args;}

这个方法只是返回其可变参数数组,看起来没什么危险,但它实际上很危险!这个数组的类型,是由传到方法的参数的编译时类型来决定,编译器没有足够的信息去做准确的决定。因此该方法返回其可变参数数组,它会将堆污染传到调用堆栈上。

下面举个具体的例子。这是一个泛型方法,它带有三个类型为T 的参数,并返回一个包含两个(随机选择的)参数的数组:

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(a,c);      case 2 : return toArray(b,c);    }  throw new AssertionError(); // Can't get here}

这个方法本身并没有危险,也不会产生警告,除非它调用了带有泛型可变参数的toArray 方法。

在编译这个方法时,编译器会产生代码,创建一个可变参数数组,并将两个T实例传到toArray。这些代码配置了一个类型为Object[] 的数组,这是确保能够保存这些实例的最具体的类型,无论在调用时给pickTwo传递什么对象都没问题。toArray方法只是将这个数组返回给pickTwo,反过来也将它返回给其调用程序,因此pickTwo始终都会返回一个Object[] 的数组。

现在以main 方法为例,练习一下pickTwo的用法:

public static void main(String[] args){	String[] atts = pickTwo("good","fast","cheap");}

这个方法压根没有任何问题,因此编译时不会产生任何警告。但在运行的时候,它会抛出一个ClassCastException,虽然它看起来并没有包括任何可见的转换。你看不到的是,编译器在pickTwo 返回的值上产生了一个隐藏的 String[] 转换。但转换失败了,这是因为从实际导致堆污染(toArray)的方法处移除了两个级别,可变参数数组在实际的参数存入之后没有进行修改。

这个范例是为了告诉大家,允许另一个方法访问一个泛型可变参数数组是不安全的,有两种情况例外:将数组传给另一个@SafeVarargs 正确注解过的可变参数是安全的,将数组传给只计算数组内容部分函数的非可变参数方法也是安全的。

这里有一个安全使用泛型可变参数的典型范例。这个方法中带有一个任意数量参数的列表,并按顺序返回包含输入清单中所有元素的唯一列表。由于该方法用@SafeVarargs 注解过,因此在声明处或者调用处都不会产生任何警告:

// Safe method with a generic varargs parameter@SafeVarargsstatic <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 注解的规则很简单:对于每一个带有泛型可变参数或者参数化类型的方法,都要用 @SafeVarargs 进行注解,这样它的用户就不用承受那些无谓的、令人困惑的编译警报了。这意味着应该永远都不要编写像dangerous 或者toArray这类不安全的可变参数方法。每当编译器警告你控制的某个带泛型可变参数的方法可能形成堆污染,就应该检查该方法是否安全。这里先提个醒,泛型可变参数方法在下列条件下是安全的。

它没有在可变参数数组中保存任何值它没有对不被信任的代码开放该数组(或者克隆程序)。

以上两个条件只要有任何一条被破坏,就要立即修正它。

注意,SafeVarargs 注解只能用在无法被覆盖的方法上,因为它不能确保每个可能的覆盖方法都是安全的。在Java8中,该注解只在静态方法和final 实例方法中才是合法的;在Java9中,它在私有的实例方法上也合法了。

如果不想使用SafeVarargs注解,也可以采用第28条的建议,用一个List 参数代替可变参数(这是一个伪装数组)。下面举例说明这个办法再 flatten方法上的运用。注意,此处只对参数声明做了修改:

// Safe method with a generic varargs parameterstatic <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;}

随后,这个方法就可以结合静态工厂方法 List.of 一起使用了,允许使用数量可变的参数。注意,使用该方法的前提是用 @SafeVarargs对 List.of 声明进行了注解:

audience = flatten(List.of(friends, romans, countrymen));

这种做法的优势在于编译器可以证明该方法是类型安全的。你不必再通过SafeVarargs注解来证明它的安全性,也不必担心自己是否错误地认定它是安全的。其缺点在于客户端代码有点繁琐,运行起来速度会慢一些。

这一技巧也适用于无法编写出安全的可变参数方法的情况,比如本条之前提到的toArray方法。其 List 对应是 List.of 方法,因此我们不必编写;Java类库的设计者已经替我们完成了。因此 pickTwo方法就变成了下面这样:

static <T> T[] pickTwo(T a, T b,T c){	switch(ThreadLocalRandom.current().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(); // Can't get here}

main方法变成了下面这样:

public static void main(String[] args){	List<String> atts = pickTwo("good","fast","cheap");}

这样得到的代码就是类型安全的,因为它只使用泛型,没有用到数组。

总而言之,可变参数和泛型不能良好地合作,这是因为可变参数设施是构建在顶级数组之上的一个技术露底,泛型数组有不同的类型规则。虽然泛型可变参数不是类型安全的,但它们是合法的。如果选择编写带有泛型(或者参数化)可变参数的方法,首先要确保该方法是类型安全的,然后用@SafeVarargs 对它进行注解,这样使用起来就不会出现不愉快的情况了。

up 补充说明

本条目出现的原因在于Java本身兼容性带来的问题 —— Java的泛型是后来出现的,为了保证兼容,并不是真正的泛型,而是编译器擦除泛型标记并强转来实现的。 以 List<E> 为例, 使用List<Integer> 时,底层并不是真正的 Integer 数组,而是使用 Object[] ,在put 和 get 时候由 编译器检查是不是类型为 Integer(Java文件中使用的泛型E),在class 文件中,实际上是用Object 强转的,只是在 java 文件中不用再显式处理,相比手工强转更方便也更安全一些。

也就是说,对于 List<Integer> ,实际上可以通过某种方式来保存和取出一个String 对象,只要我们将其泛型标记抹掉,就能做到这一点。

如下,下面例子是可以正确编译并执行的,不会报错:

public static void main(String[] args) {        List<Integer> l = new ArrayList<>();        addObject(l, 1, 3, "测试");        System.out.println("检查l中的元素");        System.out.println(l);        System.out.println("-------------------");        printList(l);    }    static void printList(List list){        list.forEach(System.out::println);    }    static void addObject(List list, Object... objs) {        for (Object o : objs) {            list.add(o);        }    }

可是,数组不一样,数组是编译时就要确认其真实类型,也就是它的Class 信息就是固定的,而不是使用Object来处理。

因此,Java中不能使用 new T[] 来创建泛型数组,因为数组是编译时就必须显式表述其真实类型,也就是 Class,而泛型在编译时没有确定,底层用的Object ,两者冲突无法解决。

在Java7出现了可变参数时,为了兼容性,底层是通过数组实现的。

也就是

void test(Integer ... ints){}void test2(){ test( 1 , 3, 2, 4);}

上述代码中, ints 在运行时实际是 Integer[] , test2 中调用 test 的时候,实际上

void test2(){  Integer[] tmp = new Integer[]{1 , 3, 2, 4}; test(tmp);}

大家可以使用如下代码来执行以下

 public static void main(String[] args) {        test1();        test2();        test3(new ArrayList<>());        test4(new ArrayList<>());    }   static void test1(Integer ... ints){       System.out.println(ints.getClass());    }   static void test2(String ... strs){       System.out.println(strs.getClass());    }    static void test3(List<Integer> list){        System.out.println(list.getClass());    }    static void test4(List<String> list){        System.out.println(list.getClass());    }

上面会分别输出

class [Ljava.lang.Integer;

class [Ljava.lang.String;

class java.util.ArrayList

class java.util.ArrayList

我们发现,数组底层的class 是不一样的,而List尽管泛型不同,底层class 确是一样的。

所以,我们使用可变参数的时候,固定类型是没有问题的,也就是上面的 Integer ... ints,编译的时候就直接使用Integer[] 。 使用泛型也没问题,底层使用 Object 来强转就行。

可是联合使用可变参数和泛型就出问题了。

泛型数组不能直接 new,可变参数又必须隐式new 一个指定类型数组。这怎么办?本来这样的问题就应该不允许这么操作,因为数组真实类型无法确认。可是Jdk 开发人员认为 可变数组和泛型都是非常常用的东西,于是忽视了这种矛盾性,在无法确认T类型的情况下,底层就用能确认的类型来处理了。也就是如下:

 public static void main(String[] args) {        String[] m = toArray("a","B");        System.out.println(m);        String[] m2 = pickTwo("a","B");        System.out.println(m2);    }    static <T> T[] pickTwo(T a,T b){        return toArray(a,b);    }    static <T> T[] toArray(T ... args){        return args;    }

我们会发现,m 是正常执行的,m2这里却报错了。

原因在于:

对于m来说,调用的时候,toArray 能够知道T类型是 String ,因此隐式生成了 String[] ,这样的话用于m接收这个值就是符合类型的。

而m2来说,调用时候没有直接调用,而是通过了pickTwo,可pickTwo 的泛型T 是不可知的,只能推断为Object,那么 toArray 接受的就只能是Object[], m2的类型是String[] . 这样运行时就报错了

以上的例子就是为什么不要联合使用 泛型和可变参数,因为可能产生堆污染,导致编译期通过,运行时报 ClassCastException。这本来是Java静态类型处理的强项,在编译期就发现问题,程序员直接就能消除bug,现在却无法检查出来,让问题走到运行时。

标签: #java创建泛型数组