Java泛型

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 stamps = ... ;
//编译错误,在编译阶段明确告诉你类型转化异常
//stamps.add(new Coin( ... ));
stamps.add(new Stamp());
for (Iterator i = stamps.iterator(); i.hasNext(); )
  //编译器做了隐式转化,因为确定元素是Stamp类型,所以不会有转化错误
  Stamp stamp =  i.next(); 

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

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

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

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

public static void main(String[] args) {
        Set s1 = new HashSet<>();
        s1.add("one");
        Set 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.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[] 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[] 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和List没有类型关系。数组的协变特征会埋下隐患

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

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

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

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

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

//可创建无上限通配符类型的数组
ArrayList[] listArray = new ArrayList[10];
ArrayList strings = new ArrayList<>();
strings.add("abc");
listArray[0] = strings;

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

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

public class Chooser {
    private final List choiceList;
    public Chooser(Collection 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 {
    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 {
    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 extends AbstractQueue
   implements BlockingQueue {

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

Item 30 首选泛型方法

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

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

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

 // Generic singleton factory pattern
private static UnaryOperator IDENTITY_FN = (t) -> t;
   @SuppressWarnings("unchecked")

public static  UnaryOperator identityFunction() { 
  return (UnaryOperator) 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 {
       int compareTo(T o);
}

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

PhoneNumber.java
public class PhoneNumber implements Comparable {
@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接口出现在泛型方法中通常都伴随着一个令人头疼的名字:递归类型限制,如下所示的>

public static > E max(Collection c);

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

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

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

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

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

}

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

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

}

在Item 29我们有个泛型类Stack

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

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

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

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

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

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

public void popAll(Collection dst) {
           while (!isEmpty())
           dst.add(pop());
}

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

public void popAll(Collection dst) {
   while (!isEmpty()) dst.add(pop());
}

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

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

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

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

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

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

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

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

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

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

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

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

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

public interface ScheduledFuture extends Delayed, Future {
}

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

List> scheduledFutures = ... ;

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

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

//使用类型参数实现
public static  void swap(List 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  void swapHelper(List 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   void show(T... msgs) {
       for (T string : msgs) {
           System.out.println(string);
       }
}

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

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

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

Arrays.java
    @SafeVarargs
    @SuppressWarnings("varargs")
    public static  List asList(T... a) {
        //注意:此处ArrayList为Arrays的私有静态内部类
        return new ArrayList<>(a);
    }

Collections.java
    @SafeVarargs
    public static  boolean addAll(Collection c, T... elements) {
        boolean result = false;
        for (T element : elements)
            result |= c.add(element);
        return result;
    }

EnumSet.java
@SafeVarargs
    public static > EnumSet of(E first, E... rest) {
        EnumSet result = noneOf(first.getDeclaringClass());
        result.add(first);
        for (E e : rest)
            result.add(e);
        return result;
    }

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

  • 不要使用可变参数存储任何值,很容易导致类型不匹配
  • 不要暴露可变参数的引用
//使用可变参数存储了其它值,违反第一条
objects[0] = intList;

//暴露了可变参数引用,违反了第二条
static  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[] 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  List flatten(List... lists) {
    List result = new ArrayList<>();
       for (List list : lists)
          result.addAll(list); 
    return result;
}

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

static  List flatten(List> lists) {
    List result = new ArrayList<>(); 
      for (List list : lists)
        result.addAll(list); 
    return result;
}

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

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

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

public static void main(String[] args) {
    List attributes = pickTwo("Good", "Fast", "Cheap");
}

static  List 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 a=new ArrayList<>();
//使用原始类型绕过泛型检查
 List b=a;
//成功添加
 b.add(1);

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

//checkedList返回一个对ArrayList的包装类
List c=Collections.checkedList(new ArrayList<>(), String.class) ;
List d=c;
//添加失败
d.add(1);

//Collections.java
 public static  List checkedList(List list, Class type) {
        return (list instanceof RandomAccess ?
                new CheckedRandomAccessList<>(list, type) :
                new CheckedList<>(list, type));
 }

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

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泛型)