Java泛型详解

2.6 Java泛型详解

Java泛型是JDK5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter),声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK5中的新集合类框架中。

2.6.1 类型擦除

首先我们看一下Java泛型中的类型擦除:在生成的Java字节码中是不包含泛型的类型信息的,使用泛型的时候加上的类型参数在编译的时候会被编译器去掉,这个过程就称为类型擦除。如在代码中定义的List<Object>和List<String>等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的(反射是可见的,具体可参见2.3中笔记),这一点和C++模板机制实现泛型有很大区别。

很多泛型的奇怪特性都与类型擦除有关,比如:

  1. 泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或者List<Integer>.class,而只有List.class。
  2. 静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。
  3. 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM无法区分两个异常类型MyException<String>和 MyException<Integer>的,对于JVM来说,它们都是MyException类型的,也就无法执行与异常对应的catch语句。

类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类,一般是Object,如果指定了类型参数的上界的话,则是这个上界。然后把代码中的类型参数都替换成该类。同时去掉出现的类型声明,即去掉<>的内容。比如T get()方法声明就变成了Object get();List<String>就变成了List。接下来就可能需要生成一些桥接方法(bridge method),这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:

class MyString implements Comparable<String> {
    @Override
    public int compareTo(String str) {
        return 0;
    }
}

当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,类MyString就会有编译错误,因为没有实现接口Comparable声明的int compareTo(Object)方法,这个时候就由编译器来动态生成这个方法。

2.6.2 通配符与上下界

在使用泛型类的时候,既可以指定一个具体的类型,也可以用通配符?来表示未知类型,如List<?>就声明了List中包含的元素类型是未知的。通配符所代表的其实是一组类型,但具体的类型是未知的。通配符分为三类:无界通配符、上界通配符和下界通配符。通配符本身比较复杂,我们会以集合为代表,以元素的添加和获取为例简要说明其用法。

无界通配符

“?”表示无界通配符,List<?>表示:List中存储的元素的类型是未知的。

  1. 添加元素,使用无界通配符时,由于类型不确定的(可以是任何类型),不可以向List<?>添加任何元素(除了null),因为如果它包含了String的话,往里面添加Integer显然是错误的。那为什么不能添加Object引用,是因为Object引用可以指向子类实例,编译期是无法获知其具体类型,所以为了类型安全,其不可以添加任何元素(除了null)。
  2. 获取元素,List<?>中的元素只可以使用Object来引用,因为其元素肯定是Object及其子类引用。

事实上无界通配符通常会用在以下两种情况:

  1. 在业务逻辑与泛型类型无关,如List.size和List.clean等。实际上,最常用的就是Class<?>,因为Class<T>并没有依赖于T。
  2. 当方法参数是原始的Object类型,如下:
public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + "");
}

//使用泛型类替换
public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + "");
}

这样就可以兼容更多的输出,而不单纯是List<Object>,如下:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

上界通配符

“? extends Animal”表示通配符的上界是Animal,即“? extends Animal”可以代表Animal及其子类,不能代表Animal父类。
首先要阐明一点,上界通配符和下界通配符更多的是为了解决泛型不协变的问题。

  1. 添加元素,使用上界通配符时,其类型仍然是不确定的(会是某个类型及其子类型),所以仍然不可以向List<? extends Animal>添加任何元素(null除外),因为如果它包含了Cat的话,往里面添加Dog显然是错误的,同样不可以添加Object引用。
  2. 获取元素,List<? extends Animal>中的元素可以用Animal来引用的,因为其元素肯定是Animal及其子类引用。

而且引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法,因为其中元素肯定是Animal类或其子类成员引用。

下界通配符

“? super Animal”表示通配符的下界是Animal,即“? super Animal”可以代表Animal及其父类,不能代表Animal子类。

  1. 添加元素,使用下界通配符时,虽然类型仍然是不确定的(会是某个类型及其父类),但是此时可以向List<? super Animal>添加Animal或者其子类型Dog元素,因为如果它包含了Animal的话,往里面添加Dog显然是可以的,子类型可以替换父类型。
  2. 获取元素,List<? super Animal>中的元素只可以用Object来引用的,“? super Animal”可以代表Animal及其父类,所以只能通过Object来进行应用。

总结

PECS(Producer Extends Consumer Super)原则:频繁往外读取内容的,适合用上界Extends;经常往里插入的,适合用下界Super。

通配符是实参,通配符这些看起来很奇怪的特性原因在于:编译器要保证类型安全,Object类型是所有类型的祖宗类型,而父类引用是可以引用子类对象的。

2.6.3 类型系统

在Java中,大家比较熟悉的是通过继承机制而产生的类型体系结构,比如String继承自Object。根据Liskov替换原则,子类是可以替换父类的。当需要Object类的引用的时候,如果传入一个String对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用的时候,就需要进行强制类型转换。这种自动的子类替换父类的类型转换机制,对于数组也是适用的(前面说过数组是协变的),String[]可以替换Object[]。

而泛型的引入,对于这个类型系统产生了一定的影响,正如前面提到的List<String>是不能替换掉List<Object>的。引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口的继承体系结构。前者是指对于List<String>和List<Object>这样的情况,类型参数String是继承自Object的。而后者是指List接口继承自Collection接口。对于这个类型系统,有如下的一些规则:

  1. 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构,即List<String>是Collection<String>的子类型,List<String>可以替换Collection<String>,这种情况也适用于带有上下界的类型声明。
  2. 当泛型类的类型声明中使用了通配符的时候,其子类型可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开,即Collection<Double>和Collection<Integer>等。如此循环下去,ArrayList<Long>和HashSet<Double>等也都算是Collection<? extends Number>的子类型。
  3. 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。

2.6.4 开发自己的泛型类

泛型类与一般的Java类基本相同,只是在类和接口定义上多出来了用<>声明的类型参数。一个类可以有多个类型参数,如MyClass<X, Y, Z>。每个类型参数在声明的时候可以指定上界。所声明的类型参数在Java类中可以像一般的类型一样作为方法的参数和返回值,或是作为域和局部变量的类型。但是由于类型擦除机制,类型参
数并不能用来创建对象或是作为静态变量的类型。考虑下面的泛型类中的正确和错误的用法。

class ClassTest<X extends Number, Y, Z> {
    private X x; //正确用法
    private static Y y; //编译错误,不能用在静态变量中
    public X getFirst() { //正确用法
        return x;
    }
    public void wrong() {
        Z z = new Z(); //编译错误,不能创建对象
    }
}

2.6.5 最佳实践

在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。

  1. 在代码中避免泛型类和原始类型的混用。比如List<String>和List不应该共同使用,这样会产生一些编译器警告和潜在的运行时异常。
  2. 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
  3. 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此当需要类似数组的功能时候,使用集合类即可。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容

  • 一、泛型简介 1.引入泛型的目的 了解引入泛型的动机,就先从语法糖开始了解。 语法糖 语法糖(Syntactic ...
    Ruheng阅读 4,153评论 2 50
  • 泛型 泛型由来 泛型字面意思不知道是什么类型,但又好像什么类型都是。看前面用到的集合都有泛型的影子。 以Array...
    向日花开阅读 2,167评论 2 6
  • 一、定义 Java泛型是JDK5中引入的新特性,提供在编译时类型安全监测机制,本质是参数化类型(即所操作的数据类型...
    MrTrying阅读 1,151评论 0 2
  • 一、引入泛型机制的原因 假如我们想要实现一个String数组,并且要求它可以动态改变大小,这时我们都会想到用Arr...
    Q南南南Q阅读 512评论 0 1
  • 一不小心,中国的新年都过完了,而今天也迎来了第一个上班的日子,虽然,地铁上人并不多... 在看艾力的《你一年的87...
    冰淇淋在路上阅读 816评论 6 5