Java泛型小结

什么是泛型

泛型, 可以理解为对类型的抽象。指定了泛型参数的类型就是一个具体化了的类型。一个泛型和其指定了泛型参数的类型的关系,在概念上更像是父类和子类的关系,是一种抽象与具体的关系。我自己觉得他是在面向对象编程思想多态的特点上对实际情况的更高一层的抽象。

Java为什么引入泛型

Java中的泛型是在SE5中加入的概念,也就是说Java一开始是不支持泛型的。那么Java为什么要引入泛型机制呢?

代码的复用性

首先为了代码的复用性,也就是为了使用户能更加泛化自己的代码。正如前面所说的,泛型是面向对象编程思想多态的特点上对实际情况更高一层的抽象,这种抽象更凌驾于父类-子类这种关系之上。因此提供了更高层的抽象能力。这是其第一个目的,但是不是其最重要的目的。我们知道在Java中Object类是所有类的父类。那么,如果单纯为了代码的复用性,我们是不是通过只持有Object类的对象就可以解决了:

class MyClass{

  private Object object;

  public setObject(Object object){
    this.object=object;
  }

  public getObject(){
    return object;
  }

}

这个类可以持有Java中的所有对象,如果单单从泛化的角度来说,这其实就够了。那么为什么还要引入泛型呢?这就引出了引入泛型概念的第二个目的-编译期类型检查

编译期类型检查

还以我们前面定义的MyClass为例子,虽然它可以持有Java中的所有对象,但是要看一下我们在程序中使用时是多么的麻烦。

public class TestA {
    public String aTalk(){
        return "this is TestA";
    }
}
public class TestB {
    public String bTalk(){
        return "this is TestB";
    }
}

public class Main {
  public static void main(String[] args){
    MyClass myClass=new MyClass();
    myClass.setObject(new TestA());
    TestA testA=(TestA)myClass.getObject();
    System.out.println(testA.aTalk());
    myClass.setObject(new TestB());
    TestB testB=(TestB)myClass.getObject();
    System.out.println(testB.bTalk());
  }
}

可以看到,虽然程序可以运行正确,但前提是在我们一路强转,并且一路强转正确的情况下程序才能运行正确的。但实际用在工程中呢?这段程序存在两个比较大的问题:a、程序强制转换太多,很容易强转出错,导致程序运行异常。b、即使程序强转出错了,在编译器在编译期也检查不出来,只能在程序运行的时候抛异常。虽然大部分程序猿是很靠谱的,但是不排除那一部分不靠谱的。嗯哼,怎么拯救这些程序猿呢?OK引入泛型吧。看下用泛型重写以上代码的结果吧。

public class MyClass1<T> {
    private T object;
    public void setObject(T object){
        this.object=object;
    }
    public T getObject(){
        return object;
    }
}

public class Main {
    public static void main(String[] args){
        MyClass1<TestA> myClass=new MyClass1<TestA>();
        myClass.setObject(new TestA());
        TestA testA=myClass.getObject();
        System.out.println(testA.aTalk());
        /*myClass.setObject(new TestB());           //编译错误
        TestB testB=(TestB)myClass.getObject();
        System.out.println(testB.bTalk());*/

    }
}

可以看到在这个例子中,并没有像上例中那样人为地对元素进行强转。从而避免了人为因素导致的强转错误而程序执行失败。而且对于本身就错误的代码,编译器可以在编译期就给出错误提示,而不用让我们等到运行时才提示错误。之所以泛型能让我们这么省事,其实是编译器帮忙做了很多工作。这里先不谈这些编译器为我们做了哪些工作,因为涉及到Java是如何实现泛型的问题,文章最后会提到。接下来我们看Java都对泛型做了哪些支持。

Java对泛型的支持类型

泛型可应用的场合

泛型类

使用方法如上节中的MyClass。

泛型接口

和泛型类的使用方法相同,只不过类声明的时候使用class而接口使用interface。

泛型方法

在Java中,上一节中MyClass就是典型的泛型类的使用,除此之外还有影响范围更小的泛型方法可以使用。泛型方法的使用和泛型类的使用是相互独立的。并且在TIJ里面明确指出如果可以使用泛型方法来解决的问题,就不要使用泛型类了。究其原因,我想是由于如果使用泛型类,那么如果想处理另外一种类型的话,免不了要生成一个针对那种类型的新类。但是如果使用泛型方法就不用,他只是在调用泛型方法的时候确定参数类型,不用生成新类。ok,泛型方法的定义一般如下所示:

public class GernericMethod {
    public <T> void printClazz(T clazz){
        System.out.println(clazz.getClass().getName());
    }
}

注意格式,泛型参数要写在返回类型前面哦。其使用方法如下:

public class Main {
    public static void main(String[] args){
        GernericMethod gernericMethod=new GernericMethod();
        gernericMethod.printClazz(1);
        gernericMethod.printClazz("天啦噜!");
    }
}

其输出结果如下:

java.lang.Integer
java.lang.String

程序在执行gernericMethod.printClazz(1)这一句的时候,由于Java的泛型不支持源生类型如int,long等,因此Java通过autoBoxing将1包装成了Integer类型的了。细心点的同学可能发现了,上面在对泛型方法的调用中,并没有什么迹象表明这是在调用一个泛型方法。可以看到,在使用泛型类时,在声明的时候我们需要使用MyClass[TestA]这种形式来显示的声明泛型类的类型参数,但是在使用泛型方法的时候却完全没有使用到相关的类型信息。这是为什么呢?Java中的泛型方法有一种叫做argument inference的能力,在调用泛型方法的时候,他可以通过你的调用过程来推断出自己的类型参数是什么,而不用我们来指定。在很多情况下,泛型方法的这个特性可以用来简化对Java容器类的声明过程。例如在Guava中的Maps等容器工具类中,都有构造JDK容器类的静态工具方法如Maps.new HashMap()。其实内部就是利用的泛型方法的类型推断机制。
但是有一点需要注意,大家不要以为泛型方法这种argument inference的能力是万能的,Type inference doesn’t work for anything other than assignment.只有在赋值语句或者是上面这种情形下,这种能力才能生效,很多情况下如:其返回值作为参数交给其他方法去处理的时候就不生效了。但是JDK1.8中对于Java中的类型推导做了很大的改进。Java的整个类型推导有了很大的改善。不论是泛型类的声明还是使用都变的很智能了。详见JDK1.8泛型类型推导

泛型声明的方式

普通声明方式

普通的声明方式就如同上例中的MyClass一样:

public class MyClass<T> {
    private T object;

    public void setObject(T object) {
        this.object = object;
    }

    public T getObject() {
        return object;
    }
}

带有限定符的声明方式

带有限定符的声明方式会把类型参数限定为某一个类的子类,其一般声明方式如下:

public class MyClass<T extends TestB> {
    private T object;

    public void setObject(T object) {
        this.object = object;
    }

    public T getObject() {
        return object;
    }

    public void action() {
        object.bTalk();
    }

    public static void main(String[] args) {
        // MyClass<TestA> myClass = new MyClass<TestA>();
        // myClass.setObject(new TestA());
        // TestA testA = myClass.getObject();
        // System.out.println(testA.aTalk());
        // myClass.setObject(new TestB()); //编译错误 TestB testB=(TestB)myClass.getObject();
        // System.out.println(testB.bTalk());
        MyClass<TestB> testBMyClass = new MyClass<TestB>();
        testBMyClass.setObject(new TestB());
        TestB testB = testBMyClass.getObject();
        System.out.println(testB.bTalk());
    }
}

如上,如果在声明MyClass时,限定了其类型参数为T extend TestB,那么就限制了MyClass在使用的时候所能持有的类型必须为TestB类型或者其子类型。如果还想持有TestA类型,就如main方法中第一行所示。编译期直接会报错Type parameter "TestA" is not within its bound, should extend TestB
但是也有一个好处,如MyClass类中新加的那个action方法,现在可以调用持有类型的bTalk方法了。为什么没有限定参数T的时候不能调用bTalk方法呢?因为Java类型擦除机制的原因(稍后会解释)。类型参数T直接被认为成Object来处理。因此程序并不能知道其持有对象有bTalk方法。而当限定了泛型参数为TestB的子类时,Java就会把类型参数T当成TestB来处理,因为他知道MyClass能持有的类型最高只能是TestB类(不是和TestB一系的对象,在编译期是放不到MyClass中去的,直接编译报错)。后面类型擦除机制的时候会详细讲解。

自限定泛型

在带有限定符的泛型声明方式中有一朵奇葩,那就是自限定类型的泛型,其最为常见的一个例子就是JDK中的Enum类,可以去源码中看一下它的声明方式:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
    private final String name;
    public final int compareTo(E o){......}
    public final Class<E> getDeclaringClass() {......}
    ....
    ....
}

单看这一句class Enum<E extends Enum<E>> 是不是有一种Enum类型是一个无线递归的概念。理解起来就像Enum类的参数类型是E,而E是继承自Enum[E]的,而它继承的这个Enum[E]中的E又是继承自Enum[E]的……。但是这种声明方式并没有递归的意思在里面。容我喝口水一一道来。
在Java里面,Enum是所有枚举类的父类,所有Enum类型都会被编译器解释成继承自Enum类的一个子类。而Enum存在的意义就是为所有的这些子类提供其公用的方法。举个栗子,假设有个Color的枚举类型声明如下:

enum Color {RED, BLUE, GREEN}

而编译器则会把他编译成类似下面的这种方式。注意是编译器把Color硬写成Color extends Enum[Color]的,而不是说Color extends Enum[somethin else extends Enum]就不行,后面有个例子。

public final class Color extends Enum<Color> { 
  public static final Color[] values() { return (Color[])$VALUES.clone(); } 
  public static Color valueOf(String name) { ... }
  private Color(String s, int i) { super(s, i); }

  public static final Color RED; 
  public static final Color BLUE; 
  public static final Color GREEN;

  private static final Color $VALUES[];

  static { 
    RED = new Color("RED", 0); 
    BLUE = new Color("BLUE", 1); 
    GREEN = new Color("GREEN", 2); 
    $VALUES = (new Color[] { RED, BLUE, GREEN }); 
  } 
}

Color继承自Enum的意思也就是说Color拥有了Enum所有的方法。嗯哼,重点来了,Enum是有compareTo方法的啊,那么Color从Enum里面继承过来的compareTo方法,应该有一个怎样的参数啊。从直观上来讲,Color只能跟Color比吧,不能跟其他的什么不是枚举类或者其它枚举比吧,因此compareTo的参数也必须支持变更为Enum的各种子类。但是呢compareTo是枚举类大家公有的方法,必须写在Enum里面,因此把compareTo方法的参数设为了泛型已支持Enum的各种子类型。那总得有个什么方法来限制程序员,让他不能在继承Enum的时候,乱写其类型参数吧。恩,对的,这个目的就是通过上面的声明达到的。也就是说,Enum的类似递归的声明其实是在表达:任何继承自我的类,他给我传的的类型参数都必须是我,或者我的子类。下面通过一个例子来解释:

class SelfBounded<T extends SelfBounded<T>>{
    T element;
    SelfBounded<T> set(T arg){
        this.element=arg;
        return this;
    }
    T get(){
        return element;
    }
}
class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{}
class C extends SelfBounded<C>{
    C setAndGet(C arg){
        set(arg);
        return get();
    }
}
class D{}
//class E extends SelfBounded<D>{}
class F extends SelfBounded{}
public class Main {
    public static void main(String[] args) {
        A a=new A();
        a.set(new A());
        a=a.set(new A()).get();
        a=a.get();
        C c=new C();
        c=c.setAndGet(new C());
        F f = new F();
        SelfBounded selfBounded = f.get();
    }
}

如上,我们声明了自限定类型SelfBounded,然后声明了类型A。注意这个时候SelfBoundes的类型参数里面只能填A,因为SelfBounded到现在只有A一个子类。但是在声明B的时候,可以看到,我们不一定非要给其类型参数里面传入B,只要传入的是SelfBounded的子类就可以了,我们传了A。注意看在声明A的时候,我们传给类型参数的是E,编译器会报错,编译不通过,因为D不是SelfBounded的子类。另外我们声明了F,什么类型参数都没有传给它,那编译期默认就是用的是默认左边界SelfBounded。
要区分两个概念,class SelfBounded<T extends SelfBounded<T>> 这种声明方式只是限定了SelfBounded的类型参数只能是其子类。而没有表示非得是继承他的子类本身,如例子中声明的B类。而Java中,我们声明的枚举类如Color,是一种特殊情况,被编译成public final class Color extends Enum<Color> 是编译器限定死了类型参数就是继承Enum的那个类本身Color,毕竟这样做是有意义的,因为Color只能和Color枚举比。

泛型的使用方式

普通方式

普通方式被称为exact方式,就是说List[A] alist=new List[A]() 这样,类型完全一样的泛型之间的相互赋值。

上界通配符

我们来看一个例子

package com.person.yecheng.li.blogcases;

import java.util.ArrayList;
import java.util.List;

/**
 * Version: 1.0.0 Date:2017-08-05 Time: 17:44 Author: yecheng.li.
 */
class Fruit {
    public String isWhat() {
        return "Fruit";
    }
}

class Apple extends Fruit {
    @Override
    public String isWhat() {
        return "Apple";
    }
}

class Orange extends Fruit {
    @Override
    public String isWhat() {
        return "Orange";
    }
}

public class WildCards {
    public static void main(String[] args) {
        List<Apple> apples = new ArrayList<Apple>();
        // List<Fruit> fruits=apples;
        Apple[] arrayApples = new Apple[10];
        Fruit[] arrayFruits = arrayApples;
        arrayFruits[0] = new Orange();
    }
}

WildCards中,我们先声明了一个Apple的List apples,然后我们准备将apples赋值给一个Fruit的List fruits。但是编译器不让,给出一个报错。这是为什么呢?作为对比,我们拿一个在Java中和List非常相近的数据结构-array做了同样的操作,一个apple的数组赋值给一个fruit的数组完全没有问题。其实直观上来讲也很合理,一堆苹果不也就是一堆水果嘛,将其赋于一个更大的概念没什么错。然后在fruitsArray里面,我们新建一个橘子对象。这句代码看起来也没什么问题。但是运行一下呢?报错了,最后一行报了ArrayStoreException。关于数组的这三行代码,每一行单单拎出来其实都没什么问题,但是这三行代码组合起来就有问题了。这三行代码一起其实是在往一个Apple的数组里面塞一个Orange对象。要知道数组可是Java正经一开始就支持的功能,其中存储了它在声明的时候,指定要存储的类型信息。所以在arrayFruits往其中塞Orange的时候会报错。
类比到上面的List中,现在知道为什么List不让将一个Apple的List赋值给一个Fruit的List了吧,因为它怕你往里面放橘子。并且比数组更糟糕的是,由于类型擦除的原因,它根本就不知道它里面应该放点什么。用数组的时候,往arrayApples里面放个Orange,系统还会报个异常出来。如果是往apple的List里面放个orange,系统可是不会报异常的哦。所以为了杜绝这种情况的发生。泛型干脆就不让这样相互赋值。
但是呢,上面说的 一堆苹果不也就是一堆水果嘛,将其赋于一个更大的概念没什么错这句话是有道理的啊。有些情况下就是要将一个小的概念赋值给一个大的概念啊。不要慌,Java考虑到了这种情况。这也就是这一小节要说的上界通配符的用法,还是刚才的那个例子,这次我们换个方法来赋值,如下:

public class WildCards {
    public static void main(String[] args) {
        List<Apple> apples = new ArrayList<Apple>();
        // List<Fruit> fruits=apples;
        List<? extends Fruit> fruits = apples;
        // fruits.add(new Apple());
        // fruits.add(new Orange());
        Fruit fruit = fruits.get(0);
        fruits.contains(new Apple());
        fruits.indexOf(new Orange());

        Apple[] arrayApples = new Apple[10];
        Fruit[] arrayFruits = arrayApples;
        arrayFruits[0] = new Orange();
    }
}

我们使用List<? extends Fruit> fruits = apples; 这句来将apple的List赋值给fruit的List。但是接下来的事情出乎了我们的意料,fruits里面不能add东西了,任何东西都不能往里面放了,编译器会报错。为啥呢?联想一下上面数组的例子你心里还能没点数么。刚刚就是因为往apples里面放了不该放的东西导致错误。这次List能再这种赋值的情况下,阻止往fruits里面加东西也纯属正常,因为你根本就不知道当初付给fruits的List具体是什么,也许是apples,也许是oranges,还也许是bananas呢。既然运行的时候Java已经不知道你往里面放什么是安全的了,拿干脆就不让你往里面add了。那是fruits所有的方法都不能调了嘛?并不是,get,contains,indexOf三个方法调起来确实没什么问题啊。那是编译期能检测到我们那个方法是要修改存储内容的?然后把这部分方法屏蔽掉了不让用?其实并不是,我们来看下ArrayList的源码是怎么写着四个方法的:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    public E get(int index) {
        rangeCheck(index);
        checkForComodification();
        return ArrayList.this.elementData(offset + index);
    }
    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

}

可以看到,其实除了add的参数是E参数类型,其他三个方法的参数都是Object及以下的类型。关键就在于这里。当一个泛型的参数类型被声明为? extends Fruit以后,他对应所有使用到参数类型的方法的参数也都会变成? extends Fruit,这个时候方法也表示很无奈啊,你只说了是Fruit的一个子类型,我怎么知道具体应该是哪个类型啊?所以就直接会编译失败。从这里我们可以得出来一个结论。如果不想让声明为上界通配符的类执行的方法,大可以将其参数设置成为参数类型E,但是如果想被执行的方法,参数类型就要是Object类型的了
但是,有一点啊!如果泛型被声明为? extends Fruit以后,是不是说明其持有的对象的上界就是Fruit,因此泛型类的方法的返回值是类型参数的,返回值一律按照上边界Fruit来。这也就是List的get方法能返回一个Fruit的原因。

下界通配符

这个通配符的功能也是用来接受一个类的,举个栗子吧

class Fruit {
    public String isWhat() {
        return "Fruit";
    }
}

class Apple extends Fruit {
    @Override
    public String isWhat() {
        return "Apple";
    }
}

class Orange extends Fruit {
    @Override
    public String isWhat() {
        return "Orange";
    }
}

class Fuji extends Apple {
    @Override
    public String isWhat() {
        return "Fuji";
    }
}

public class WildCards {
    public static void main(String[] args) {
        List<Fruit> fruits = new ArrayList<Fruit>();
        List<? super Apple> appleSupers=fruits;
        appleSupers.add(new Fuji());
        Object object = appleSupers.get(0);
    }
}

还是一开始的那些类,加了个Apple的子类Fuji(红富士)。这次我们声明了一个fruit的List,List<? super Apple> appleSupers=fruits; 这句话将其赋给了appleSupers,appleSupers表示的就是一个下界通配符,它表示:我正持有的类是Apple的某个父类,具体是哪个我不太清楚。然后我们往appleSupers里面添加元素Fuji,结果证明是可以添加成功的,为什么呢?因为add的参数是? super Apple ,这能说明什么?说明其现在正在持有的对象最少是Apple类型的,那么我现在往其中添加Apple或者是Apple的子类时不会破坏List中持有对象的原则的。接着我们往外取数据,啊哦!又出事了,我们从List里面拿出来的东西变成了Object类型的啦。为啥?因为我们刚开了get的返回值是E类型的,对应到appleSupers对象上,就是? super Apple类型的。具体类型它也不太清楚,反正就是大于Apple的类型。那Java就只能把他当其左边界Object来处理啦!

无界通配符

无界通配符集合了上界下界通配符的优点,同时他们的缺点也几种到了一起。不论什么样参数类型的泛型类都可以付给带有无界通配符的泛型类(上下界通配符的优点)。但是!赋值就赋值了,你可千万别想再通过它调用泛型类中参数值是泛型参数的方法(上界通配符的缺点),也别想从它这里拿到某个具体类型了,所有以参数类型为返回值的方法都将返回Object(下界通配符的缺点)。下面还是以一个例子为例:

public class WildCards {
    public static void main(String[] args) {
        List<Fruit> fruits = new ArrayList<Fruit>();
        List<?> appleSupers=fruits;
        //appleSupers.add(new Fuji());
        Object object = appleSupers.get(0);
    }
}

可以看到,例子证明了以上阐释。

无界通配符与RawType的区别与联系

无界通配符指的就是像List<?> 这样的,RawType指的是List,不指定泛型类型这样的。他们最终其实持有的都是Object类型的对象。 这是他们的联系,但是还是有一些区别的,以TIJ中的一个例子来详细阐述。

class Holder<T> {
    private T value;

    public Holder() {
    }

    public Holder(T val) {
        value = val;
    }

    public void set(T val) {
        value = val;
    }

    public T get() {
        return value;
    }

    public boolean equals(Object obj) {
        return value.equals(obj);
    }
}

public class WildCarsInAll {
}

class Wildcards {
    // Raw argument:
    static void rawArgs(Holder holder, Object arg) {
        // holder.set(arg); // Warning:
        // Unchecked call to set(T) as a
        // member of the raw type Holder
        // holder.set(new Wildcards()); // Same warning
        // Can’t do this; don’t have any ‘T’:
        // T t = holder.get();
        // OK, but type information has been lost:
        Object obj = holder.get();
    }

    // Similar to rawArgs(), but errors instead of warnings:
    static void unboundedArg(Holder<?> holder, Object arg) {
        // holder.set(arg); // Error:
        // set(capture of ?) in Holder<capture of ?>
        // cannot be applied to (Object)
        // holder.set(new Wildcards()); // Same error
        // Can’t do this; don’t have any ‘T’:
        // T t = holder.get();
        // OK, but type information has been lost:
        Object obj = holder.get();
    }

    static <T> T exact1(Holder<T> holder) {
        T t = holder.get();
        return t;
    }

    static <T> T exact2(Holder<T> holder, T arg) {
        holder.set(arg);
        T t = holder.get();
        return t;
    }

    static <T> T wildSubtype(Holder<? extends T> holder, T arg) {
        // holder.set(arg); // Error:
        // set(capture of ? extends T) in
        // Holder<capture of ? extends T>
        // cannot be applied to (T)
        T t = holder.get();
        return t;
    }

    static <T> void wildSupertype(Holder<? super T> holder, T arg) {
        holder.set(arg);
        // T t = holder.get(); // Error:
        // Incompatible types: found Object, required T
        // OK, but type information has been lost:
        Object obj = holder.get();
    }

    public static void main(String[] args) {
        Holder raw = new Holder<Long>();
        // Or:
        raw = new Holder();
        Holder<Long> qualified = new Holder<Long>();
        Holder<?> unbounded = new Holder<Long>();
        Holder<? extends Long> bounded = new Holder<Long>();
        Long lng = 1L;
        rawArgs(raw, lng);
        rawArgs(qualified, lng);
        rawArgs(unbounded, lng);
        rawArgs(bounded, lng);
        unboundedArg(raw, lng);
        unboundedArg(qualified, lng);
        unboundedArg(unbounded, lng);
        unboundedArg(bounded, lng);
        // Object r1 = exact1(raw); // Warnings:
        // Unchecked conversion from Holder to Holder<T>
        // Unchecked method invocation: exact1(Holder<T>)
        // is applied to (Holder)
        Long r2 = exact1(qualified);
        Object r3 = exact1(unbounded); // Must return Object
        Long r4 = exact1(bounded);
        // Long r5 = exact2(raw, lng); // Warnings:
        // Unchecked conversion from Holder to Holder<Long>
        // Unchecked method invocation: exact2(Holder<T>,T)
        // is applied to (Holder,Long)
        Long r6 = exact2(qualified, lng);
        // Long r7 = exact2(unbounded, lng); // Error:
        // exact2(Holder<T>,T) cannot be applied to
        // (Holder<capture of ?>,Long)
        // Long r8 = exact2(bounded, lng); // Error:
        // exact2(Holder<T>,T) cannot be applied
        // to (Holder<capture of ? extends Long>,Long)
        // Long r9 = wildSubtype(raw, lng); // Warnings:
        // Unchecked conversion from Holder
        // to Holder<? extends Long>
        // Unchecked method invocation:
        // wildSubtype(Holder<? extends T>,T) is
        // applied to (Holder,Long)
        Long r10 = wildSubtype(qualified, lng);
        // OK, but can only return Object:
        Object r11 = wildSubtype(unbounded, lng);
        Long r12 = wildSubtype(bounded, lng);
        // wildSupertype(raw, lng); // Warnings:
        // Unchecked conversion from Holder
        // to Holder<? super Long>
        // Unchecked method invocation:
        // wildSupertype(Holder<? super T>,T)
        // is applied to (Holder,Long)
        wildSupertype(qualified, lng);
        // wildSupertype(unbounded, lng); // Error:
        // wildSupertype(Holder<? super T>,T) cannot be
        // applied to (Holder<capture of ?>,Long)
        // wildSupertype(bounded, lng); // Error:
        // wildSupertype(Holder<? super T>,T) cannot be
        // applied to (Holder<capture of ? extends Long>,Long)
    }
}

从上面的例子我们至少可以得出两个结论:
1. RawType表示,我可以持有任意对象。而无界限定类型则表示,我持有的对象是一旦确定了(赋值了)就是固定的,只是现在还没有确定。
2. RawType表示,你随便调我方法,我认怂算我输!但是我可能还是会对于你不检查类型哔哔你两句(warning)。而无界限定类型则表示:你尽管调我方法,如果我能让你调通我以类型参数为参数的方法的话,算我输!如果你调了我以类型参数为返回值的方法,而我返回的不是Object也算我输。

应用场景

带有通配符的限定类型一般用于方法的参数或者类的域上。这些方法或类一般不知道自己具体要处理那种类型的变量,只知道要处理数据的上界或者下界,亦或者上下界都不知道(无界)。并且在参数或者域一旦被指定之后,想要调用其方法就要遵循以上所说的那些原则了!

类型擦除机制

类型擦除机制的原因

文章一开始就说了,Java一开始并没有泛型的概念,是在SE5中才引入的。那么就存在一个问题,应该如何向用户透明的引入泛型的概念呢?换句话说,如何让使用了泛型客户端代码(SE5以后的代码)和没有使用过泛型的代码(SE5以前的代码)相互调用不出问题呢(前后兼容)?这是个很大的问题。
理想情况下,我们可能期望能有单独的一天,让我们来对所有的代码升级,让其都支持泛型。但现实是,即使从SE5出现的那一刻起,大家都只写支持泛型的程序,但依然会有程序需要用到SE5之前那些不支持泛型的库。而那些库的作者可能根本就不想对其库进行升级。
所以Java的泛型不但要支持向前兼容-已经存在的代码在SE5之后依然是合法的代码,并且其表示的意义不变。同时还要支持移植兼容性-即保证现有的库(不支持泛型的)如果想要迁移到支持泛型的库的时候,只有他自己需要修改,不至于影响到其使用方。原文如下:

The core motivation for erasure is that it allows generified clients to be used with non-generified libraries, and vice versa. This is often called migration compatibility. In the ideal world, we would have had a single day when everything was generified at once. In reality, even if programmers are only writing generic code, they will have to deal with non-generic libraries that were written before Java SE5. The authors of those libraries may never have the incentive to generify their code, or they may just take their time in getting to it.
So Java generics not only must support backwards compatibility—existing code and class files are still legal, and continue to mean what they meant beforebut also must support migration compatibility, so that libraries can become generic at their own pace, and when a library does become generic, it doesn’t break code and applications that depend upon it. After deciding that this was the goal, the Java designers and the various groups working on the problem decided that erasure was the only feasible solution. Erasure enables this migration towards generics by allowing non-generic code to coexist with generic code.

基于以上的问题,Java引入了类型擦除机制来实现泛型。因为类型擦除机制可以解决以上说的兼容性问题。

什么是类型擦除机制

实现泛型机制一般有两种方法。其中之一以C++中的模板(Code Specialization)方法为代表,C++编译器会为每一个泛型类实例生成一份执行代码。执行代码中integer list和string list是两种不同的类型。这样会导致代码膨胀(code bloat)。但是好处是,这种泛型相对于Java中的伪泛型来说是“真泛型”,其类型参数信息可以保留到运行时。另外一种便以Java类型擦除机制为代表。类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上(Code Sharing)。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。
类型擦除过程可以理解为将泛型类编程普通的Java代码的过程。一般包括两个步骤:
1. 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
2. 移除所有的类型参数。
煮个栗子,还以刚才的MyClass为例。

public class MyClass<T extends TestB> {
    private T object;

    public void setObject(T object) {
        this.object = object;
    }

    public T getObject() {
        return object;
    }

    public void action() {
        object.bTalk();
    }

    public static void main(String[] args) {
        //MyClass<TestA> myClass = new MyClass<TestA>();
        // myClass.setObject(new TestA());
        // TestA testA = myClass.getObject();
        // System.out.println(testA.aTalk());
        // myClass.setObject(new TestB()); //编译错误 TestB testB=(TestB)myClass.getObject();
        // System.out.println(testB.bTalk());
        MyClass<TestC> testBMyClass = new MyClass<TestC>();
        testBMyClass.setObject(new TestC());
        TestC testC = testBMyClass.getObject();
        System.out.println(testC.cTalk());
    }
}

在类型擦除之后,你可以理解它变成了这样

public class MyClass {
    private TestB object;

    public void setObject(TestB object) {
        this.object = object;
    }

    public TestB getObject() {
        return object;
    }

    public void action() {
        object.bTalk();
    }

    public static void main(String[] args) {
        // MyClass<TestA> myClass = new MyClass<TestA>();
        // myClass.setObject(new TestA());
        // TestA testA = myClass.getObject();
        // System.out.println(testA.aTalk());
        // myClass.setObject(new TestB()); //编译错误 TestB testB=(TestB)myClass.getObject();
        // System.out.println(testB.bTalk());
        MyClass testBMyClass = new MyClass();
        testBMyClass.setObject(new TestC());
        TestC testC = testBMyClass.getObject();
        System.out.println(testC.cTalk());
    }
}

即将声明中的类型参数T都以其左边界TestB来做了替换,如果没有extends这样的限定,则以Object替换。
但是仔细看这行代码TestC testC = testBMyClass.getObject(); 的话,我们可以发现,类型擦除后,getObject方法返回的是TestB类型,到这里,为什么就能更加具体化的转到TestB的子类TestC呢?其实是编译器对于上面的代码做了点什么。使用javap -c Myclass.class 文件去看MyClass类的字节码如下:

Compiled from "MyClass.java"
public class com.person.yecheng.li.blogcases.MyClass<T extends com.person.yecheng.li.blogcases.TestB> {
  public com.person.yecheng.li.blogcases.MyClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void setObject(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field object:Lcom/person/yecheng/li/blogcases/TestB;
       5: return

  public T getObject();
    Code:
       0: aload_0
       1: getfield      #2                  // Field object:Lcom/person/yecheng/li/blogcases/TestB;
       4: areturn

  public void action();
    Code:
       0: aload_0
       1: getfield      #2                  // Field object:Lcom/person/yecheng/li/blogcases/TestB;
       4: invokevirtual #3                  // Method com/person/yecheng/li/blogcases/TestB.bTalk:()Ljava/lang/String;
       7: pop
       8: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #4                  // class com/person/yecheng/li/blogcases/MyClass
       3: dup
       4: invokespecial #5                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: new           #6                  // class com/person/yecheng/li/blogcases/TestC
      12: dup
      13: invokespecial #7                  // Method com/person/yecheng/li/blogcases/TestC."<init>":()V
      16: invokevirtual #8                  // Method setObject:(Lcom/person/yecheng/li/blogcases/TestB;)V
      19: aload_1
      20: invokevirtual #9                  // Method getObject:()Lcom/person/yecheng/li/blogcases/TestB;
      23: checkcast     #6                  // class com/person/yecheng/li/blogcases/TestC
      26: astore_2
      27: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
      30: aload_2
      31: invokevirtual #11                 // Method com/person/yecheng/li/blogcases/TestC.cTalk:()Ljava/lang/String;
      34: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      37: return
}

可以仔细看下public static void main中的标有20、23、26的那三行。尤其是第23行,那是什么?编译器自动帮我们加上的类型强转的字节码!!由此可见,编译器由我们的代码推知了我们需要进行强转。因此在编译成字节码的时候偷偷摸摸的帮我们加上了类型转换的代码。以免我们手动加出错。

类型擦除机制带来的问题

上面也说了类型擦除机制的实现原理-类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上(Code Sharing),在运行期间类型参数信息丢失。就单单是是只有一份字节码这个事就会出很多匪夷所思的问题:

只有一份字节码带来的问题
  1. 不能用同一个泛型类的实例区分方法签名
public class Erasure{  

            public void test(List<String> ls){  
                System.out.println("Sting");  
            }  
            public void test(List<Integer> li){  
                System.out.println("Integer");  
            }  
    }

这样是不行的,因为List[String]和List[Integer]在类型擦除之后都会变成List,那两个test方法签名就一毛一样了,编译器直接报错了。
2. 不能同时catch同一个泛型异常类的多个实例
3. 泛型类的静态变量类型不能带有类型参数,因为所有的泛型实例共用一份代码,所以静态变量如果是有泛型参数的话,编译期究竟应该把它强转成那个泛型实例的类型呢?
4. 不能隔离泛型实例之间对泛型类静态变量的使用。就是说如果List类里面有个静态变量的话,那么List[String]泛型实例和List[Integer]泛型实例对其的改变是相互可见的。

运行时类型缺失带来的问题

首先我们得知道在泛型类中,对于new T这种操作是肯定不能成功的。因为运行时T的类型信息都已经没有了。那么是不是说,在类型擦除了以后,我们根本不能去在泛型类中创建某一个类的实例?
其实不然,不过需要额外的信息。常见的方法就是传入类的class信息

class ClassAsFactory<T>{

T x;

public ClassAsFactory(Class<T> kind){

    try{

       x=kind.newInstance();
    }catch(Exception e){
       throw new RuntimeExeption(e);
    }
 }


}
public class Employee {
}

public class InstantiateGenericType {
    public static void main(String[] args){
        ClassAsFactory<Employee> fe=new ClassAsFactory<Employee>(Employee.class);
        System.out.println("ClassAsFactory<Employee> succeed!");
        try{
            ClassAsFactory<Integer> fi=new ClassAsFactory<Integer>(Integer.class);
        }catch (Exception e){
            System.out.println("ClassAsFactory<Integer> failed");
        }
    }

}

这种方法的一个缺点是,类型参数必须要有无参构造函数。下面是一种使用工厂方法的方案:

interface  Factory<T>{
    T creat();
}
class  Foo2<T>{
    private T x;
    public <F extends Factory<T>> Foo2(F factory){
        x=factory.creat();
    }
}
class IntegerFactory implements Factory<Integer>{

    @Override
    public Integer creat() {
        return new Integer(0);
    }
}
class Widget{
    public static class FactoryI implements Factory<Widget>{

        @Override
        public Widget creat() {
            return new Widget();
        }
    }
}
public class FactoryConstraint {
    public static void main(String... args){
        new Foo2<Integer>(new IntegerFactory());

        new Foo2<Widget>(new Widget.FactoryI());
    }
}

这种方式在比较安全完善的同时,还可以保证编译期检查的特点。但是他其实限制了类必须要以某种方式实现Factory接口。最后一种是使用模板方法的方式。

abstract class GenericWithCreate<T>{
    final T element;
    GenericWithCreate(){
        element=creat();
    }
    abstract T creat();

}
class X{

}
class Creator extends GenericWithCreate<X>{

    @Override
    X creat() {
        return new X();
    }
    void f(){
        System.out.println(element.getClass().getSimpleName());
    }
}
public class CreatorGeneric {
    public static void main(String... args){
        Creator c=new Creator();
        c.f();
        /*ArrayList<String> strings=new ArrayList<String>();
        strings.add("haha");
        strings.add("hehe");
        String[] strings1=strings.toArray();*/
    }

}

这种方法几乎没有什么明显的缺点。

如何在泛型类中使用数组

首先应该注意的是,能不在泛型内使用数组,千万不要使用数组!如果实在要使用数组,请千万要传入Class参数。或者用类似以上创建具体对象的方法。
正确的使用方式:

public class GenericArrayWithTypeToken<T> {
    private T[] array;
    @SuppressWarnings("Unchecked")
    public GenericArrayWithTypeToken(Class<T> type,int sz){
        array= (T[])Array.newInstance(type,sz);
    }
    public void put(int index,T item){
        array[index]=item;

    }
    public T get(int index){
        return array[index];
    }
    public T[] rep(){
        return array;
    }
    public static void main(String... args){
        GenericArrayWithTypeToken<Integer> gai=new GenericArrayWithTypeToken<Integer>(Integer.class,10);
        Integer[] ia=gai.rep();
    }
}

但是你如果看ArraList的源码的话,他并不是这么使用的。大家可以试一下ArrayList的toArray方法吧。他返回的其实是一个Object数组。但是我如果是一个ArrayList[Apple]类型的list,调用其toArray返回的是一个Object[]类型的数组是不是就有点太说不过去了。这就是使用Object[]数组的弊端。

java泛型的小结

Java的泛型是由类型擦除机制实现的,其实它就只工作在程序的编译期间,保证编译期间类型检查正确,以及再生成字节码的时候自动加上类型转换的逻辑。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值