Java基础知识总结(七)——泛型

更新:2016/01/13,增加了几个编译器插入checkcast支持泛型使用的例子;

心得:
我觉得Java泛型的意义在于在我们使用父类引用操作子类时(擦除),让编译器和JVM来代替我们进行必要的类型检查和转换(checkcast指令,桥方法,signature这些都是编译器和JVM提供的额外帮助)。

问题的关键在于签名实在调用者的方法表属性中的;而描述符是在自己类型信息中的。

对象本身不知道自己的泛型参数具体是什么。

1. 泛型方法

泛型方法可以定义在泛型类或者普通类中。

定义:

public static <T> T getT(T...t) {
    return t[0];
}

泛型方法与编译器类型推断:
尤其和可变参数列表结合使用,对于编译器的类型判断稍有一些问题,方法getT有两个条件可以用来推断类型:返回值参数类型
比如:

String str = getT("x", 123.0, 1);
因此需要了解以下事实:

(1)传入泛型方法实参必须是对象,因此基本类型会自动装箱(这里是String,Double,Integer);
(2)编译器会分析出所有参数的共同基类(这里是Comparable,Serializable,Object);
(3)如果返回是void,传入不同类型的参数给可变参数列表方法不会有编译问题;如果返回类型为泛型参数(T),会根据使用的情况而定;
(4)如果使用了<String>限定:

String str = GenericShow.<String>getT("xx", "123");

那么参数的参数只能为String;
如果省略了限定,根据参数共同基类变量类型决定,如果变量类型属于共同基类则编译没有问题,否则将会抛出异常;

综合来看,编译器是在检查所有<T>的实现能否找到一个合理的类型统一解释。

PS:实际使用的时候显然应该避免这么复杂的情况,设计合理的类型和方法才是正道。

2. 类型变量的限定与“擦除”

Java的泛型在编译时使用了“擦除”,被替换成限定类型;

(1)对于<T>的方法,描述符是:
descriptor: ([Ljava/lang/Object;)Ljava/lang/Object;

因此,我们使用T类型的变量时只能直接使用Object拥有的方法/属性,实际上<T><T extends Object>

(2)多个限定:Object类是所有类型的超类,可以通过限定符来“缩小范围”:

(1)<T extends Comparable>
(2)<T extends Comparable && Seriablizable && List>
(3)限定中如果有类,只能有一个且必须是第一个;
它们的描述符是:

descriptor: (Ljava/lang/Comparable;)V

因此可以使用Comparable的方法;

擦除与替换类型

将会使用第一个限定类型进行替换,因此在调用其他限定类型的方法时,需要先进行checkcast指令的检查和转型(虽然在编译和加载时会根据signature进行检查,这一步还是需要的,这是语言设计上的问题);
在调用者方法中这种强制类型转换在方法返回值,域访问赋值中(如果擦除替换的类型和实际泛型参数类型不匹配,比如List<String>变量类型中参数类型是String,但List类中被擦除成了Object,在进行赋值时就要插入checkcast指令)都会出现;
例子1

    String s = list.get(0);

对应的字节码为:

        17: aload_1
        18: iconst_0
        19: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        24: checkcast     #7                  // class java/lang/String
        27: astore_2

PS:list.get(0);对于JVM来说需要传入2个参数,一个是list对象引用,一个是索引,所以我们看到invokeinterface前将它们加载到了操作数栈,并且invokeinterface指令后的count为2(表示取操作数栈顶2个元素);

注意这里checkcast指令,就是编译器为我们自动添加的;

例子2:多个限定的时候,调用除了第一个限定的方法需要有checkcast指令

    private static <T extends List & Serializable & Comparable<? super T>> void comp2(T t1, T t2) {
        t1.get(1);
        t1.compareTo(t2);
    }

编译后:

         8: aload_0
         9: checkcast     #8                  // class java/lang/Comparable
        12: aload_1
        13: invokeinterface #9,  2            // InterfaceMethod java/lang/Comparable.compareTo:(Ljava/lang/Object;)I
        18: pop

Comparable限定并不是第一个限定,因此编译器通过生成插入一个checkcast进行转换;
我们在看看这个方法的描述符:
““java
descriptor: (Ljava/util/List;Ljava/util/List;)V

编译器使用的是第一个限定进行擦除替换的,因此其他的限定只能通过checkcast先转换了;

**例子3**:
类定义
```java
    private static class TestA<T extends Comparable<? super T>> {
        public T get(T t) {
            return t;
        }
    }




<div class="se-preview-section-delimiter"></div>

使用:

    TestA<String> testA = new TestA<>();
    Comparable<String> c = testA.get("123");




<div class="se-preview-section-delimiter"></div>

编译后:

        30: new           #8                  // class com/jvm/showByteCode/GenericShow$TestA
        33: dup
        34: aconst_null
        35: invokespecial #9                  // Method com/jvm/showByteCode/GenericShow$TestA."<init>":(Lcom/jvm/showByteCode/GenericShow$1;)V
        38: astore        4
        40: aload         4
        42: ldc           #4                  // String 123
        44: invokevirtual #10                 // Method com/jvm/showByteCode/GenericShow$TestA.get:(Ljava/lang/Comparable;)Ljava/lang/Comparable;
        47: astore        5

这里Comparable<String> c = testA.get("123");就不会在插入checkcast指令,因为Comparable正是TestA擦除后的类型;

通过举这些例子应该可以更好的理解泛型在Java中到底是怎样工作的:擦除—>使用:根据签名+checkcast。

3. 桥方法,泛型和多态

private static class Pair<T> {
    private T first;
    private T second;
    public T getFirst() {return first;}
    public void setFirst(T first) {this.first = first;}
    public T getSecond() {return second;}
    public void setSecond(T second) {this.second = second;}
}

private static class DateInterval extends Pair<Date> {
    @Override
    public void setSecond(Date second) {
    }
}

Pair类在擦除之后,实际上是基于Object的;
在子类DateInterval覆盖了setSecond使用的Date,如果是多态也就是使用Pair引用DateInterval调用实例方法setSecond方法(invokevirtual指令)也是查找setSecond(Object)的实现,因此这里需要一个桥方法:

public void setSecond(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V

实际上在桥方法中调用的是setSecond((Date)obj);

PS:协变返回类型也是需要桥方法,因此对于虚拟机来说返回值也是区别方法的一个重要特征

4. 约束与局限性

Java的泛型是基于擦除的,故而不能向C++的模板一样灵活,擦除之后类型参数在描述符中没有了,对于数组,instanceof,getClass来说它们不能识别原有的类型参数的区别,因此如果允许数组,instanceof等结合泛型使用,不能保证正确的类型,导致ClassCastException的风险大大增加,而泛型的意义在于可以自动进行类型的检查和转换(基于signature的),因此编译器禁止了这些做法。
(1)不能用基本类型实例化类型参数;
(2)运行时类型检查只适用于原始类型(instanceof,getClass,不管泛型参数如何它们都基于同一个Class对象);
(3)不能创建参数化类型的数组(使用ArraList<Pair<String>>):
数组类型没有能力(因为擦除)检查写入泛型类型对象的类型是否与签名(类型参数)一致,因此这样的写法List<Stirng> lsa = new ArrayList<String>[10];使得new ArrayList<String>的定义毫无意义,违反这句代码的本意;

//为什么不可以
List<Stirng>[] lsa = new ArrayList<String>[10]; //假设可以
Object[] oa = (Object[])lsa;
List<Integer> li = new ArrayLIst<Integer>();
li.add(new Integer(3));
oa[1] = li; //可以通过
String s = lsa[1].get(0); //ClassCastException

//这两种是可以的,因为你你这样写编译器认为你
//主动放弃了编译器和JVM提供的类型检查和转换,或者说,你本来就希望放入O比较恶臭它
static List<String>[] ls = new ArrayList[10];
static List<?>[] ls1 = new ArrayList<?>[10];

(4)带泛型参数的类和可变参数列表,可变参数列表本质是数组实现的,这里是可行的,使用@SafeVarargs可以消除警告,但这样做,数组只会检查擦除之后的类型,因此实际使用有ClassCastException的风险;
(5)不能实例化类型变量(要想结合泛型创建实例或者数组只能通过反射创建);
(6)泛型类的静态上下文中类型变量无效;
(7)不能抛出或捕获泛型类的实例:
class Problem<T> extends Throwablecatch(T e)是不允许的,因为异常表需要具体的类型描述;
throw T是允许的,擦除之后实际上声明的是

Exceptions:
  throws java.lang.Throwable

利用throw T可以消除对checked异常的检查:

  public static void main(String[] args) {
    GenericLimit.<RuntimeException>doWork();
}

通过这个方法我们可以在实现类似Runnable接口run方法没有异常声明的接口方法中,以RuntimeException的方式抛出,不过我还是觉得这个没什么意义,通过throw new RuntimeException(t);的形式一样可以,或者使用Callable来代替Runnable;
(8)擦除后的冲突:

public boolean equals(T t) {...}

这样的方法在擦除后会和equals(Object)冲突;

不同同时实现/继承两个带有不同类型参数的同一类型接口,比如

private abstract static class P implements Comparable<String> {
}
private static class PA extends P implements Comparable<Integer>

因为继承/实现带有类型参数的类/接口,会生成对应的桥方法,如果同时实现带不同类型惨数的两个同类型接口,必须要生成各自的桥方法,而桥方法的方法签名是完全相同的(包括返回值类型);

5. 泛型类型的继承规则

数组和泛型的区别:
(1)数组对象的描述符中是带有原始类型是啥的,因此它可以检查赋值的类型是否正确(错误抛出ArrayStoreException),同时数组SubClass[]和SuperClass[]是有继承关系的;
(2)Pair<SuperClass>Pair<SubClass>之间没有关系,它们类型检查和变量的签名有关,实际上在堆中这两个类型的实例对象数据并没有什么性质上的差别,它们也同样基于同一个Class对象,因此它们是平行的,因此编译器会禁止不同类型参数的变量之间的赋值(因为泛型的本意就是让编译器和JVM来保证类型的检查和转换),它认为这种行为破坏了泛型的本意;
(3)Pair p = new Pair<SubClass>();是合法的,这意味着你放弃了编译器为你提供的额外帮助;
(4)真正具有继承关系的是形如:

List<String> s = new ArrayList<String>();

心得:问题的关键在于签名实在调用者的方法表属性中的;而描述符是在自己类型信息中的;

6. 通配符类型:

我觉得通配符的运用可以参考离散数学中的集合,根据限定和extends(+),super(-)来确定范围;
范围大小(== 表示平行关系):

Pair > Pair<?> > Pair<? super SubClass> > Pair<SubClass> == Pair<Object> == Pair<SuperClass>
Pair > Pair<?> > Pair<? extends SuperClass> > Pair<SuperClass> == Pair<SubClass>

(1)<? extends SuperClass>:包含SuperClass自身,对应的签名是Lcom/yjh/generic/GenericAndWildcard$Pair<+Lcom/yjh/generic/GenericAndWildcard$SuperClass;>;,带有子类限定的通配符可以用于从泛型对象中读取;
(2)<? super SubClass>:包含SubClass自身,对应的签名是Lcom/yjh/generic/GenericAndWildcard$Pair<-Lcom/yjh/generic/GenericAndWildcard$SubClass;>;,带有超类限定的通配符可以用于向泛型对象中写入;
一个典型的super关键字用法:

static <T extends Comparable<? super T>> T min(T[] ts) {...}

(3)无限定通配符:签名Lcom/yjh/generic/GenericAndWildcard$Pair<*>;
(4)如果方法内需要使用泛型参数声明变量,使用泛型方法要比void swap(Pair<?> pair)要方便;
(5)通配符捕获,只有在编译器能够确信通配符表达的是单个、确定的类型时才可以。ArrayList<Pair<T>>不能捕获ArrayList<Pair<?>>中的通配符;

附:C++中template和Java的泛型

Java的泛型似乎是一种模拟的方式,本质还是基于向上转型(子类的引用可以赋值给父类引用);
而C++的Template是静态机制,编译就进行类型替换,而java的想要替换成实际参数类型是在运行通过checkcast实现的,因此java不可以new T();这样写,因为真正的类型是变量的签名中,类和对象自己并不知道。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值