Java 泛型要点总结

527 阅读13分钟
原文链接: www.jianshu.com

为什么需要泛型?

  通过泛型可以定义类型安全的数据结构,而无须使用实际的数据类型(可扩展)。这能够显著提高性能并得到更高质量的代码,因为您可以重用数据处理算法,而无须复制类型特定的代码(可重用)。  考虑一种普通的、提供传统Push() 和Pop()方法的数据结构(例如,堆栈)。在开发通用堆栈时,您可能愿意使用它来存储各种类型的实例。您可以使用基于 Object 的堆栈,这意味着,在该堆栈中使用的内部数据类型是难以归类的 Object,并且堆栈方法与 Object 交互:

public class Stack {
    Object[] mItems;
    public void push(Object item){...}
    public  Object pop(){...}
}

  上述容器可按下述方式使用:

 Stack stack = new Stack();
        stack.push(1);
        stack.push(2);
        int number = (int)stack.pop();

基于Object 的解决方案存在两个问题:

  • 第一个问题是性能。在使用值类型时,必须boxing & unboxing。装箱和取消装箱都会根据它们自己的权限造成重大的性能损失,但是它还会增加托管堆上的压力,导致更多的垃圾收集工作,而这对于性能而言也不太好。即使是在使用引用类型而不是值类型时,仍然存在性能损失,这是因为必须从 Object 向您要与之交互的实际类型进行强制类型转换,从而造成强制类型转换开销。
  • 第二个问题(通常更为严重)是类型安全。因为编译器允许在任何类型和 Object 之间进行强制类型转换,所以您将丢失编译时类型安全。例如,以下代码可以正确编译,但是在运行时将引发无效强制类型转换异常:  
      Stack stack = new Stack();
      stack.push(1);
      //可以通过编译,但不是类型安全的,将会抛出异常;
      String number = (String)stack.pop();
  • 第三个问题
    您可以通过提供类型特定的(因而是类型安全的)高性能堆栈来克服上述两个问题。对于整型,可以实现并使用 **IntStack。对于字符串,可以实现 StringStack。
    遗憾的是,以这种方式解决性能和类型安全问题,会引起第三个同样严重的问题 — 影响工作效率(无法重用)。编写类型特定的数据结构是一项乏味的、重复性的且易于出错的任务。在修复该数据结构中的缺陷时,您不能只在一个位置修复该缺陷,而必须在实质上是同一数据结构的类型特定的副本所出现的每个位置进行修复。此外,没有办法预知未知的或尚未定义的将来类型的使用情况,因此还必须保持基于 Object 的数据结构。
    既不能用Object,又想避免使用具体类型所带来的重复修改,我们需要泛型。

什么是泛型?

我们首先写一个最简单的泛型类来说明:

public class Generics<T> { //T是type的简称,可以使用任何名称代替  
    private T o; //泛型成员变量  
    public Generics(T o){  
        this.o=o;  
    }  
    public T getObject() {  
        return o;  
    }  
    public void setObject(T o) {  
        this.o = o;  
    }  
    public String getType() {  
        return o.getClass().getName();  
    }  
}

这个泛型类包括自己的构造函数,以及get、set方法和一个getType方法。我们可以这样使用这个类:

public class GenericsDemo{  
    public static void main(String[] args){  
        Generics<Integer> intObject=new Generics<Integer>(1);  
        System.out.println("the value of intObject is " + intObject.getObject());  
        System.out.println("the type of intObject  is " + intObject.getType());  

        Generics<String> stringObject=new Generics<String>("hi, generics!");  
        System.out.println("the value of stringObjectis " + stringObject.getObject());  
        System.out.println("the type of stringObjectis " + stringObject.getType());  
    }  
}

我们可以使用这个泛型类的各种类类型的版本(如例子中的Integer, String, 以及所有Object类及其子类)。不过泛型并不支持基本数据类型。在泛型接口、泛型类和泛型方法的定义过程中,我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,由于接收来自外部使用时候传入的类型实参。

泛型只在编译阶段有效

AyyayList<String> a = new ArrayList<String>();  
ArrayList b = new ArrayList();  
Class c1 = a.getClass();  
Class c2 = b.getClass();  
System.out.println(a == b); //true

上面程序的输出结果为true。所有反射的操作都是在运行时的,既然为true,就证明了编译之后,程序会采取去泛型化的措施,也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。上述结论可通过下面反射的例子来印证:

ArrayList<String> a = new ArrayList<String>();  
a.add("CSDN_SEU_Cavin");  
Class c = a.getClass();  
try{  
    Method method = c.getMethod("add",Object.class);  
    method.invoke(a,100);  
    System.out.println(a);  
}catch(Exception e){  
    e.printStackTrace();  
}

因为绕过了编译阶段也就绕过了泛型,输出结果为:

[CSDN_SEU_Cavin, 100]

类型通配符

简单示例如下:

    List<Integer> ex_int= new ArrayList<Integer>();
    List<Number> ex_num = ex_int;      //非法的
    List<? extends Number>  ex_g = ex_int;  //合法

简单来说,泛型解决了类内部变量的通配,类型通配符解决的是一个类的不同对象拥有不同类型参数时的通配。详解如下:

    public class Animal {
        private String name;
        public Animal(String name) {
            this.name = name;
        }
        public void eat() {
            System.out.println(getName() + " can eat.");
        }
        public String getName() {
            return name;
        }
    }

    public class Cat extends Animal {
        public Cat(String name) {
            super(name);
        }
        public void jump() {
            System.out.println(getName() + " can jump.");
        }
    }

    public class Bird extends Animal {
        public Bird(String name) {
            super(name);
        }
        public void fly() {
            System.out.println(getName() + " can fly.");
        }
    }

    public class Magpie extends Bird {
        public Magpie(String name) {
            super(name);
        }
        public void sing() {
            System.out.println(getName() +
                    " can not only eat,but sing");
        }
    }

首先我们看一下无通配符的使用示例,如下:

public class AnimalTrainer {
        public void act(List<Animal> list) {
            for (Animal animal : list) {
                animal.eat();
            }
        }
    }

测试代码如下:

public class TestAnimal {
        public static void main(String[] args) {
            AnimalTrainer animalTrainer = new AnimalTrainer();
            //Test 1
            List<Animal> animalList = new ArrayList<>();
            animalList.add(new Cat("cat1"));
            animalList.add(new Bird("bird1"));
            animalTrainer.act(animalList);    //可以通过编译

            //Test 2
            List<Cat> catList = new ArrayList<>();
            catList.add(new Cat("cat2"));
            catList.add(new Cat("cat3"));
            animalTrainer.act(catList);        //无法通过编译
        }
    }

如上,Test 1 的执行应该可以理解的,不过顺带提醒一下的是,因为cat1和bird1都是Animal对象,自然可以添加List<Animal>里,具体解释可参考Java泛型基础 。对于Test 2,无法通过编译是因为List<Cat>并不是List<Animal>子类,传入参数有误,也就无法通过编译了。现在尝试去修改AnimalTrainer.act()代码,让它变得更为通用的一点,即不仅仅是接受List<Animal>参数,还可以接受List<Bird>等参数。

  • 通配符的上界既然知道List<Cat>并不是List<Anilmal>的子类型,那就需要去寻找替他解决的办法, 是AnimalTrianer.act()方法变得更为通用(既可以接受List<Animal>类型,也可以接受List<Cat>等参数)。在java里解决办法就是使用通配符“?”,具体到AnimalTrianer,就是将方法改为act(List<? extends Animal> list),当中“?”就是通配符,而“? extends Animal”则表示通配符“?”的上界为Animal,换句话说就是,“? extends Animal”可以代表Animal或其子类,可代表不了Animal的父类(如Object),因为通配符的上界是Animal。如下,为改进之后的AnimalTrianer

    public class AnimalTrainer {
          public void act(List<? extends Animal> list) {
              for (Animal animal : list) {
                  animal.eat();
              }
          }
      }

    再来测试一下,如下,发现Test 2 可以通过编译了:

    public class TestAnimal {
      public static void main(String[] args) {
          AnimalTrainer animalTrainer = new AnimalTrainer();
          //Test 1
          List<Animal> animalList = new ArrayList<>();
          animalList.add(new Cat("cat1"));
          animalList.add(new Bird("bird1"));
          animalTrainer.act(animalList);    //可以通过编译
    
          //Test 2
          List<Cat> catList = new ArrayList<>();
          catList.add(new Cat("cat2"));
          catList.add(new Cat("cat3"));
          animalTrainer.act(catList);        //也可以通过编译
      }
    }

    经过上述分析,可以知道List<Animal>和List<Cat>都是List<? extends Animal>的子类型,类似有List<Bird>,List<Magpie>也是List<? extends Animal>的子类型。现总结如下,对于通配符的上界,有以下几条基本规则:(假设给定的泛型类型为G,(如List<E>中的List),两个具体的泛型参数X、Y,当中Y是X的子类(如上的Animal和Cat))

  • G<? extends Y> 是 G<? extends X>的子类型(如List<? extends Cat> 是 List<? extends Animal>的子类型)。

  • G<X> 是 G<? extends X>的子类型(如List<Animal> 是 List<? extends Animal>的子类型)
  • G<?> 与 G<? extends Object>等同,如List<?> 与List<? extends Objext>等同。

学到这里,可能会遇到一些疑惑的地方,或者说事理解不透的地方,先观察如下两段代码片段,判断一下其是否可行??

    public void testAdd(List<? extends Animal> list){
        //....其他逻辑
        list.add(new Animal("animal"));
        list.add(new Bird("bird"));
        list.add(new Cat("cat"));
    }

    List<? extends Animal> list = new ArrayList<>();
    list.add(new Animal("animal"));
    list.add(new Bird("bird"));
    list.add(new Cat("cat"));

先分析如下:因为“? extends Animal”可代表Animal或其子类(Bird,Cat),那上面的操作应该是可行的。事实上是”不行“,即无法通过编译。为什么呢??

在解释之前,再来重新强调一下已经知道的规则:在List<Aimal> list里只能添加Animal类对象及其子类对象(如Cat和Bird对象),在List<Bird>里只能添加Bird类和其子类对象(如Magpie),可不能添加Animal对象(不是Bird的子类),类似的在List<Cat>和List<Magpie>里只能添加Cat和Bird对象(或其子类对象,不过这没有列出)。现在再回头看一下testAdd()方法,我们知道List<Animal>、List<Cat等都是List<? extends Animal>的子类型。先假设传入的参数为为List<Animal>,则第一段代码的三个“add”操作都是可行的;可如果是List<Bird>呢??则只有第二个“add”可以执行;再假设传入的是List<Tiger>(Tiger是想象出来的,可认为是Cat的子类),则三个“add”操作都不能执行。

现在反过来说,给testAdd传入不同的参数,三个“add”操作都可能引发类型不兼容问题,而传入的参数是未知的,所以java为了保护其类型一致,禁止向List<? extends Animal>添加任意对象,不过却可以添加 null,即list.add(null)是可行的。有了上面谈到的基础,再来理解第二段代码就不难了,因为List<? extends Animal>的类型“? extends Animal”无法确定,可以是Animal,Bird或者Cat等,所以为了保护其类型的一致性,也是不能往list添加任意对象的,不过却可以添加 null。

先总结如下:不能往List<? extends Animal> 添加任意对象,除了null。

另外提醒大家注意的一点是,在List<? extends Animal> 可以是Animal类对象或Bird对象等(只是某一类对象),反过来说,在List<? extends Animal> list里的都是Animal对象,即Bird也是Animal对象,Cat也是Animal对象,那么在Animal里的所有方法都是可以调用的,如下:

for (Animal animal : list) { animal.eat(); }
泛型得弄清楚下面这些的区别

1,List
2,List<Object>
3,List<Number>
4,List<?>
5,List<? extends Number>
6,List<? super Long>
普通的泛型使用估计没啥问题,但是带上 ? 的泛型就会变得很复杂。对于 <? super T> 和 <? extends T> 看看这个例子:如果要将某一个数组中的元素拷贝至另一个的话1,先看看这个方法签名:

public  static  <T> void copy(List<T> src, List<T> dest)

如果 src 和 dest 是相同泛型参数的话,这个签名是没有问题,但是由于泛型类与数组不一样,由于泛型类并不具有协变性,也就是说下面的代码是错误的:

List<Object> objs = new ArrayList<Long>();

数组是协变性的,下面的代码是正确的:Java code

Object[] objs = new Long[4];

用这个 copy 方法的话,如果 src 是 List<Long>,而 dest 是 List<Object> 的话,这样就会产生编译错误。但是实际上 List<Long> 中的每一个元素是可以赋值给 List<Object> 的,因此这个 copy 方法签名还有待完善。由于泛型类不具有协变性,但是 Java 的泛型提供了一个通配符类型 ? 使用这个可以将泛型类变成协变的,下面的代码是正确的:

List<? extends Object> objs = new ArrayList<Long>();

<? extends Object> 表示泛型是 Object 或者是 Object 的子类型,同理<? super Long> 表示泛型是 Long 或者是 Long 的父类型。这样就可以把 copy 方法完善成为:

public static <T> void copy (List<? extends T> src, List<? super T> dest)

至于其他的,看看下面的语句哪些正确的,哪些是不正确的?Pair 是个泛型类,SubTypeOfPair 是 Pair 的子类

Collection<Pair<String, Long>> c1 = new ArrayList<Pair<String, Long>>(); 
Collection<Pair<String, Long>> c2 = c1;            // s1
Collection<Pair<String, ?>> c3 = c1;               // s2
Collection<? extends Pair<String, ?>> c4 = c1;     // s3
Collection<SubTypeOfPair<String, Long>> c1 = new ArrayList<SubTypeOfPair<String, Long>>();

Collection<Pair<String, Long>> c2 = c1;            // s4
Collection<SubTypeOfPair<String, Long>> c3 = c1;   // s5
Collection<Pair<String, ?>> c4 = c1;               // s6
Collection<? extends Pair<String, ?>> c5 = c1;     // s7

泛型使用的注意事项

  1. 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。我们可以使用这个泛型类的各种类类型的版本(如例子中的Integer, String, 以及所有Object类及其子类)。不过泛型并不支持基本数据类型。
  2. 泛型的类型参数可以有多个。
  3. 不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。
    if(ex_num instanceof FX<Number>){   
    }
  4. 不能创建一个确切的泛型类型的数组。下面使用Sun的一篇文档的一个例子来说明这个问题:
    List<String>[] lsa = new List<String>[10]; // Not really allowed.    
    Object o = lsa;    
    Object[] oa = (Object[]) o;    
    List<Integer> li = new ArrayList<Integer>();    
    li.add(new Integer(3));    
    oa[1] = li; // Unsound, but passes run time store check    
    String s = lsa[1].get(0); // Run-time error: ClassCastException.
    这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList<Integer>而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
    下面采用通配符的方式是被允许的:
    List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
    Object o = lsa;    
    Object[] oa = (Object[]) o;    
    List<Integer> li = new ArrayList<Integer>();    
    li.add(new Integer(3));    
    oa[1] = li; // Correct.    
    Integer i = (Integer) lsa[1].get(0); // OK

    参考

    Java技术----Java泛型详解
    Java总结篇系列:Java泛型
    什么是java泛型
    Java 通配符解惑【一】
    Java 通配符解惑【二】