Java泛型

作者: 风骚无俩 | 来源:发表于2020-05-23 17:31 被阅读0次

    Item 26 不要使用原始类型

    原始类型的问题在于类型不确定,在编译阶段看不出来问题,而将隐患拖到运行时

    //一个 原始类型集合,对集合的元素类型没有约束
    private final Collection stamps = ... ;
    //往邮票集合添加硬币,编译器会警告但不报错
    stamps.add(new Coin( ... ));
    //当程序运行到这里时
    for (Iterator i = stamps.iterator(); i.hasNext(); )
       //如果放入了非Stamp对象,这里就会类型转化异常
       Stamp stamp = (Stamp) i.next(); 
    

    开发的一个重要守则就是尽早发现问题排除隐患,能在编译时排查的问题就不要拖到运行时,使用泛型可以有效的在编译阶段排查错误,避免运行时类型转化错误

    //参数化的集合,指定集合元素必须时Stamp类型
    private final Collection<Stamp> stamps = ... ;
    //编译错误,在编译阶段明确告诉你类型转化异常
    //stamps.add(new Coin( ... ));
    stamps.add(new Stamp());
    for (Iterator<Stamp> i = stamps.iterator(); i.hasNext(); )
      //编译器做了隐式转化,因为确定元素是Stamp类型,所以不会有转化错误
      Stamp stamp =  i.next(); 
    

    Java为了和之前没有泛型时代的代码兼容,编译的时候泛型信息会被擦除,在取出元素的时候会隐式的帮你做类型转换,正是有参数类型限制,这种转化可以确保是安全的。

    那原始类型的集合和参数类型为Object的有什么区别,比如List和List<Object>,它们都可以插入任意对象,但却是有区别的,前者逃避泛型检查,而后者是明确参数类型为Object。指定参数类型的List,如List<String>是List的子类型却不是List<Object>的子类型,看下面的示例

    public static void main(String[] args) {
      List<String> strings = new ArrayList<>(); 
      unsafeAdd(strings, Integer.valueOf(42));
      //编译报错
      safeAdd(strings, Integer.valueOf(42));
      String s = strings.get(0); 
    }
    //由于List<String>是List的子类型,这里编译不会报错,只会警告,但类型却是不安全的,因为原始类型逃避泛型检查
    private static void unsafeAdd(List list, Object o) {
       list.add(o);
    }
    //List元素必须是Object类型,否则编译报错
    private static void safeAdd(List<Object> list, Object o) {
       list.add(o);
    }
    

    如果参数类型不重要,使用原始类型作为形参确实很方便,没有约束,但有很大隐患,实际开发中有一个更安全的替代方案:无上限通配符,使用这种参数类型的集合,除了Null你不能添加任何元素,也取不出来任何元素

    public static void main(String[] args) {
            Set<String> s1 = new HashSet<>();
            s1.add("one");
            Set<Integer> s2 = new HashSet<>();
            s2.add(12);
            int result = cacl(s1, s2);
            System.out.println(result);
        }
            //不在乎Set集合元素的类型,但有需要类型安全
        private static int cacl(Set<?> s1, Set<?> s2) {
            int count = 0;
            for (Object object : s2) {
                if (s1.contains(object)) {
                    count++;
                }
            }
            return count;
        }
    

    但有些例外,必须使用原始类型,一个就是类字面量

    //合法
    List.class, String[].class, and int.class
    //不合法
    List<String>.class and List<?>.class
    

    另外一个就是instancof类型检查,因为泛型类型在运行时被擦除,所以除了无上限通配符参数类型外,其它参数类型使用instanceof都不合法,但这个无上限的通配符用上显得多余,所以使用instanceof判断就直接使用原始类型了,下面是推荐写法

    if (o instanceof Set) {
    //必须使用<?>,说明是受检的转换,不会编译警告
      Set<?> s = (Set<?>) o; 
    }
    

    Item 27 消除非受检警告

    尽可能消除编译阶段提示的每个未受检警告,这样可以极大的提高代码运行时的安全。有些时候实现没有办法消除未受检的警告,但你又确定代码是类型安全的,这种情况下可以使用@SuppressWarnings("unchecked")注解明确告诉编译器这个地方不需要警告,但这个注解的使用要注意两点

    • 只有在你确定代码是类型安全的才使用,如果滥用只让编译的结果好看一点,但会隐藏问题给运行时埋下隐患
    • 尽量精确的覆盖作用范围,粒度要小,能用在本地变量上就不要用在方法上,能用在方法上就不要用在类上,使用的地方需要给出注释说明理由

    以ArrayList的toArray一个方法为例,这是来自Android SDK的源码,@SuppressWarnings("unchecked")直接作用在方法上,而且没有说明理由,其实你是看不出来这个注解对 Arrays.copyOf还是 System.arraycopy起作用或者两个方法都需要,如果以后里面添加一个类型不安全的方法,也会被抑制掩盖

    @SuppressWarnings("unchecked")
        public <T> T[] toArray(T[] a) {
            if (a.length < size)
                // Make a new array of a's runtime type, but my contents:
                return (T[]) Arrays.copyOf(elementData, size, a.getClass());
            System.arraycopy(elementData, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }
    

    所以作者推荐的做法是这样的,因为return语句不能使用注解,为了让这个注解的作用范围更加精确,声明了本地变量result让这个注解作用其上,同时需要说明理由,这样代码的维护就更加明朗和安全。

      public <T> T[] toArray(T[] a) {
            if (a.length < size) {
                //这个转换是安全的,因为数组和我们的参数类型都是T
                @SuppressWarnings("unchecked")
                T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
                return result;
            }
            System.arraycopy(elements, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }
    

    总结有两点

    • 重视未受检警告
    • 准确的使用@SuppressWarnings("unchecked")注解,并添加注释说明理由

    Item28列表优先于数组

    为啥优先选择列表呢?因为列表能比数组更早的发现风险,这要从数组不同于列表的两个关键点说起
    数组是协变类型(Covariant),意指如果Child是Parent的子类型,那么Child[]也是Parent[]的子类型,但泛型是不变的,List<Child>和List<Parent>没有类型关系。数组的协变特征会埋下隐患

    //因为数组是协变类型,这个编译是没问题的,却也掩盖了问题
    Object[] objectArray = new Long[1];
    //运行时抛异常ArrayStoreException
    objectArray[0] = "I don't fit in"; 
    

    数组是具体的(reified),数组在运行时是知道元素类型的,而泛型仅在编译时限制元素类型,运行时元素类型会被擦除,这是为了和Java5之前的版本兼容。

    正是由于这两点,泛型和数组无法配合使用,new List<E>[], new List<String>[], new E[]这些表达式都是不合法的,因为泛型数组类型不安全,下面通过反证法说明泛型数组是不应该合法的,如果合法那么泛型的安全机制都荡然无存

    //首先假如泛型数组是合法的,下面这个数组元素类型是List<String>
    List<String>[] stringLists = new List<String>[1]; 
    //又创建一个列表,类型是List<Integer>
    List<Integer> intList = List.of(42);
    //因为数组是协变类型,List<String>是子Object类型,所以List<String>[]也是Object[]的子类型
    Object[] objects = stringLists;
    //因为泛型擦除,运行时List<String>[]变成List[],list<Integer>变成list,所以这里不会抛ArrayStoreException异常
    objects[0] = intList;
    //但最后编译器想把取出的元素转化成String,实际取出的元素是Integer,于是就会抛ClassCastException
    String s = stringLists[0].get(0);
    

    技术上来说,像E、E[]、List<E>这样的类型都是非具体化的,通俗讲就是运行时的信息表达要比编译时少,参数化类型唯一可具体化的是无上限通配符,如 List<?> and Map<?,?>,极少用到但却是合法的。下面代码编译运行都是成功的

    //可创建无上限通配符类型的数组
    ArrayList<?>[] listArray = new ArrayList<?>[10];
    ArrayList<String> strings = new ArrayList<>();
    strings.add("abc");
    listArray[0] = strings;
    
    ArrayList<Integer> integers = new ArrayList<>();
     integers.add(10);
    listArray[1] =integers;
    //参数类型为无上限通配符的List<?>既不能添加元素也不能取出,但可以移除、比较
    System.out.println(listArray[0].remove("abc"));//true
    System.out.println(listArray[1].contains(10));//true
    

    因为可变参数本质也是数组,所以它和泛型也配合不好,有令人不解的警告,可以使用SafeVarargs注解解决这个问题。如果在使用泛型数组时出现错误或警告,最好使用泛型列表替代。下面的代码使用的就是泛型数组,虽然我们确信转换是安全的,也可以通过注解让警告消失,但如果没有警告会更好

    public class Chooser<T> {
        private final T[] choiceArray;
        //参数类型都是T,是安全的
        @SuppressWarnings("unchecked")
        public Chooser(Collection<T> choices) {
            choiceArray = (T[]) choices.toArray();
        }
    // choose method unchanged}
    

    下面使用泛型列表实现,虽然性能不及数组,但没有任何警告和错误,类型是安全的

    public class Chooser<T> {
        private final List<T> choiceList;
        public Chooser(Collection<T> choices) {
          choiceList = new ArrayList<>(choices); 
        }
    
        public T choose() {
          Random rnd = ThreadLocalRandom.current();
          return choiceList.get(rnd.nextInt(choiceList.size())); 
        } 
    }
    

    数组和泛型区别小结如下,如果数组和泛型混用有问题,优先使用列表

    数组 泛型
    协变的,可具体化的 不变的,类型可擦除的
    运行时类型安全,编译时类型不安全 编译时类型安全,运行时类型不安全

    Item29首选泛型

    在Item28中建议当遇到数组与列表时,优先考虑使用列表,但Java并不先天性的支持列表,Java列表是基于数组实现的,如ArrayList,这个时候就必须借助数组。下面是个简单的演示

    public class Stack<E> {
        private E[] elements;
        private int size = 0;
        private static final int DEFAULT_INITIAL_CAPACITY = 16;
        //elements 是私有的,引用没有外泄,包含元素只有push方法的E,所以确定是类型安全的
        @SuppressWarnings("unchecked")
        public Stack() {
            elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
        }
    
        public void push(E e) {
            elements[size++] = e;
        }
    
        public E pop() {
            if (size == 0)
                throw new EmptyStackException();
            E result = elements[--size];
            elements[size] = null; 
            return result;
        }
    }
    

    上面这个泛型的实现还是很简洁的,只需要做一次类型转换,但缺点是有堆污染,也就是数组的编译时类型和运行时类型不一样,除非E是Object类型,即使这种情况下堆污染是无害的,也有些令人不爽,但我们还有下面的方案:使用Object数组,对取出的元素内部转换,而不是让客户端去做类型转换,这也是ArrayList的做法。

    public class Stack<E> {
        private Object[] elements;
        private int size = 0;
        private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
        public Stack() {
            elements = new Object[DEFAULT_INITIAL_CAPACITY];
        }
    
        public void push(E e) {
            elements[size++] = e;
        }
    
        public E pop() {
            if (size == 0)
                throw new EmptyStackException();
            //push时的元素只能是E,所以这里是类型安全的
            @SuppressWarnings("unchecked")
            E result = (E) elements[--size];
            elements[size] = null;
            return result;
        }
    }
    

    上面泛型的参数类型只要是引用就是合法的,有些场景我们需要对参数类型进行限制,也就是有限制的类型参数,如下

    public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
       implements BlockingQueue<E> {
    

    E extends Delayed说明参数类型必须是Delayed的子类,这样客户端在使用Delayed方法时就不需要冒险强转。泛型比那些需要在客户端做转换的类型要简单和安全,如有可能尽量泛型化。

    Item 30 首选泛型方法

    和类一样,方法也可以从泛型中获得同样的好处,尤其是静态工具方法,一个简单的静态泛型方法如下,特殊的地方在于修饰符static和返回值Set<E> 之间需要有类型参数<E>,这个泛型方法的两个输入参数和返回参数类型是一样的,如果使用有限制的通配符会更加灵活,这个下一节说明。

    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
      Set<E> result = new HashSet<>(s1); 
      result.addAll(s2);
      return result;
    }
    

    书中介绍的常用于函数的泛型单列工厂模式,没看出来有什么用途,暂时跳过。

     // Generic singleton factory pattern
    private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
       @SuppressWarnings("unchecked")
    
    public static <T> UnaryOperator<T> identityFunction() { 
      return (UnaryOperator<T>) IDENTITY_FN;
    }
    

    深入理解 Java Object一文中介绍的比较两个对象是否相等的equals方法,就会发现使用的不是泛型,实现比较的代码就比较冗长且模版化

    PhoneNumber.java
    @Override
        public boolean equals(Object o) {
            //判断引用是否相等
            if (o == this) {
                return true;
            }
            if (!(o instanceof PhoneNumber)) {
                return false;
            }
          
          PhoneNumber pNum = (PhoneNumber) o;
    
       ...
    
    

    而对象如果要比较排序的话,通常需要实现下面这个泛型接口

    public interface Comparable<T> {
           int compareTo(T o);
    }
    

    类型参数T限制了需要比较的对象的类型,因为绝大部分比较都是在同类型之间进行,这个方法的复写就比equals简洁的多

    PhoneNumber.java
    public class PhoneNumber implements Comparable<PhoneNumber> {
    @Override
        public int compareTo(PhoneNumber phoneNumber) {
            int result = Short.compare(areaCode, phoneNumber.areaCode);
            if (result == 0) {
                result = Short.compare(prefix, phoneNumber.prefix);
                if (result == 0) {
                    result = Short.compare(linNum, phoneNumber.linNum);
                }
            }
            return result;
        }
    }
    

    Comparable<T>接口出现在泛型方法中通常都伴随着一个令人头疼的名字:递归类型限制,如下所示的<E extends Comparable<E>>

    public static <E extends Comparable<E>> E max(Collection<E> c);
    

    类型限制<E extends Comparable<E>>可以读作针对可以与自身进行比较的每个类型E,也就是列表中的每个元素E都是可以互相比较的。

    总之,泛型方法和泛型一样,不需要转化参数就能使用,这样更加安全简洁,所以尽可能将方法泛型化。

    Item 31 使用有限制通配符提升API的灵活性

    在Item 28中提到过:参数化类型是不可变的。比如Integer是Number的子类,但List<Integer>并不是List<Number>的子类,导致下面的例子编译不通过,这种无限制的类型参数降低了代码的灵活性

    public class Test {
        public static void main(String[] args) {
            List<Integer> integers=new ArrayList<>(10);
            integers.add(2);
            //print(integers);编译不通过
        }
        public static void print(List<Number> number){
            System.out.println(number.toString());  
        }
    
    }
    

    还好Java提供了一种有限制的通配符类型解决这类问题,我们对print方法作如下修改,这时输入参数的含义就不是Number的集合,而是Number的某个子类的集合(包括Number自己)。但示例仅仅说明有限制的通配符类型,并没有涉及泛型

    public class Test {
        public static void main(String[] args) {
            List<Integer> integers=new ArrayList<>(10);
            integers.add(2);
            print(integers);
        }
            //使用有限制的通配符类型
        public static void print(List<? extends Number> number){
            System.out.println(number.toString());  
        }
    
    }
    

    在Item 29我们有个泛型类Stack

    public class Stack<E> {
           public Stack();
           public void push(E e);
           public E pop();
           public boolean isEmpty();
    }
    

    如果我们添加一个方法addAll,按照之前的思路会是下面这个样子

    public void pushAll(Iterable<E> src) {
          for (E e : src)
           push(e);
    }
    

    如果一个名为Obj的对象是E的子类,调用push(Obj)没有问题,但如前文所述,调用pushAll(Iterable<Obj>)就不行,解决方案就是改成下面这个样子

    public void pushAll(Iterable<? extends E> src) {
           for (E e : src)
               push(e);
    }
    

    如果再添加一个方法,把Stack所有元素弹出到某个集合中,根据经验应该写成这样,其实这样编译不通过的,Java中父类引用可以指向子类对象,但反过来不行,下面就犯了这个错误,dst中的引用是E的子类类型,pop返回的是E,就好像往List<Integer>添加Nubmer,所以无法添加

    public void popAll(Collection<? extends E> dst) {
               while (!isEmpty())
               dst.add(pop());
    }
    

    知道错误在哪就好办了,只要 限定dst中元素引用是E的父类类型即可,只要把extends改成super即可

    public void popAll(Collection<? super E> dst) {
       while (!isEmpty()) dst.add(pop());
    }
    
    

    关于使用extends还是super有一个口诀

    • 如果输入参数是生产者,如addAll的参数,使用extends
    • 如果是消费者,如popAll的参数,使用super

    在上一节有这样一个泛型方法,之前也提到了这种输入参数和返回参数类型都一致的泛型不够灵活,只能是同一种类型

    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
      Set<E> result = new HashSet<>(s1); 
      result.addAll(s2);
      return result;
    }
    

    这个方法如果想把Integer类型和Double类型合并就做不到,所以需要使用有限制的通配符类型,两个参数都是生产者,修改如下,注意返回参数类型没变

    public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
      Set<E> result = new HashSet<>(s1); 
      result.addAll(s2);
      return result;
    }
    

    这种带泛型的有限通配符类型方法使用起来就更加灵活

    public static void main(String[] args) {
           //Set没有of这个方法,编译是不通过的,为了演示简洁
           Set<Integer> integers = Set.of(1, 3, 5);
           Set<Double> doubles = Set.of(2.0, 4.0, 6.0); 
           //Java 8可以推导出E的类型为Number
           Set<Number> numbers = union(integers, doubles);
           //Java 8 之前无法推导出,需要使用显示的类型参数
           // Set<Number> numbers2 = Test.<NUmber>union(integers, doubles);
           
       }
    

    现在回头看Item 30的max方法,发现也有优化空间

    • 参数c是生产者,从Collection<E>改成Collection<? extends E>
    • Comparable始终是消费者,Comparable<? super E>优于Comparable<E>
    //优化前
    public static <E extends Comparable<E>> E max(Collection<E> c);
    //优化后
    public static <E extends Comparable<? super E>> E max(Collection<? extends E> c);
    

    举个例子说明这样优化的好处,下面代码来自JDK。Delayed接口继承了Comparable接口,类型参数为Delayed

    public interface Delayed extends Comparable<Delayed> {
        long getDelay(TimeUnit unit);
    }
    

    ScheduledFuture又继承了Delayed接口,所以间接的继承了Comparable接口,但是Comparable的类型参数不是ScheduledFuture,而是父类Delayed

    public interface ScheduledFuture<V> extends Delayed, Future<V> {
    }
    

    所以下面这个集合就不能传入优化前的max方法

    List<ScheduledFuture<?>> scheduledFutures = ... ;
    

    因为ScheduledFuture没有 extends Comparable< ScheduledFuture >,而是extends Comparable<Delayed >,Delayed是Comparable的父类。但优化后的max就可以,因为Comparable的类型参数声明为E,也就是这里的ScheduledFuture的父类。

    关于类型参数和通配符还有一些值得讨论,比如下面两个交换元素的静态方法,如果类型参数只在方法中出现一次,就可以用通配符替换

    //使用类型参数实现
    public static <E> void swap(List<E> list, int i, int j);
    //使用通配符实现,更简单
     public static void swap(List<?> list, int i, int j);
    

    虽然推荐第二种方法,但List<?>有一个问题,就是除了Null,不能添加其它元素,好在我们有一个辅助方法弥补这个缺陷

    public static void swap(List<?> list, int i, int j) {
           swapHelper(list, i, j);
    }
       //使用私有的方法捕获通配符类型
    private static <E> void swapHelper(List<E> list, int i, int j) { 
      list.set(i, list.set(j, list.get(i)));
    }
    

    总的来说使用通配符让API更加灵活,基本原则就是

    • 生产者使用extends
    • 消费者使用super,所有的comparables 和 comparators 都是消费者

    Item 32 谨慎结合泛型和可变参数

    可变参数可以极大的方便方法的调用,它允许给一个方法传递可变数量的参数,如下所示

    public static void main(String[] args) {
            show("one","two");
            show("one","two","three");
    }
    
    public static  void show(String... msgs) {
            for (String string : msgs) {
                System.out.println(string);
            }
    }
    

    但这样你可能还不满足,参数String是具体类型,如果改成下面这样的泛型参数岂不更妙,

    public static void main(String[] args) {
           show("one","two");
           show(1,3,4);
           show(1.2f,3,4f);
    }
    
    public static <T>  void show(T... msgs) {
           for (T string : msgs) {
               System.out.println(string);
           }
    }
    

    虽然运行是没问题的,但有些智能编辑器会警告你:msgs这个可变参数会导致堆污染。可变参数是这样实现的,当调用方法时,创建数组来保存可变长的参数,泛型在编译的时候被擦除,所以所以T... msgs就变成了Object[],但在运行的时候,这个应用指向的类型可能是String[]、Interger[]等,也就是编译时类型和运行时类型不匹配导致的堆污染,容易出现类型转换异常。看下面这个示例。

    static void dangerous(List<String>... stringLists) {
          List<Integer> intList = List.of(42);
          //stringLists=List<String>[],数组时协变类型,没有问题
          Object[] objects = stringLists;
          //object[0]的引用是参数化类型List<String>,指向了参数化类型对象List<Integer>,导致堆污染
          objects[0] = intList;
         // 虽然没有显式的cast,但会报ClassCastException
          String s = stringLists[0].get(0);
    }
    

    在Item 28里说过像new E[],new List<String>[]这样的显式的泛型数组或参数化类型数组是不合法的,但可变参数却可以隐式的使用它们,这是因为泛型和参数化类型的数组在实际开发中实在太好用了,就破例允许这种不一致的存在,下面是JDK一些源码示例。

    Arrays.java
        @SafeVarargs
        @SuppressWarnings("varargs")
        public static <T> List<T> asList(T... a) {
            //注意:此处ArrayList为Arrays的私有静态内部类
            return new ArrayList<>(a);
        }
    
    Collections.java
        @SafeVarargs
        public static <T> boolean addAll(Collection<? super T> c, T... elements) {
            boolean result = false;
            for (T element : elements)
                result |= c.add(element);
            return result;
        }
    
    EnumSet.java
    @SafeVarargs
        public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) {
            EnumSet<E> result = noneOf(first.getDeclaringClass());
            result.add(first);
            for (E e : rest)
                result.add(e);
            return result;
        }
    

    作者只有在确定可变参数是类型安全的,才可以使用@SafeVarargs注解告知编译器不要警告,那怎样才是安全的,需要满足以下条件

    • 不要使用可变参数存储任何值,很容易导致类型不匹配
    • 不要暴露可变参数的引用
    //使用可变参数存储了其它值,违反第一条
    objects[0] = intList;
    
    //暴露了可变参数引用,违反了第二条
    static <T> T[] toArray(T... args) { 
        return args;
    }
    

    下面一个示例使用了上面的toArray方法,编译运行都是正常的,但要注意返回数组的类型是由传入的参数的编译时类型决定的,编译器可能没有足够的信息作准确的判断

    public static void main(String[] args) {
                    //传人的参数编译时类型为String
            String[] msg=toArray("one","two");
                    //为Integer
            Integer[] integer=toArray(1,3,4);       
    }
    //下面是反编译的字节码  参数类型都变成了Object,但做了正确的类型转换
    invokestatic  Method toArray:([Ljava/lang/Object;)[Ljava/lang/Object;
    checkcast     class "[Ljava/lang/String;"
    
    invokestatic  Method toArray:([Ljava/lang/Object;)[Ljava/lang/Object;
    checkcast  class "[Ljava/lang/Integer;"
    

    如果上面这个可以使用,下面这个应该也没什么问题

    public static void main(String[] args) {        
        String[] reuslt=pickTwo("t1", "t2");
    }
    
    static <T> T[] pickTwo(T a, T b,) { 
      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
    }
    //没有checkcast,返回的就是Object数组,实际类型信息丢失
    invokestatic    Method toArray:([Ljava/lang/Object;)[Ljava/lang/Object;
    
    invokestatic    Method pickTwo(Ljava/lang/Object;Ljava/lang/Object;)[Ljava/lang/Object;
     checkcast               class "[Ljava/lang/String;"
    

    实际上编译的时候确实没有问题,但运行起来就会报错:ava.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    因为此时toArray接受到的参数编译时类型为Object,所以返回的是Object数组,pickTwo接受到的也是Object数组,强转成String[]就报错。如果如下去除泛型,确定pickTwo的参数类型,也不会报错,只是没什么意义。

    static String[] pickTwo(String t1,String t2){
            return  toArray(t1,t2); 
    }
    

    下面是一个典型的正确使用泛型可变参数的案例,注意:@SafeVarargs只能用在静态方法、不可覆盖方法和私有方法上(Java 9)

    @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;
    }
    

    如果你不想使用麻烦的泛型可变参数,可以使用集合替代,并配合Java 9中的List.of,它使用了@safeVarargs注解,是类型安全的

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

    不过下面原书的例子我觉得是有问题的,编译不通过,参数类型不匹配

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

    前面有问题的代码就可以用集合解决,

    public static void main(String[] args) {
        List<String> attributes = pickTwo("Good", "Fast", "Cheap");
    }
    
    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();
     }
    

    总的来说,可变参数底层使用的是数组,和泛型配合不好,如果可变参数要结合泛型,一定要遵守下面事项,保证安全才能用@safeVarargs注解

    • 它没有在可变参数数组中保存任何值
    • 它没有对不被信任的代码开发该数组

    Item33 优选类型安全的异构容器

    虽然按照规则使用泛型可以保证类型是安全的,但如果和原始类型混用泛型就有类型安全隐患,下面段代码编译的时候有unchecked warning,但可以运行,也就是整数1添加到了类型参数为String的集合中,只要不取出来涉及类型转换就不会发现问题

     List<String> a=new ArrayList<>();
    //使用原始类型绕过泛型检查
     List b=a;
    //成功添加
     b.add(1);
    

    这是一种隐患,对于错误应该越早发现越好。Java SDK的Conllections工具类提供如下解决方法,除了泛型,还添加了String.class这个字面量,其传到方法作为参数表示的是Class<String>,它可以作为类型令牌

    //checkedList返回一个对ArrayList的包装类
    List<String> c=Collections.checkedList(new ArrayList<>(), String.class) ;
    List d=c;
    //添加失败
    d.add(1);
    
    //Collections.java
     public static <E> List<E> checkedList(List<E> list, Class<E> type) {
            return (list instanceof RandomAccess ?
                    new CheckedRandomAccessList<>(list, type) :
                    new CheckedList<>(list, type));
     }
    

    上面这段代码编译的时候也有unchecked ,但运行就会报ClassCastException,原理就是添加之前利用Class<T>这个类型令牌作类型检查

    Collections.CheckedCollection.java
    public boolean add(E e) { 
        return c.add(typeCheck(e)); 
    }
    
    
     E typeCheck(Object o) {
            //类型检查
        if (o != null && !type.isInstance(o))
           throw new ClassCastException(badElementMsg(o));
          return (E) o;
     }
    

    相关文章

      网友评论

        本文标题:Java泛型

        本文链接:https://www.haomeiwen.com/subject/esyaohtx.html