泛型是一种编程范式(Programming Paradigm),是为了效率和重用性产生的。由Alexander Stepanov(C++标准库主要设计师)和David Musser(伦斯勒理工学院CS名誉教授)首次提出,自实现始,就成为了ANSI/ISO C++重要标准之一。
Java自1.5版本开始提供泛型,其本质是一个参数化的类型,那么,何谓参数化?
参数是一个外部变量。设想一个方法,其参数的名称和实际的数值是外部传入的,那么,该参数的类型是否也作为一个参数,在运行时决定呢?这就是泛型的作用。参考如下代码:
List<String> list = new ArrayList<String>(); list.add(1); |
在第2行,会抛出编译期错误。
The method add(int, String) in the type List<String> is not applicable for the arguments (int)
这就是因为,list在声明时定义了String为自己需要的类型,而1是一个整型数。在上面的例子中,以下几种添加方式都是合法的:
list.add("字符串"); list.add(new String()); String str="字符串"; list.add( str); |
在J2SE1.5之前的版本中,Java没有办法显式的对容器进行编译期内容限制,在没有注释或者文档说明的情况下,很容易出现运行时错误。下面举一个错误的例子:
ArrayList list = new ArrayList(); list.add(0); list.add(1); list.add(‘2‘); list.add(3); //输出list内容 System.out.println(list); //遍历输出list内容 for (int i = 0, len = list.size(); i < len; i++) { Integer object = (Integer) list.get(i); System.out.println(object); } |
输出结果如下所示:
[0, 1, 2, 3] 0 1 Exception in thread "main" java.lang.ClassCastException: java.lang.Character cannot be cast to java.lang.Integer at capter3.generic.Generic3_1.test2(Generic3_1.java:28) at capter3.generic.Generic3_1.main(Generic3_1.java:17) |
可以看到,直接输出list的时候,int类型的1和char类型的2是看不出区别的,假设忽略了这点,直接转为Integer来使用的时候,就抛出了强制转化异常。
除了会造成异常外,还可以思考一个问题,如果没有泛型,那么list.get(int)方法返回的始终是个Object,那么要如何在运行时之外确定它的类型呢?
综上,已经证明了泛型存在的必要性,它提供了以下能力:
1、避免代码中的强制类型转换;
2、限定类型,在编译时提供一个额外的类型检查,避免错误的值被存入容器;
3、实现一些特别编程技巧。比如:提供一个方法用于拷贝对象,在不提供额外方法参数的情况下,使返回值类型和方法参数类型保持一致。
泛型的分类
根据泛型使用方式的不同,可分为泛型接口、泛型类和泛型方法。它们的定义如下:
1、泛型接口:在接口定义的接口名后加上<泛型参数名>,就定义了一个泛型接口,该泛型参数名的作用域存在于接口定义和整个接口主体之内;
2、泛型类:在类定义的类名后加上<泛型参数名>,就定义了一个泛型类,该泛型参数名的作用域存在于类定义和整个类主体之内;
3、方法类:在方法的返回值之前加上<泛型参数名>,就定义了一个泛型方法,该泛型参数名的作用域包括方法返回值,方法参数,方法异常,以及整个方法主体。
下面通过一个例子来分别介绍这几种泛型的定义方法,示例代码如下:
/** * 在普通的接口后加上<泛型参数名>即可以定义泛型接口 */ interface GenericInterface<T> { } /** * 在类定义后加上<泛型参数名>即可定义一个泛型类,注意后面这个GenericInterface<T>,这里是使用类的泛型参数,而非定义。 */ class GenericClass<T> implements GenericInterface<T>{ /** * 在返回值前定义了泛型参数的方法,就是泛型方法。 */ public <K, E extends Exception> K genericMethod(K param) throws E { java.util.List<K> list = new ArrayList<K>(); K k = null; return null; } } |
在上例中,class GenericClass<T> implements GenericInterface<T>中有两个地方使用了<T>,它们是同一个概念吗?为了回答这个问题,下面给出几个基本概念,通过对这些基本概念的掌握,将可以解决大部分类似的泛型问题。
a、类(接口)的泛型定义位置紧接在类(接口)定义之后,可以替代该类(接口)定义内部的任意类型。在该类(接口)被声明时,确定泛型参数。
b、方法的泛型定义位置在修饰符之后返回值之前,可以替代该方法中使用的任意类型,包括返回值、参数以及局部变量。在该方法被调用时,确定泛型参数,一般来说,是通过方法参数来确定的泛型参数。
c、<>的出现有两种情况,一是定义泛型,二是使用某个类\接口来具象化泛型。
根据上面介绍的几个基本概念,再来分析class GenericClass<T> implemenets GenericInterface<T>这句代码。可知,class GenericClass是类的定义,那么第一个<T>就构成了泛型参数的定义,而接口GenericInterface是定义在别处的,该代码位置是对此接口的引用,所以,第二个<T>则是使用泛型T来规范GenericInterface。
引申:
如果泛型方法是没有形参的,那么是否还有其它方法来指定类型参数?
答案:有方法指定,但是这个语法并不常见,实现代码如下:
GenericClass<String> gc=new GenericClass<String>(); gc.<String>genericMethod(null); |
可以看到这里出现一个很特别的代码形式,gc.genericMethod(null)中间多出了一个<String>,这就是为genericMethod方法进行泛型参数定义了。
有界泛型的定义
有界泛型有三个非常重要的关键字:?,extends和 super。
a) “?”,表示通配符类型,用于表达任意类型,需要注意的是,它指代的是“某一个任意类型”,但并不是Object;(注意,这里并不是准确的表达,具体的内容将在“泛型的不变性”相关小节来讨论)
示例代码如下:
class Parent { } class Sub1 extends Parent { } class Sub2 extends Parent { } class WildcardSample<T> { T obj; void test() { WildcardSample<Parent> sample1 = new WildcardSample<Parent>(); //编译错误 WildcardSample<Parent> sample2 = new WildcardSample<Sub1>(); //正常编译 WildcardSample<?> sample3 = new WildcardSample<Parent>(); WildcardSample<?> sample4 = new WildcardSample<Sub1>(); WildcardSample<?> sample5 = new WildcardSample<Sub2>(); sample1.obj = new Sub1(); // 编译错误 sample3.obj = new Sub1(); } } |
这些代码体现了通配符的作用。
1、sample2声明里使用Parent作为泛型参数的时候,不能指向使用Sub1作为泛型参数的实例。因为编译器处理泛型时严格的按照定义来执行,Sub1虽然是Parent的子类,但它毕竟不是Parent。
2、sample3~5声明里使用?作为泛型参数的时候,可以指向任意WildcardSample实例。
3、sample1.obj可以指向Sub1实例,这是因为obj被认为是Parent,而Sub1是Parent的子类,满足向上转型。
4、sample3.obj不能指向Sub1实例,这是因为通配符是“某个类型”而并不是Object,所以Sub1并不是?的子类,抛出编译期错误。
5、虽然有如此多的限制,但是你还是可以以Object类型来读取sample3.obj,毕竟不论通配符是什么类型,Object一定是它的父类。
引申:设想如果sample3.obj = new Sub1()可以编译通过,事实上期望的sample3类型是WildcardSample<Object>,这样的话,通配符就失去意义了。而在实际应用中,这并不光是失去意义这样简单的事,还会引起执行异常。这里提供一个例子帮助理解:
WildcardSample<Parent> sample1 = new WildcardSample<Parent>(); sample1.obj = new Parent(); WildcardSample<?> extSample = sample1; //原本应当被限定为Parent类型,这里使用了String类型,必须抛出异常。 extSample.obj = new String(); |
b) extends在泛型里不是继承,而是定义上界的意思,如T extends UpperBound,UpperBound为泛型T的上界,也就是说T必须为UpperBound或者它的子类;
泛型上界可以用于定义以及声明代码处,不同的位置使用的时候,它的作用于使用方法都有所不同,示例代码如下:
/** * 有上界的泛型类 */ class ExtendSample<T extends Parent> { T obj; /** * 有上界的泛型方法 */ <K extends Sub1> T extendMethod(K param) { return this.obj; } } public class Generic3_1_2_b { public static void main(String[] args) { ExtendSample<Parent> sample1 = new ExtendSample<Parent>(); ExtendSample<Sub1> sample2 = new ExtendSample<Sub1>(); ExtendSample<? extends Parent> sample3 = new ExtendSample<Sub1>(); ExtendSample<? extends Sub1> sample4; // 编译错误 sample4 = new ExtendSample<Sub2>(); // 编译错误 ExtendSample<? extends Number> sample5; sample1.obj = new Sub1(); // 编译错误 sample3.obj = new Parent(); } } |
这个例子中使用了一个具备上界的泛型方法和一个具备上界的泛型类,它们体现了extends在泛型中的应用:
1、在方法\接口\类的泛型定义时,需要使用泛型参数名(比如T或者K)。
2、在声明位置使用泛型参数时,需要使用通配符,意义是“用来指定类的上界(该类或其子类)”。
就算加上了上界,使用通配符来定义的对象,也是只能读,不能写。理由在通配符相关小节已经论证过,不再赘述。
c) super关键字用于定义泛型的下界。如T super LowerBound,则LowerBound为泛型T的下界,也就是说T必须为LowerBound或者它的父类;
泛型下界只能应用于声明代码处,表示泛型参数一定是指定类或其父类。
参考以下代码:
class SuperSample<T> { T obj; } public class Generic3_1_2_c { public static void main(String[] args) { SuperSample<? super Parent> sample1 = new SuperSample<Parent>(); // 编译错误 SuperSample<? super Parent> sample2 = new SuperSample<Sub1>(); SuperSample<? super Sub1> sample3 = new SuperSample<Parent>(); sample1.obj = new Sub1(); sample1.obj = new Sub2(); sample1.obj = new Parent(); sample3.obj = new Sub1(); // 编译错误 sample3.obj = new Sub2(); // 编译错误 sample3.obj = new Parent(); } } |
在该示例中,可以注意到:
1、sample1.obj一定是Parent或者Parent的父类,那么,Sub1\Sub2\Parent都能满足向上转型。
2、sample3.obj一定是Sub1或者Sub1的父类,Parent和Sub2无法完全满足条件,所以抛出了异常。
引申:思考一个问题,在上面的例子里sample1.obj是什么类型?
答案: ? extends Parent,也就是说,没有类型。
通过对上述现象的分析可知:当使用extends上界时,所有以该泛型参数作为形参的方法,都不可用,当使用super下界时,所有以该泛型参数作为返回值的方法,只能以Object类型来引用。
思考:<? extends T>和<? super T>有哪些区别?
定义复杂的泛型
复杂的泛型也是由简单的泛型组合起来的,需要掌握下面几个概念:
1、多个泛型参数定义由逗号隔开,就像<T,K>这样。
2、同一个泛型参数如果有多个上界,则各个上界之间用&符号连接。
3、多个上界类型里最多只能有一个类,其他必须为接口,如果上界里有类,则必须放置在第一位。
结合以上的知识,则可以灵活的组合出复杂的泛型声明来。参考以下代码:
class A { } class B extends A { } class C extends B { } /** * 这是一个泛型类 */ class ComplexGeneric<T extends A, K extends B & Serializable & Cloneable> {...} |
通过上面代码可以看出,ComplextGeneric 类具备两个泛型参数<T,K>,其中T具备上界A,换言之,T一定是A或者其子类;K具备三个上界,分别为类B,接口 Serializable和Cloneable,换言之,K一定是B或者其子类,并且实现了Serializable和Cloneable。
复杂的泛型为更规范更精确的设计提供了可能性。
引申:前面说过,在运行时,泛型会被处理为上界类型。也就是说,ComplextGeneric在其内部用到泛型T的时候,反射会把它当成A类来处理(需要注意的是,在字节码里,还是当作Object处理),那么,反射用到泛型K的时候呢?答案是,会把它当成上界定义的第一个上界处理,在当前例子是,也就是B这个类。
知道了这个有什么意义呢?
设想一个方法 <T extends A> void method(T t);
如果需要反射获取它,必须同时知道方法名和参数类型。这时候,使用Object是找不到它的,只能通过A类来获取。