专栏名称: OSC开源社区
OSChina 开源中国 官方微信账号
目录
相关文章推荐
程序猿  ·  “再见了 ... ·  2 天前  
程序员小灰  ·  视频号可能是下一个风口! ·  5 天前  
赛尔实验室  ·  哈工大SCIR 8篇长文被 COLING ... ·  5 天前  
神秘的程序员们  ·  他从游戏走到了AI,从AI走到了诺贝尔奖 ·  2 月前  
51好读  ›  专栏  ›  OSC开源社区

Java 泛型总结

OSC开源社区  · 公众号  · 程序员  · 2017-04-24 08:37

正文

#点击图片报名上海源创会#


什么是泛型


泛型是jdk5引入的类型机制,就是将类型参数化,它是早在1999年就制定的jsr14的实现。


泛型机制将类型转换时的类型检查从运行时提前到了编译时,使用泛型编写的代码比杂乱的使用object并在需要时再强制类型转换的机制具有更好的可读性和安全性。


泛型程序设计意味着程序可以被不同类型的对象重用,类似c++的模版。


泛型对于集合类尤其有用,如ArrayList。这里可能有疑问,既然泛型为了适应不同的对象,ArrayList本来就可以操作不同类型的对象呀?那是因为没有泛型之前采用继承机制实现的,实际上它只维护了一个Object对象的数组。结果就是对List来说它只操作了一类对象Object,而在用户看来却可以保存不同的对象。


泛型提供了更好的解决办法——类型参数,如:


这样解决了几个问题:

1.可读性,从字面上就可以判断集合中的内容类型; 

2.类型检查,避免插入非法类型。 

3. 获取数据时不在需要强制类型转换。


泛型类


其中  是类型参数定义。


使用时:Pair p = new Pair();

此时类内部的field1就是字符串类型了。


如果引用多个类型,可以使用逗号分隔:

类型参数名可以使用任意字符串,建议使用有代表意义的单个字符,以便于和普通类型名区分,如:T代表type,有原数据和目的数据就用S,D,子元素类型用E等。当然,你也可以定义为XYZ,甚至xyZ。


泛型方法

泛型方法定义如下:


与泛型类一样, 是类型参数定义。如:


严格的调用方式:


一般情况下调用时可以省略,看起来就像定义String类型参数的方法:GenericMethod.getMiddle(String,String,String),这是因为jdk会根据参数类型进行推断。看一下下面的例子:


输出结果为:


这是因为jdk推断三个参数的共同父类,匹配为Object,那么相当于:


习惯了类型参数放在类的后面,如ArrayList,泛型方法为什么不放在后面?看一个例子:


因此,为了避免歧义,jdk采用类型限定符前置。



泛型方法与泛型类的方法


如果泛型方法定义在泛型类中,而且类型参数一样:


是不是说,定义GenericMethod时传了 Integer 类型,sayHi()也就自动变成 Integer 了呢?No。


该代码运行一点问题都没有。原因就在于泛型方法中的,如果去掉它,就有问题了。


小结:

泛型方法有自己的类型参数,泛型类的成员方法使用的是当前类的类型参数。


方法中有 是泛型方法;没有的,称为泛型类中的成员方法。



类型参数的限定


如果限制只有特定某些类可以传入T参数,那么可以对T进行限定,如:只有实现了特定接口的类:,表示的是Comparable及其子类型。


为什么是extends不是 implements,或者其他限定符?

严格来讲,该表达式意味着:`T subtypeOf Comparable`,jdk不希望再引入一个新的关键词;


其次,T既可以是类对象也可以是接口,如果是类对象应该是`implements`,而如果是接口,则应该是`extends`;从子类型上来讲,extends更接近要表达的意思。


好吧,这是一个约定。


限定符可以指定多个类型参数,分隔符是 &,不是逗号,因为在类型参数定义中,逗号已经作为多个类型参数的分隔符了,如:


泛型限定的优点:


限制某些类型的子类型可以传入,在一定程度上保证类型安全;


可以使用限定类型的方法。如:


加上限定符,就可以访问限定类型的方法了,类型更明确。


注:

我们知道final类不可继承,在继承机制上class SomeString extends String是错误的,但泛型限定符使用时是可以的:,只是会给一个警告。


后面的通配符限定有一个super关键字,这里没有。



泛型擦除

泛型只在编译阶段有效,编译后类型被擦除了,也就是说jvm中没有泛型对象,只有普通对象。所以完全可以把代码编译为jdk1.0可以运行的字节码。


擦除的方式

定义部分,即尖括号中间的部分直接擦除。


擦除后:


引用部分如:


其中的T被替换成对应的限定类型,擦除后:


如果没有限定类型:


那么的替换为object,即:


有多个限定符的,替换为第一个限定类型名。如果引用了第二个限定符的类对象,编译器会在必要的时候进行强制类型转换。


类擦除后变为:


而表达式返回值返回时,泛型的编译器自动插入强制类型转换。



泛型擦除的残留

反编译GenericClass:


好像前面说的不对啊,这还是T啊,没有擦除呀?


这就是擦除的残留。反汇编:


其中:

descriptor:对方法参数和返回值进行描述; signature:泛型类中独有的标记,普通类中没有,JDK5才加入,标记了定义时的成员签名,包括定义时的泛型参数列表,参数类型,返回值等;


可以看到public T field1;是签名,还保留了定义的格式;其对应的参数类型是Ljava/lang/Object;。


最后一行是类的签名,可以看到T后面有跟了擦除后的参数类型:

这样的机制,对于分析字节码是有意义的。



泛型的约束和限制


不能使用8个基本类型实例化类型参数

原因在于类型擦除,Object不能存储基本类型:

byte,char,short,int,long,float,double,boolean


包装类角度来看,或者说三个: Number(byte,short,int,long,float,double),char,boolean


类型检查不可使用泛型


不能创建泛型对象数组


可以定义泛型类对象的数组变量,不能创建及初始化。


注,可以创建通配类型数组,然后进行强制类型转换。不过这是类型不安全的。


不可以创建的原因是:因为类型擦除的原因无法在为元素赋值时类型检查,因此jdk强制不允许。


有一个特例是方法的可变参数,虽然本质上是数组,却可以使用泛型。


安全的方法是使用List。


Varargs警告

java不支持泛型类型的对象数组,可变参数是可以的。它也正是利用了强制类型转换,因此同样是类型不安全的。所以这种代码编译器会给一个警告。


去除警告有两种途径:一种是在定义可变参数方法上(本例中的getMiddle())加上@SafeVarargs注解,另一种是在调用该方法时添加@SuppressWarnings("unchecked")注解。


不能实例化泛型对象


解决办法是传入Class t参数,调用t.newInstance()。


不能在泛型类的静态域中使用泛型类型


但是,静态的泛型方法可以使用泛型类型:


这个原因很多资料中都没说的太明白,说一下个人理解,仅供参考:

1. 泛型类中,称为类型变量,实际上就相当于在类中隐形的定义了一个不可见的成员变量:`private T t;`,这是对象级别的,对于泛型类型变量来说是在对象初始化时才知道其具体类型的。而在静态域中,不需要对象初始化就可以调用,这是矛盾的。


2. 静态的泛型方法,是在方法层面定义的,就是说在调用方法时,T所指的具体类型已经明确了。


不能捕获泛型类型的对象

Throwable类不可以被继承,自然也不可能被catch。


但由于Throwable可以用在泛型类型参数中,因此可以变相的捕获泛型的Throwable对象。


这个能干什么?


FileReader实例化可能抛出已检查异常,jdk中要求必须捕获或者抛出已检查异常。这种模式把它给隐藏了。也就是说可以消除已检查异常,有点不地道,颠覆了java异常处理的认知,后果不可预料,慎用。


擦除的冲突

重载与重写

定义一个普通的父类:


那么继承一个子类,Son.java


Son类重载了一个setName(String)方法,这没问题。输出:


Parent修改泛型类:


从擦除的机制得知,擦除后的class文件为:


这和最初的非泛型类是一样的,那么Son类修改为:


发现重载无效了。这是泛型擦除造成的,无论是否在setName(String)是否标注为@Override都将是重写,都不是重载。而且,即便你不写setName(String)方法,编译器已经默认重写了这个方法。


换一个角度来考虑,定义Son时,Parent已经明确了类型参数为String,那么再写setName(Stirng)是重写,也是合理的。


反编译会发现,编译器在内部编译了两个方法:


setName(java.lang.Object) 虽然是public但编码时会发现不可见,它称为"桥方法",它会重写父类的方法。


强行调用会转换异常,也就证明了它实际上调用的是son的setName(String)。


我非要重载怎么办?只能曲线救国,改个名字吧。



继承泛型的参数化

一个泛型类的类型参数不同,称之为泛型的不同参数化。


泛型有一个原则:一个类或类型变量不可成为两个不同参数化的接口类型的子类型。如:


这样是没有问题的。如果增加了泛型参数化:


原因是Son实现了两次Comparator,擦除后均为Comparator,造成了冲突。



通配符类型


通配符是在泛型类使用时的一种机制,不能用在泛型定义时的泛型表达式中(这是泛型类型参数限定符)。


子类型通配符

如果P是S的超类,那么 Pair就是Pair extends P>的子类型,通配符就是为了解决这个问题的。


这称为子类型限定通配符,又称上边界通配符(upper bound wildcard Generics),代表继承它的所有子类型,通配符匹配的类型不允许作为参数传入,只能作为返回值。


getName()的合理性:


为什么setName(str)会抛出异常呢?


超类型通配符

与之对应的是超类型 Pair super P>,又称下边界通配符(lower bound wildcard Generics),通配符匹配的类型可以为方法提供参数,不能得到返回值。


setName的可行性:

1. 无论bean2指向Parent,Parent还是Parent都是允许的;


2. 都可以传入Integer或Integer的子类型。


getName为毛报错?

1. 由于限定类型的超类可能有很多,getName返回类型不可预知,如Integer 或其父类型Number/OtherParentClass...都无法保证类型检查的安全。


2. 但是由于Java的所有对象的顶级祖先类都是Object,因此可以用Object获取getName返回值。


无限定通配符

Pair> 就是 Pair extends Object>


因此,无限定通配符可以作为返回值,不可做入参。


返回值只能保存在Object中。


P> 和P


Pair可以调用setter方法,这是它和Pair>最重要的区别。


P> 不等于 P


P是P>的子类。


类型通配符小结

1. 限定通配符总是包括自己;

2. 子类型通配符:set方法受限,只可读,不可写;

3. 超类型通配符:get方法受限,不可读(Object除外),只可写;

4. 无限定通配符,只可读不可写;

5. 如果你既想存,又想取,那就别用通配符;

6. 不可同时声明子类型和超类型限定符,及extends和super只能出现一个。

通配符的受限只针对setter(T)和T getter(),如果定义了一个setter(Integer)这种具体类型参数的方法,无限制。



通配符捕获


通配符限定类中可以使用T,编译器适配类型。


有一个键值对的泛型类:


使用通配类型创建一个swap方法交换key-value,交换时需要先使用一个临时变量保存一个字段:


这里有一个办法解决它,再封装一个swapHelper():


这种方式,称为:通配符捕获,用一个Pair 来捕获 Pair>中的类型。


注:

当然,你完全可以直接使用swapHelper,这里只是为了说明这样一种捕获机制。


只允许捕获单个、确定的类型,如:ArrayList> 是无法使用 ArrayList> 捕获的。



泛型与继承


继承的原则

继承泛型类时,必须对父类中的类型参数进行初始化。或者说父类中的泛型参数必须在子类中可以确定具体类型。


例如:有一个泛型类Parent,那么Son类定义时有两种方式初始化父类型的类型参数:


1 用具体类型初始化:


2 用子类中的泛型类型初始化父类:


Pair

和 Pair

无论P和S有什么继承关系,一般Pair

和Pair没什么关系。



Parent 和 Son

泛型类自身可以继承其他类或实现接口,如 List实现ArrayList


泛型类可以扩展泛型类或接口,如ArrayList 实现了 List,此时ArrayList可以转换为List。这是安全的。


Parent和Parent

Parent随时都可以转换为原生类型Parent,但需要注意类型检查的安全性。


运行没有异常,注意。


Person extends XXX>

严格讲通配符限定的泛型对象不属于继承范畴,但使用中有类似继承的行为。


Son是Parent的子类型,那么Person extends Son>就是Person extends Parent> 的子类型。


Person extends Object> 等同于 Person>,那么基于上以规则可以推断:Person extends Parent> 是 Person> 的子类型。


Person 是 Person> 的子类型。



泛型与反射


泛型相关的反射

有了泛型机制,jdk的reflect包中增加了几个泛型有关的类:


实例

基于泛型的通用JDBC DAO。


User.java


AbstractBaseDaoImpl.java


UserDaoImpl.java


运行UserDaoImpl.main(),输出:


可以看到,在抽象类AbstractBaseDaoImpl中可以拿到泛型类的具体类。


从这一机制,可以通过AbstractBaseDaoImpl实现通用的JDBA DAO。


完善AbstractBaseDaoImpl.java


有现成的ORM框架可用,这里就意思意思得了。输出:




推荐阅读

实用贴 | 适用于开发者的最佳火狐插件

12 个学习新的编程语言的方法

MyBatis 思维导图,让 MyBatis 不再难懂(一)

Java 的泛型擦除和运行时泛型信息获取

软件的复杂性:命名的艺术

点击“阅读原文”查看更多精彩内容