java 泛型简介(转载)
原文出处: absfree
1. Why ——引入泛型机制的原因
假如我们想要实现一个String数组,并且要求它可以动态改变大小,这时我们都会想到用ArrayList来聚合String对象。然而,过了一阵,我们想要实现一个大小可以改变的Date对象数组,这时我们当然希望能够重用之前写过的那个针对String对象的ArrayList实现。
在Java 5之前,ArrayList的实现大致如下:
| 1 2 3 4 5 6 | publicclassArrayList {    publicObject get(inti) { ... }    publicvoidadd(Object o) { ... }    ...    privateObject[] elementData;} | 
从以上代码我们可以看到,用于向ArrayList中添加元素的add函数接收一个Object型的参数,从ArrayList获取指定元素的get方法也返回一个Object类型的对象,Object对象数组elementData存放这ArrayList中的对象, 也就是说,无论你向ArrayList中放入什么类型的类型,到了它的内部,都是一个Object对象。
基于继承的泛型实现会带来两个问题:第一个问题是有关get方法的,我们每次调用get方法都会返回一个Object对象,每一次都要强制类型转换为我们需要的类型,这样会显得很麻烦;第二个问题是有关add方法的,假如我们往聚合了String对象的ArrayList中加入一个File对象,编译器不会产生任何错误提示,而这不是我们想要的。
所以,从Java 5开始,ArrayList在使用时可以加上一个类型参数(type parameter),这个类型参数用来指明ArrayList中的元素类型。类型参数的引入解决了以上提到的两个问题,如以下代码所示:
| 1 2 3 4 5 | ArrayList<String> s = newArrayList<String>();s.add("abc");String s = s.get(0); //无需进行强制转换s.add(123);  //编译错误,只能向其中添加String对象... | 
在以上代码中,编译器“获知”ArrayList的类型参数String后,便会替我们完成强制类型转换以及类型检查的工作。
2. 泛型类
所谓泛型类(generic class)就是具有一个或多个类型参数的类。例如:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | publicclassPair<T, U> {    privateT first;    privateU second;    publicPair(T first, U second) {        this.first = first;        this.second = second;    }    publicT getFirst() {        returnfirst;    }    publicU getSecond() {        returnsecond;    }    publicvoidsetFirst(T newValue) {        first = newValue;    }    publicvoidsetSecond(U newValue) {        second = newValue;    }} | 
上面的代码中我们可以看到,泛型类Pair的类型参数为T、U,放在类名后的尖括号中。这里的T即Type的首字母,代表类型的意思,常用的还有E(element)、K(key)、V(value)等。当然不用这些字母指代类型参数也完全可以。
实例化泛型类的时候,我们只需要把类型参数换成具体的类型即可,比如实例化一个Pair<T, U>类我们可以这样:
| 1 | Pair<String, Integer> pair = newPair<String, Integer>(); | 
3. 泛型方法
所谓泛型方法,就是带有类型参数的方法,它既可以定义在泛型类中,也可以定义在普通类中。例如:
| 1 2 3 4 5 | publicclassArrayAlg {    publicstatic<T> T getMiddle(T[] a) {        returna[a.length / 2];    }} | 
以上代码中的getMiddle方法即为一个泛型方法,定义的格式是类型变量放在修饰符的后面、返回类型的前面。我们可以看到,以上泛型方法可以针对各种类型的数组调用,在这些数组的类型已知切有限时,虽然也可以用过重载实现,不过编码效率要低得多。调用以上泛型方法的示例代码如下:
| 1 2 | String[] strings = {"aa", "bb", "cc"};String middle = ArrayAlg.getMiddle(names); | 
4. 类型变量的限定
在有些情况下,泛型类或者泛型方法想要对自己的类型参数进一步加一些限制。比如,我们想要限定类型参数只能为某个类的子类或者只能为实现了某个接口的类。相关的语法如下:
<T extends BoundingType>(BoundingType是一个类或者接口)。其中的BoundingType可以多于1个,用“&”连接即可。
5. 深入理解泛型的实现
实际上,从虚拟机的角度看,不存在“泛型”概念。比如上面我们定义的泛型类Pair,在虚拟机看来(即编译为字节码后),它长的是这样的:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | publicclassPair {    privateObject first;    privateObject second;    publicPair(Object first, Object second) {        this.first = first;        this.second = second;    }    publicObject getFirst() {        returnfirst;    }    publicObject getSecond() {        returnsecond;    }    publicvoidsetFirst(Object newValue) {        first = newValue;    }    publicvoidsetSecond(Object newValue) {        second = newValue;    }} | 
上面的类是通过类型擦除得到的,是Pair泛型类对应的原始类型(raw type)。类型擦除就是把所有类型参数替换为BoundingType(若未加限定就替换为Object)。
我们可以简单地验证下,编译Pair.java后,键入“javap -c -s Pair”可得到:

上图中带“descriptor”的行即为相应方法的签名,比如从第四行我们可以看到Pair构造方法的两个形参经过类型擦除后均已变为了Object。
由于在虚拟机中泛型类Pair变为它的raw type,因而getFirst方法返回的是一个Object对象,而从编译器的角度看,这个方法返回的是我们实例化类时指定的类型参数的对象。实际上, 是编译器帮我们完成了强制类型转换的工作。也就是说编译器会把对Pair泛型类中getFirst方法的调用转化为两条虚拟机指令:
第一条是对raw type方法getFirst的调用,这个方法返回一个Object对象;第二条指令把返回的Object对象强制类型转换为当初我们指定的类型参数类型。
我们通过以下的代码来直观的感受下:
| 1 2 3 4 5 6 7 8 9 10 | publicclassPair<T, U> {    //请见上面贴出的代码    publicstaticvoidmain(String[] args) {        String first = "first", second = "second";        Pair<String, String> p = newPair<String, String>(first, second);        String result = p.getFirst();    }} | 
编译后我们通过javap查看下生成的字节码:

我们重点关注下上面标着”17:”的那行,根据后面的注释,我们知道这是对getFirst方法的调用,可以看到他的返回类型的确是Object。
我们再看下标着“20:”的那行,是一个checkcast指令,字面上我们就可以知道这条指令的含义是检查类型转换是否成功,再看后面的注释,我们这里确实存在一个到String的强制类型转换。
类型擦除也会发生于泛型方法中,如以下泛型方法:
| 1 | publicstatic<T extendsComparable> T min(T[] a) | 
编译后经过类型擦除会变成下面这样:
| 1 | publicstaticComparable min(Comparable[] a) | 
方法的类型擦除会带来一些问题,考虑以下的代码:
| 1 2 3 4 5 6 7 8 9 10 11 12 | publicclassDateInterval extendsPair<Date, Date> {    publicDateInterval(Date first, Date second) {        super(first, second);    }    publicvoidsetSecond(Date second) {        if(second.compareTo(getFirst()) >= 0) {            super.setSecond(second);        }    }} | 
以上代码经过类型擦除后,变为:
| 1 2 3 4 5 6 7 8 9 10 | publicclassDateInterval extendsPair {    ...    publicvoidsetSecond(Date second) {        if(second.compareTo(getFirst()) >= 0) {            super.setSecond(second);        }    }} | 
而在DateInterval类还存在一个从Pair类继承而来的setSecond的方法(经过类型擦除后)如下:
| 1 | publicvoidsetSecond(Object second) | 
现在我们可以看到,这个方法与DateInterval重写的setSecond方法具有不同的方法签名(形参不同),所以是两个不同的方法,然而这两个方法之前却是override的关系。考虑以下的代码:
| 1 2 3 4 | DateInterval interval = newDateInterval(...);Pair<Date, Date> pair = interval;Date aDate = newDate(...);pair.setSecond(aDate); | 
由以上代码可知,pair实际引用的是DateInterval对象,因此应该调用DateInterval的setSecond方法,这里的问题是类型擦除与多态发生了冲突。
我们来梳理下为什么会发生这个问题:pair在之前被声明为类型Pair<Date, Date>,该类在虚拟机看来只有一个“setSecond(Object)”方法。因此在运行时,虚拟机发现pair实际引用的是DateInterval对象后,会去调用DateInterval的“setSecond(Object)”,然而DateInterval类中却只有”setSecond(Date)”方法。
解决这个问题的方法是由编译器在DateInterval中生成一个桥方法:
| 1 2 3 | publicvoidsetSecond(Object second) {    setSecond((Date) second);} | 
我们再来通过javap来感受下:

我们可以看到,在DateInterval类中存在两个setSecond方法,第一个setSecond方法(即我们定义的setSecond方法)的形参为Date,第二个setSecond方法的形参是Object,第二个方法就是编译器为我们生成的桥方法。我们可以看到第二个方法中存在到Date的强制类型转换,而且调用了第一个setSecond方法。
综合以上,我们知道了泛型机制的实现实际上是编译器帮我们分担了一些麻烦的工作。一方面通过使用类型参数,可以告诉编译器在编译时进行类型检查;另一方面,原本需要我们做的强制类型转换的工作也由编译器为我们代劳了。
6. 注意事项
(1)不能用基本类型实例化类型参数
也就是说,以下语句是非法的:
| 1 | Pair<int, int> pair = newPair<int, int>(); | 
不过我们可以用相应的包装类型来代替。
(2)不能抛出也不能捕获泛型类实例
泛型类扩展Throwable即为不合法,因此无法抛出或捕获泛型类实例。但在异常声明中使用类型参数是合法的:
| 1 2 3 4 5 6 7 8 | publicstatic<T extendsThrowable> voiddoWork(T t) throwsT {    try{        ...    } catch(Throwable realCause) {        t.initCause(realCause);        throwt;    }} | 
(3)参数化类型的数组不合法
在Java中,Object[]数组可以是任何数组的父类(因为任何一个数组都可以向上转型为它在定义时指定元素类型的父类的数组)。考虑以下代码:
| 1 2 3 | String[] strs = newString[10];Object[] objs = strs;obj[0] = newDate(...); | 
在上述代码中,我们将数组元素赋值为满足父类(Object)类型,但不同于原始类型(Pair)的对象,在编译时能够通过,而在运行时会抛出ArrayStoreException异常。
基于以上原因,假设Java允许我们通过以下语句声明并初始化一个泛型数组:
| 1 | Pair<String, String>[] pairs = newPair<String, String>[10]; | 
那么在虚拟机进行类型擦除后,实际上pairs成为了Pair[]数组,我们可以将它向上转型为Object[]数组。这时我们若往其中添加Pair<Date, Date>对象,便能通过编译时检查和运行时检查,而我们的本意是只想让这个数组存储Pair<String, String>对象,这会产生难以定位的错误。因此,Java不允许我们通过以上的语句形式声明并初始化一个泛型数组。
可用如下语句声明并初始化一个泛型数组:
| 1 | Pair<String, String>[] pairs = (Pair<String, String>[]) newPair[10]; | 
(4)不能实例化类型变量
不能以诸如“new T(…)”, “new T[...]“, “T.class”的形式使用类型变量。Java禁止我们这样做的原因很简单,因为存在类型擦除,所以类似于”new T(…)”这样的语句就会变为”new Object(…)”, 而这通常不是我们的本意。我们可以用如下语句代替对“new T[...]“的调用:
| 1 | arrays = (T[]) newObject[N]; | 
(5)泛型类的静态上下文中不能使用类型变量
注意,这里我们强调了泛型类。因为普通类中可以定义静态泛型方法,如上面我们提到的ArrayAlg类中的getMiddle方法。关于为什么有这样的规定,请考虑下面的代码:
| 1 2 3 4 5 6 | publicclassPeople<T> {    publicstaticT name;    publicstaticT getName() {        ...    }} | 
我们知道,在同一时刻,内存中可能存在不只一个People<T>类实例。假设现在内存中存在着一个People<String>对象和People<Integer>对象,而类的静态变量与静态方法是所有类实例共享的。那么问题来了,name究竟是String类型还是Integer类型呢?基于这个原因,Java中不允许在泛型类的静态上下文中使用类型变量。
7. 类型通配符
介绍类型通配符前,首先介绍两点:
(1)假设Student是People的子类,Pair<Student, Student>却不是Pair<People, People>的子类,它们之间不存在”is-a”关系。
(2)Pair<T, T>与它的原始类型Pair之间存在”is-a”关系,Pair<T, T>在任何情况下都可以转换为Pair类型。
现在考虑这样一个方法:
| 1 2 3 4 | publicstaticvoidprintName(Pair<People, People> p) {    People p1 = p.getFirst();    System.out.println(p1.getName()); //假设People类定义了getName实例方法} | 
在以上的方法中,我们想要同时能够传入Pair<Student, Student>和Pair<People, People>类型的参数,然而二者之间并不存在”is-a”关系。在这种情况下,Java提供给我们这样一种解决方案:使用Pair<? extends People>作为形参的类型。也就是说,Pair<Student, Student>和Pair<People, People>都可以看作是Pair<? extends People>的子类。
形如”<? extends BoundingType>”的代码叫做通配符的子类型限定。与之对应的还有通配符的超类型限定,格式是这样的:<? super BoundingType>。
现在我们考虑下面这段代码:
| 1 2 3 | Pair<Student> students = newPair<Student>(student1, student2);Pair<? extendsPeople> wildchards = students;wildchards.setFirst(people1); | 
以上代码的第三行会报错,因为wildchards是一个Pair<? extends People>对象,它的setFirst方法和getFirst方法是这样的:
| 1 2 | voidsetFirst(? extendsPeople)? extendsPeople getFirst() | 
对于setFirst方法来说,会使得编译器不知道形参究竟是什么类型(只知道是People的子类),而我们试图传入一个People对象,编译器无法判定People和形参类型是否是”is-a”的关系,所以调用setFirst方法会报错。而调用wildchards的getFirst方法是合法的,因为我们知道它会返回一个People的子类,而People的子类“always is a People”。(总是可以把子类对象转换为父类对象)
而对于通配符的超类型限定的情况下,调用getter方法是非法的,而调用setter方法是合法的。
除了子类型限定和超类型限定,还有一种通配符叫做无限定的通配符,它是这样的:<?>。这个东西我们什么时候会用到呢?考虑一下这个场景,我们调用一个会返回一个getPairs方法,这个方法会返回一组Pair<T, T>对象。其中既有Pair<Student, Student>, 还有Pair<Teacher, Teacher>对象。(Student类和Teacher类不存在继承关系)显然,这种情况下,子类型限定和超类型限定都不能用。这时我们可以用这样一条语句搞定它:
| 1 | Pair<?>[] pairs = getPairs(...); | 
对于无限定的通配符,调用getter方法和setter方法都是非法的。
java 泛型简介(转载)的更多相关文章
- 【转载】Java泛型详解
		[转载]http://www.importnew.com/24029.html 对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下. 本文参考 ... 
- Java泛型深入理解(转载)
		原文地址 http://blog.csdn.net/sunxianghuang/article/details/51982979 泛型之前 在面向对象编程语言中,多态算是一种泛化机制.例如,你可以将 ... 
- Java异常(一) Java异常简介及其架构
		概要 本章对Java中的异常进行介绍.内容包括:Java异常简介Java异常框架 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3544168.html ... 
- 转:有关Java泛型的类型擦除(type erasing)
		转载自:拈花微笑 自从Java 5引入泛型之后,Java与C++对于泛型不同的实现的优劣便一直是饭后的谈资.在我之前的很多training中,当讲到Java泛型时总是会和C++的实现比较,一般得出的结 ... 
- Java 泛型 协变式覆盖和泛型重载
		Java 泛型 协变式覆盖和泛型重载 @author ixenos 1.协变式覆盖(Override) 在JDK 1.4及以前,子类方法如果要覆盖超类的某个方法,必须具有完全相同的方法签名,包括返回值 ... 
- java泛型应用实例 - 自定义泛型类,方法
		近 短时间需要使用泛型,就研究了下,发现网上的问关于泛型的文章都是讲原理的, 很少有提到那里用泛型比较合适, 本文就泛型类和泛型方法的使用给出两 个典型应用场景. 例如一个toString的泛型方法, ... 
- 关于Java泛型"擦除"的一点思考
		头次写博客,想说的东西不难,关于泛型的疑问,是前一阵在学习jackson中遇到的. 下面就把我所想到的.遇到的,分享出来. 泛型是JDK1.5后的一个特性,是一个参数类型的应用,可以将这个参数声明在类 ... 
- java 泛型详解(普通泛型、 通配符、 泛型接口,泛型数组,泛型方法,泛型嵌套)
		JDK1.5 令我们期待很久,可是当他发布的时候却更换版本号为5.0.这说明Java已经有大幅度的变化.本文将讲解JDK5.0支持的新功能-----Java的泛型. 1.Java泛型 其实Java ... 
- Java异常简介、异常捕获还是上抛总结
		概要 本章对Java中的异常进行介绍.内容包括:1.Java异常简介2.Java异常框架 一.Java异常简介 Java异常是Java提供的一种识别及响应错误的一致性机制. Java异常机制可以使程序 ... 
随机推荐
- 使用iframe的好处与坏处详细比拼
			一.使用iframe的坏处 1.搜索引擎的蜘蛛不会识别在iframe中被调用的图片.文本.url等内容的,因为该内容不属于该页面,只是访问的时候被临时的调用,而且在SEO建议中也有提到:"f ... 
- kNN算法 Demo
			项目链接: https://github.com/WES6/kNN 
- Proto3:Arena分配指南
			Arena分配是仅C++有的功能,在使用Protocol Buffer时,它可以帮助你优化你的内存使用,提高性能.在.proto文件中启用Arena分配会在生成的C++代码中添加处理Arena分配的额 ... 
- android-interview
			如何减小安装包的大小 主要是减小资源的大小 不常使用的资源,使用时再从网络下载. 绘制代替图片资源 OOM (Out Of Memory) https://www.zhihu.com/question ... 
- 测试LFI WITH PHPINO过程中的一些记录
			原理:以往LFI漏洞都是需要满足两个条件:1.攻击者上传一个含PHP代码的的文件,后缀名任意,没有后缀名也可以:2.需要知道上传后的文件路径及文件名,然后包含之. 后来有国外研究者发现了新的攻击方式, ... 
- JavaScript的封装和继承
			提到JavaScript"面向对象编程",主要就是封装和继承,这里主要依据阮一峰及其他博客的系列文章做个总结. 继承机制的设计思想 所有实例对象需要共享的属性和方法,都放在这个对象 ... 
- C++走向远洋——23(项目一,三角形,类)
			*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:sanjiaoxing.cpp * 作者:常轩 * 微信公众号: ... 
- TypeScript声明文件
			为什么需要声明? 声明的本质是告知编译器一个标识符的类型信息.同时,在使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全.接口提示等功能. 声明在TypeScript中至关重要,只有通过 ... 
- HTTP协议 有这篇文章足够了
			HTTP 协议详解 HTTP(HyperText Transfer Protocol)超文本传输协议.其最初的设计目的是为了提供一种发布和接收HTML页面的方法. HTTP是一个客户端(用户)和服务端 ... 
- linux 下修改最大文件数
			环境为centosV7系列 1.查看进程的打开最大文件数,默认为1024 [root@localhost ~]# ulimit -a core file size (blocks, -c) 0 dat ... 
