What
Java从1.0版本到现在的8,中间Java5中发生了一个很重要的变化,那就是泛型机制的引入。Java5引入了泛型,主要还是为了满足在1999年指定的最早Java规范之一。经过了5年左右的时间,专家组定义了一套泛型规范,实现后通过测试投入到使用。所以说泛型是Java5以后才有的,欲知详情,继续往下看。
Why
换个角度想,Java5引入泛型,必定是它能带来好处,否则牛气的Java专家工程师就要遭到吐槽了。我们来吐槽一下没有泛型的程序是怎么写的。
[code01]
- ArrayList al = new ArrayList();
- al.add("ysjian001");
- al.add(1);
- al.add(new Object());
这段代码看似功能强大,为什么呢?因为它似乎能够往集合添加各种类型的对象(int 类型会被装箱成 Integer对象类型),貌似一些老程序员也倾向于这么去做,而且他们可以理直气壮的告诉我理由:我这么做想存什么就存什么!先不否定这种说法,让我们继续,看看下面代码。
[code02]
-
- String first = (String) al.get(0);
往集合里面存值就是为了后期取出来用的,而不是System.out.println(first),这里就产生了一个强制转换的问题,而往往这种类型的强制转换在编译器是允许通过的,而写程序的人们会犯下无意间的错误,错误的进行了强制转换,导致程序运行失败。
强制类型转换导致的程序运行失败的原因是没有在编译器对类型进行控制,看看code01调用ArrayList对象的add方法,任何类型都是可以添加进行的,编译器无法进行错误检验,埋下了安全隐患,例如:
[code03]
- ArrayList al = new ArrayList();
-
- al.add(new File());
- String first = (String) al.get(0);
1.编译器无法进行类型检查,可以向集合中添加任意类型的对象。
2.取值时类型转换失败导致程序运行失败。
没有泛型的程序导致的后果:
1.程序的可读性有所降低,因为程序员可以不受限制往集合中添加任意对象。
2.程序的安全性遭到质疑,类型转换失败将导致程序运行失败。
Java5泛型提供了一个更好的解决方案:类型参数(type parameters),使用泛型的程序改善上述代码如下:
[code04]
- ArrayList<String> al = new ArrayList<String>();
- al.add( "ysjian001");
-
- String first = al.get(0);
问:到这里,通过前后对比,泛型的好处是不是很清楚了呢?为什么用泛型呢?
答:因为出现编译错误比类在运行时出现强制类型转换异常要好得多,泛型的好处在于提高了程序的可读性和安全性,这也值程序设计的宗旨。
Who
使用泛型类是一件很轻松的事,集合框架中的类都是泛型类,用起来很方便。有人会想类型限制我们为什么不直接用数组呢?这个问题就好像问为什么集合优于数组,数组是固定的,而集合是可以自动扩展的。另外在实际中,实现一个泛型其实并不是那么容易。看一个员工和经理继承结构:
[code05]
- public class Employee {
-
- }
- public class Manager extends Employee {
-
- }
当我们创建并使用一个员工的集合的时候,使用起来并不复杂:
[code06]
- ArrayList<Employee> employees = new ArrayList<Employee>();
- employees.add(new Employee());
- employees.add( new Manager());
上述的使用毫无问题的,因为Manager is a Employee,典型的继承关系,但是当反过来的时候,可能不那么顺利了,比如:
[code07]
- ArrayList<Manager> employees = new ArrayList<Manager>();
- employees.add(new Manager());
-
上面的代码就有问题了,而这种需求又不是不存在,那么怎么办呢?不要着急,聪明的Java设计者发明了一个独具创新的新概念,通配符类型(wildcard type),这里只需要知道这个概念,后面会详细讲解。
大多数应用程序员对泛型的熟练程度仅仅停留在使用泛型上,像集合类中的List、Set和Map这些泛型集合用的很多,他们不必考虑这些泛型集合的工作方式和原理。那么当把不同的泛型类混合在一起使用时,或者对Java5之前的遗留代码进行衔接时,可能会看到含糊不清的的错误消息。这样一来,程序员就需要学习Java泛型来解决问题了,而不是在程序中胡乱猜测了。最终,部分程序员可能想要实现自己的泛型类和泛型方法。
提炼出泛型程序设计的三种熟练程度就是:
1.仅仅使用泛型。
2.学习泛型解决一些问题。
3.掌握泛型,实现自己的泛型。
How
如何使用泛型听起来是一件很容易的事情,因为Sun公司的那些工程师已经做了很大努力,而需求总是会稍微苛刻一点的,需要解决因为缺乏类型参数模糊不清的问题,或者我们有必要实现自己的泛型来满足业务需求,所以学习和掌握泛型是很有必要的。
泛型类:
从简单的入手,定义一个泛型类:
[code08]
- public class Couple<T> {
- private T wife ;
- private T husband ;
-
- public Couple(T wife, T husband) {
- this.wife = wife;
- this.husband = husband;
- }
- public void setWife(T wife) {this. wife = wife;}
- public void setHusband(T husband) {this. husband = husband;}
-
- public T getWife() {return wife;}
- public T getHusband() {return husband;}
- }
Couple 夫妇类引入一个类型参数T,注意了,类型参数是用尖括号(< >)括起来的,并且放在类名后面,code08中的Couple类有一个类型参数,可以定义多个类型参数,格式为<T, K, V>, 类型参数可以用来定义方法的返回类型、参数类型、以及定义域或局部变量,如下面代码
[code09]
- public class Couple<T, K, V> {......}
- private T wife ;
- public T getWife() {return wife;}
在Java类库中,类型变量通常用大写的字母表示,E表示集合的元素类型,K和V分别表示映射表的关键字和值的类型,T(U或S)表示任意类型。
一个简单的泛型类Couple定义好了,怎么使用呢?别着急,我们使用这个Couple类时指定一个具体的参数类型,如Person类:Couple<Person>
[code10]
- Couple<Person>(Person,Person);
- setWife(Person);
- setHusband(Person);
- Person getWife();
- Person getHusband();
code10中的代码是使用Person类型作为参数类型后,Couple类型的变化,注意这里 不是真正的变化成这样了,而是我们使用的时候这么理解,至于为什么呢?在后面会详细讲解擦除。
泛型方法:
定义了泛型类有什么好处呢?通过前面的例子,这个泛型类可以让使用该类的用户在使用的时候指定才具体的类型,提高了一定的灵活性。那么看看泛型方法的定义是怎么回事?
[code11]
- public class GenericMethod {
- public static <T> T getFirstValue(T[] values) {
- return values[0];
- }
- }
从已经学习的泛型类加上code11中的泛型方法的定义中总结一条,就是所有泛型都必须先以<T>(多个类型参数用逗号隔开,如<K, V>)的形式进行定义,这是必要的前提,回过头来看看上述泛型方法的定义,给方法定义了一个类型参数T,指定方法的返回值为T,方法的参数为T类型的数组。
对方法的调用就比较直观了
[code12]
- String[] values = { "JavaSE","CoreJava" ,"EffectiveJava"};
- String firstValue = GenericMethod.<String>getFirstValue(values);
咋看调用还是有点复杂,为什么要在方法调用前用<String>呢?其实这不是必要的,当我们将String[]类型的values传给方法时,编译器足以判断T的具体类型为String类型,所以<String>可以省略掉。
[code13]
- String firstValue = GenericMethod.getFirstValue(values);
总结:
这一节里,对泛型有了一个整体的认识,知道它是什么?为什么要用它?谁会用它?以及如何使用它?通过了泛型类和泛型方法的实践,感受了如何实现自己的泛型,后面一节,将对泛型中通配符进行讲解,以及虚拟机对泛型类和泛型方法的擦除。
Java泛型解析(02):通配符限定
考虑一个这样的场景,计算数组中的最大元素。
[code01]
- public class ArrayUtil {
- public static <T> T max(T[] array) {
- if (array == null || 0 == array.length) { return null ;}
- T max = array[0];
- for (int i = 1; i < array.length; i++) {
- if (max.compareTo(array[i]) < 0) {max = array[i];}
- }
- return max;
- }
- }
仔细看看code01里面的代码(代码不完整),使用类型参数T定义一个max局部变量,这几意味着这个max可以是任意的类型,那么max.compareTo(array[i]) 方法的调用的前提是T所属的类中有compareTo方法,怎么能做到这一点呢?别着急,让我们来看看如何给类型参数进行限定,现在来对code01中的代码进行完善。
[code02]
- public class ArrayUtil {
- public static <T extends Comparable<T>> T max(T[] array) {
- if (array == null || 0 == array.length) { return null ;}
- T max = array[0];
- for (int i = 1; i < array.length; i++) {
- if (max.compareTo(array[i]) < 0) {max = array[i];}
- }
- return max;
- }
- }
注意看,我们定义类型参数的变化:<T extends Comparable<T>>,这里将T类型限定在Comparable及其所有的子类。是不是很好奇Comparable明明是一个interfacce,根据所学知识判断,实现interface用的关键字是implements,为什么呢?
<T extends Bounding Type>,表示T类型应该是绑定类型及其子类型(subType),T和绑定类型可以是类或者接口,使用extends关键字因为它更接近于子类的概念,另外Java设计者并不打算为Java添加新的关键字如:sub
如果给T限定多个类型,则需要使用符号"&",如下面格式
[code03]
- <T extends Runnable & Serializable>
细心的读者可能会发现,这里限定的都是interface,如果限定为class是不是也这么自由的呢?先不急着回答这个问题,我们知道Java中可以实现多个接口,而继承只能是单继承,可想而知,当我们给T限定类型的时候,限定为某个class的时候是有限制的,看看下面几组泛型限定的代码
[code04]
- <T extends Runnable & Serializable & ArrayList>
- <T extends Runnable & ArrayList & Serializable>
- <T extends ArrayList & LinkedList & Serializable>
- <T extends ArrayList & Runnable& Serializable>
不难看出,如果要限定T为class的时候,就有一个非常严格的规则,这个class只能放在第一个,最多只能有一个class。其实很容易理解,这样一来,就能够严格控制T类型是单继承的,遵循Java的规范。
小结:
1.类型限定只能限定某个类型及其子类,使用关键字extends。
2.多个类型参数用","隔开,如:<K, V>,多个限定类型用"&"隔开,如: <T extends Runnable & Serializable>
3.限定interface的时候,对interface的个数和顺序无严格要求,限定class时,则需要将class类型置于第一个,且最多只能存在一个class类型。
钻牛角尖:
问:类型限定中可以通过 extends 来限定子类型,是否可以通过类似super关键字来限定超类型呢?
答:哈哈,问的好,接下来一一揭晓。
比较遗憾的是,类似<T extends Runnable & Serializable>这样的泛型限定子类的语法,来限定超类是没有成为Java中的一个语法规范的,例如:
[code05]
- <T super File & Runnable>
code03中的类型参数的定义是错误的,至少目前Java中没有这样的规范来支撑这种语法,如何解释这个问题,笔者得花一番心思了...
不得不请教面向对象先生了:
1.面向接口(抽象)编程,而非面向实现编程。这个设计原则告诉我们,方法调用通过高层的抽象类或者接口来进行,具体调用的方法体就是我们实际运行时期传递的具体实现类的实例,这也是多态的一种体现。我们实现自己的泛型是提供后期应用程序员使用的,限定一个子类,这就需要我们通过子类来调用方法,而调用的方法体则是这个类的超类的实例,继承结构越往上就可能是abstract的,或者是interface,抽象类和接口是无法实例化对象,这种反设计让调用面临失败,一旦限定的这个类就是抽象的或者是接口,必定会造成这个泛型类或泛型方法无法使用,导致设计失败。举个例子:
[code06]
- public static <T super Runnable> void test(T runner) {
- runner.run();
- }
这个T类型限定为Runnable的超类,这个Runnable是一个接口,无法实例化对象,方法参数runner就是一个不存在的实例,所以这是一个失败的设计,而且这种语法也无法通过编译器。
面向对象先生的解释对初学者可能有点晦涩难懂,没关系,这里只要知道Java是不能支持这种泛型限定的。无论从设计角度,还是从后期扩展的角度,都是说不过去的。
但是不能这样定义泛型,并不代表Java泛型中就没有
super 关键字了,接下来说说泛型中的通配符类型,有了前面的基础,这里恐怕不是问题了。
通配符类型
通配符类型,相比于固定的泛型类型,它是一个巧妙的解决方案。如:
[code07]
- Couple<? extends Employee>
表示Couple的泛型类型是Employee或者其子类,Couple<Manager>满足,而Couple<File>不满足了。通配符用 "?" 表示。
我们来打印一下夫妇类中的wife:
[code08]
- public static void printWife(Couple<Employee> couple) {
- Employee wife = couple.getWife();
- System. out.println(wife);
- }
code08中的方法参数只能将Employee组成的夫妇传入,貌似经理的如Couple<Manager>就不能了,这有点不合适了,搞得好像Manager还不能结婚了。所以要想让Manager也能结婚并打印其wife,需要更改我们的设计了:
[code09]
- public static void printWife(Couple<? extends Employee> couple) {
- Employee wife = couple.getWife();
- System. out .println(wife);
- }
通配符的子类型限定的语法与文章一开始介绍的类型限定有点相似,但是这里有些细节的秘密。
[code10]
- public static <T extends Comparable<T>> T max(T[] array) {...}
- public static void printWife(Couple<? extends Employee> couple) {...}
前者T是预定义的类型参数,T可以作为一个具体的类型来定义方法的参数类型,局部变量等,T的作用域是整个方法(方法返回值,参数,方法体中局部变量),这种设计是为了给使用者带来方便,将参数类型的指定权有限制地交给了使用者。而后者中不存在类型参数的定义,max方法参数的类型是预先定义好的Couple类型,使用者无法在使用的时候指定其他类型,但可以有限制地指定Couple中的泛型参数的类型,
? extends Employee 自身不能单独使用,可以理解为只能寄生在其他泛型类中,作为泛型类一个具体的类型参数,通常用于定义阶段,如下面:
[code11]
- public static ? extends Employee printWife() {...}
- public static void printWife(? extends Empoyee employee) {...}
使用通配符来定义方法返回值和方法的参数类型在Java中是不允许的!
弄清楚了前面类型限定和通配符的区别以后,再引入通配符的超类型限定就不是那么难以理解了。
通配符的超类型限定:
和前面子类型的限定一样,用"?"表示通配符,它的存在必须存在泛型类的类型参数中,如:
格式跟通配符限定子类型一样,用了关键字super,但是这两种方式的通配符存在一个隐蔽的区别,让我们来揭晓吧,先看看下面代码:
[code13]、
- Couple<Manager> couple = new Couple<Manager>(new Manager(),new Manager());
- Couple<? extends Employee> couple2 = couple;
- Employee wife = couple2.getWife();
-
Couple<?
extends
Employee>定义了couple2后可以将getWife和setWife想象成如下:
[code14]
- ? extends Employee getWife() {...}
- void setWife(? extends Employee) {...}
getWife是可以通过的,因为将一个返回值的引用赋给超类Employee是完全可以的,而setWife方法接受的是一个Employee的子类,具体是什么子类,编译器并不知道,拒绝传递任何特定的类型,所以couple2.setWife(new Manager())是不能被调用的。所以通配符的子类限定适用于读取。
在来看看通配符的超类型限定,即Couple<? super Manager>,getWife和setWife可以想象成:
[code15]
- ? super Manager getWife() {...}
- void setWife(? super Manager) {...}
getWife方法的返回值是Manager的超类型,而Manger的超类型是得不到保证的,虚拟器会将它会给Object,而setWife方法是需要的是Manager的超类型,所以传入任意Manager都是允许的,所以通配符的超类型限定适用于写入。
无限定通配符
无限定通配符去除了超类型和子类型的规则,仅仅用一个"?"表示,并且也只能用于指定泛型类的类型参数中。如Couple<?>的形式,此时getWife和setWife方法如:
[code16]
- ? getWife() {...}
- void setWife(?) {...}
getWife返回值直接付给了Object,而setWife方法是不允许调用的。那么既然这么脆弱,牛逼的Java设计者为什么还要引入这种通配符呢?在一些简单的操作中,五限定通配符还是有用武之地的,比如:
[code17]
- public static boolean isCoupleComplete(Couple<?> couple) {
- return couple.getWife() != null && couple.getHusband() != null;
- }
这个方法体中,getWife和getHusband返回值都是Object类型的,此时我们只需要判断是否为null即可,而不需要知道具体的类型是什么,这就发挥了无限定通配符的作用了。发动脑经想一想,这个方法用文章开始所提到类型限定是否可以代替呢?自我思考中...
[code18]
- public static <T> boolean isCoupleComplete(Couple<T> couple) {
- return couple.getWife() != null && couple.getHusband() != null ;
- }
- public static <T extends Employee> boolean isCoupleComplete(Couple<T> couple) {
- return couple.getWife() != null && couple.getHusband() != null ;
- }
到这里,爱思考的读者可能在思考一个问题,通配符代表了泛型类中的参数类型,在方法体中,怎么去捕获这个参数类型呢?这里考虑三种通配符的捕获
1.Couple<? extends Employee> couple:getWife返回Employee
2.Couple<? super Manager> couple:无法捕获,getWife返回Object
3.Couple<?> couple:无法捕获,getWife返回Object
悲催了,只有第一种能捕获,怎么办呢?别着急,看看下面的小魔术:
[code19]
- public static void print(Couple<?> couple) {
- printHelp(couple);
- }
- public static <T> void printHelp(Couple<T> couple) {
- T husband = couple.getHusband();
- T wife = couple.getWife();
- couple.setHusband(wife);
- couple.setWife(husband);
- System. out.println(husband);
- System. out.println(wife);
- }
当需要捕获通配符的时候,可以借助前面所学的类型参数进行辅助,其实这是一个多余的动作,基本上用不到这么麻烦,这么做是为了把通配符和泛型限定联系起来,巩固一下之前的学习。
总结:
1.泛型参数的限定,使用extends关键字,限定多个类型时用"&"隔开。如:<T extends Runnable& Serializable>
2.泛型参数限定中,如果限定的类型是class而不是interface,则class必须放在限定类表中的第一个,且最多只能存在一个class。如:<T extends ArrayList & Runnable& Serializable>
3.通配符只能用在泛型类的泛型参数中,不能单独使用。如Couple<?>、Couple<? extends Employee> 、Couple<? super Manager>
4.通配符的超类型限定适用于写入,通配符的子类型限定适用于读取,无限定通配符适用于一些非null判断等简单操作。
5.通配符的捕获可以借助泛型类型限定来辅助。
这一节内容比较多,需要花点时间好好消化,体会总结中的5点,下一节,说一个深刻点的话题,虚拟机中的针对泛型代码的擦除。
Java泛型解析(03):虚拟机执行泛型代码
Java虚拟机是不存在泛型类型对象的,所有的对象都属于普通类,甚至在泛型实现的早起版本中,可以将使用泛型的程序编译为在1.0虚拟机上能够运行的class文件,这个向后兼容性后期被抛弃了,所以后来如果用Sun公司的编译器编译的泛型代码,是不能运行在Java5.0之前的虚拟机的,这样就导致了一些实际生产的问题,如一些遗留代码如何跟新的系统进行衔接,要弄明白这个问题,需要先了解一下虚拟机是怎么执行泛型代码的。
虚拟机的一种机制:擦除类型参数,并将其替换成特定类型,没有指定特定类型用Object代替,如前文中的Couple<T>类,虚拟机擦除后:
[code01]
- public class Couple {
- private Object wife ;
- private Object husband ;
-
- public Couple(Object wife, Object husband) {
- this.wife = wife;
- this.husband = husband;
- }
- public void setWife(Object wife) {this. wife = wife;}
- public void setHusband(Object husband) {this. husband = husband;}
-
- public Object getWife() {return wife;}
- public Object getHusband() {return husband;}
- }
类型参数T是一个任意类型的,所以擦除后用Object代替了。不管是Couple<Employee>或者Couple<String>擦除后都成为了原始类Couple类,这就好比回到了泛型引入Java之前的普通类。所以这里重点围绕着擦除类型参数这个机制展开讲解。
如有对类型参数有类型限定会怎么替换呢?擦除类型参数机制告诉我们,使用限定的类型代替,如果有多个,使用第一个代替,看一段代码:
[code02]
- public class Period<T extends Comparable<T> & Serializable> {
- private T begin;
- private T end;
-
- public Period(T one, T two) {
- if (one.compareTo(two) > 0) {begin = two;end = one;
- } else {begin = one;end = two;}
- }
- }
code02擦除后,Period的原始类型如下:
[code03]
- public class Period {
- private Comparable begin;
- private Comparable end;
-
- public Period(Comparable one, Comparable two) {
- if (one.compareTo(two) > 0) {begin = two; end = one;
- } else {begin = one; end = two;}
- }
- }
思考一下,如果将Period<T extends Comparable<T> & Serializable>写成Period<T extends Serializable & Comparable<T>>会是怎么样呢?同理,擦除后原始类型用第一个Serializable代替,这样进行compareTo方法调用的时候,编译器会进行必要的强制类型转换,所以为了提高效率,将标签接口(没有任何方法的接口,也叫tagging接口)放在后面。
先来看看虚拟机执行表达式的时候发生了什么,如:
[code04]
- Couple<Employee> couple = ...;
- Employee wife = couple.getWife();
擦除后,getWife()返回的是Object类型,然后虚拟机会插入强制类型转换,将Object转换为Employee,所以虚拟机实际上执行了两天指令:
1.调用Couple.getWife()方法。
2.将Object转换成Employee类型。
再来看看虚拟机执行泛型方法的时候发生了什么,泛型方法如:
[code05]
- public static <T extends Comparable<T>> max(T[] arrays) {... }
- 擦除后成了:
- public staticComoparable max(Comparable[] arrays) {... }
但是泛型方法的擦除会带来两个复杂的问题,且看第一个实例,一个实例:
[code06]
- public class Period <T extends Comparable<T> & Serializable> {
- private T begin;
- private T end;
-
- public Period(T one, T two) {
- if (one.compareTo(two) > 0) {begin = two;end = one;
- } else {begin = one;end = two;}
- }
- public void setBegin(T begin) {this. begin = begin;}
- public void setEnd(T end) {this. end = end;}
- public T getBegin() {return begin;}
- public T getEnd() {return end;}
- }
- public class DateInterval extends Period<Date> {
-
- public DateInterval(Date one, Date two) {
- super(one, two);
- }
- public void setBegin(Date begin) {
- super.setBegin(begin);
- }
- }
DateInterval类型擦除后,Period中的方法变成:
public void setBegin(Object begin) {...}
而DateInterval中的方法还是:
public void setBegin(Date begin) {...}
所以DateInterval从Period中继承了 public void setBegin(Object begin) {...}而自身又存在public void setBegin(Date begin) {...}方法,用户使用时问题发生了:
[code07]
- Period<Date> period = new DateInterval(...);
- period.setBegin(new Date());
这里因为period引用指向了DateInterval实例,根据多态性,setBegin应该调用DateInterval对象的setBegin方法,可是这个擦除让Period中的 public void setBegin(Object begin) {...}被调用,导致了擦除与多态发生了冲突,怎么办呢?虚拟机此时会在DateInterval类中生成一个桥方法(bridge method),调用过程发生了细微的变化:
[code08]
- public void setBegin(Object begin) {
- setBegin((Date)begin);
- }
有了这个合成的桥方法以后,code07中对setBegin的调用步骤如下:
1.调用DateInterval.setBegin(Object)方法。
2.DateInterval.setBegin(Object)方法调用DateInterval.setBegin(Date)方法。
发现了吗,当我们在DateInterval中增加了getBegin方法之后会是什么样子的呢?是不是Peroid中有一个Object getBegin()的方法,而DateInterval中有一个Date getBegin()方法呢,这两个方法在Java中是不能同时存在的?可是Java5以后增加了一个协变类型,使得这里是被允许的,看看DateInterval中getBegin方法就知道了:
[code09]
- @Override
- public Date getBegin(){ return super.getBegin(); }
这里用了@Override,说明是覆盖了父类的Object getBegin()方法,而返回值可以指定为父类中的返回值类型的子类,这就是协变类型,这是Java5以后才可以允许的,允许子类覆盖了方法后指定一个更严格的类型(子类型)。
总结:
1.记住一点,虚拟机中没有泛型,只有普通的类。
2.所有泛型的类型参数都用它们限定的类型代替,没有限定则用Object。
3.为了保持类型安全性,虚拟机在有必要时插入强制类型转换。
4.桥方法的合成用来保持多态性。
5.协变类型允许子类覆盖方法后返回一个更严格的类型。
Java泛型解析(04):约束和局限性
前两节,认识和学习了泛型的限定以及通配符,初学者可能需要一些时间去体会到泛型程序设计的好处和力量,特别是想成为库程序员的同学就需要下去体会通配符的运用了,应用程序员则需要掌握怎么使用泛型,这里针对泛型的使用中的约束和局限性做一个介绍性的讲解。
不能用基本类型实例化类型参数
这个很好理解,实例化泛型类时泛型参数不能使用基本类型,如List<int>这是不合法的,要存储基本类型则可以利用基本类型的包装类如List<Integer> 、List<Double>等,下面补充一下八种基本类型对应的包装类,和默认初始值
[code01]:
- byte Byte 0
- short Short 0
- int Integer 0
- long Long 0L
- float FLoat 0.0F
- double Double 0.0D
- char Character '\u0000'
- boolean Boolean false
不能实例化类型参数
Java中创建对象使用new关键字,但是泛型的类型参数不能用于实例化对象,如:
[code02]
- public Couple() {wife = new T(); husband = new T();}
如果是在想要创建T对象,就必须使用Java反射来创建对象了,遗憾的是T.class在Java中也是不被支持使用的,所以一种弥补的方式,传入一个类型阐述为T的Class对象,如Class<T>
[code03]
- public static <T> Couple<T> createInstance(Class<T> clazz) {
- try {
- return new Couple<T>(clazz.newInstance(), clazz.newInstance());
- } catch (Exception e) {
- return null ;
- }
- }
初学者对Java反射不熟悉不用着急,这里只要知道不能实例化类型参数即可,同理,不能实例化一个泛型数组,如
[code04]
- public static <T> T[] maxTwo(T[] values) {T[] array = new T[2];}
泛型构建数组是不合法的,因为这么构建在擦除之后构造的永远是new Object[2],这不是我们所希望的结果。而且这样会导致一些运行时错误。为了更进一步说明隐患问题,来看看下面代码:
[code05]
- public static <T extends Comparable<T>> T[] maxTwo(T[] array) {
- Object[] result = new Object[2];
- return (T[]) result;
- }
这种方式会产生变异警告:Object[]转换为T[]是没有被检查的。我们来试一试这个调用: maxTwo(new String[] { "5", "7" , "9" });,运行后,发生了类型转换异常,因为方法在调用的时候将Object[]转换为String[],失败的类型转化。怎么解决呢?同样这里可以使用Java发射来解决:
[code06]
- public static <T extends Comparable<T>> T[] maxTwo(T[] array) {
-
- return (T[]) Array.newInstance(array.getClass().getComponentType(), 2) ;
- }
不能声明参数化的数组
Couple<Employee>[] couple = new Couple<Employee>[5] ;这种声明式不合法的,这里面有一个问题还是通过类型擦除机制来解释,类型擦除后couple的类型是Couple[],考虑一下两种赋值方式:
1.couple[0] = "wife"时,编译器会报错,这个很明显的错误,couple每个元素都是Couple类型。
2.couple[0] = new Couple<String>(),类型擦除后这个可以通过数组检测,但仍然是错误的类型,因为couple在声明的时候定义的是Couple<Employee>,所以会出现问题。
如果要存放参数化类型对象的集合,可以考虑使用ArrayList<Couple<Employee>>进行声明,而且Java中建议优先使用集合,这样既安全又有效。
类型参数不能进行类型查询
通常我们会使用if (arg1 instanceof Couple)来判断arg1是否属于Couple类型,有些爱思考的程序员可能会想更具体地去判断arg1类型是属于Couple<T>,指定类型参数,如:
[code07]
- if (arg1 instanceof Couple<Employee>){...}
Java虚拟机中没有泛型概念,所有instanceof 类型查询只查询原始类型,所以code07中的语法在Java中的不支持的。同理,在强制类型转换中也是不允许指定类型参数的,如:Couple<Employee> couple = (Couple<Employee>)arg1; // 错误
不能抛出、不能捕获泛型类实例
在Java中,public class GenericException <T> extends Exception {...}这种泛型类扩展子Throwable是不合法的,不能通过编译器。
不能再catch子句中使用类型参数,如:
[code08]
- public static <T extends Throwable> void doSomething(Class<T> t) {
- try {
-
- } catch (T e) {
- e.printStackTrace();
- }
- }
code08中的代码是不合法的,下面在异常声明中使用类型参数是合法的,例如:
[code09]
- public static <T extends Throwable> void doSomething(T t) throws T {
- try {
-
- } catch (Throwable e) {
- e.printStackTrace();
- throw t;
- }
- }
泛型类中的类型参数不能用于静态上下文中
怎么理解这个呢?看个例子就知道了,如一个泛型单例:
[code10]
- public class Singleton <T>{
-
- private static T instance;
-
- public static T getInstance(){ ..}
- }
上述代码,是无法通过编译器的,怎么解释这个呢?试想假设这个程序可以运行,声明一个Singleton<String>和一个Singleton<Employee>,类型擦除之后都是Singleton,但只包含了一个instance域,这个域不可能同时是String类型的又是Employee类型的,所以假设是不成立的,顺便回顾了一下学校里学的反证法~ ~
类型擦除后引起的冲突
看一个泛型类:
[code11]
- public class NameClash<T> {
- public boolean equals(T value) {
- return false ;
- }
- }
从这个类的定义中来看,存在两个equals方法,一个是自身定义的public boolean equals(T value) {...},一个是从Object继承的public boolean equals(Object obj) {...},但类型擦除以后,前者方法成为了public boolean equals(Object value) {...},而在一个类中同时存在两个方法名和参数一样的方法是不可能的,所以这里引发的冲突是没法通过编译器的。可以通过重新命名方法进行修正。
擦除引起的冲突还体现在另一点上,再看一段错误的代码:
[code12]
- class Calendar implements Comparable<Calendar> {...}
- class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar> {...}
上述代码是非法的,为什么?回顾一下类型擦除后,虚拟机会为Calendar 类合成桥方法,实现了Comparable<Calendar>获得一个桥方法:
public int compareTo (Object o) {return compareTo((Calendar)o);}
而实现了Comparable<GregorianCalendar >在类型擦除后,虚拟机为GregorianCalendar合成一个桥方法:
public int compareTo (Object o) {return compareTo((GregorianCalendar )o);}
这样一来在GregorianCalendar类中存在两个一样的方法,这是不允许的。