java泛型
1. Why ——引入泛型机制的原因
假如我们想要实现一个String数组,并且要求它可以动态改变大小,这时我们都会想到用ArrayList来聚合String对象。然而,过了一阵,我们想要实现一个大小可以改变的Date对象数组,这时我们当然希望能够重用之前写过的那个针对String对象的ArrayList实现。
在Java 5之前,ArrayList的实现大致如下:
1
2
3
4
5
6
|
public class ArrayList { public Object get( int i) { ... } public void add(Object o) { ... } ... private Object[] 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 = new ArrayList<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
|
public class Pair<T, U> { private T first; private U second; public Pair(T first, U second) { this .first = first; this .second = second; } public T getFirst() { return first; } public U getSecond() { return second; } public void setFirst(T newValue) { first = newValue; } public void setSecond(U newValue) { second = newValue; } } |
上面的代码中我们可以看到,泛型类Pair的类型参数为T、U,放在类名后的尖括号中。这里的T即Type的首字母,代表类型的意思,常用的还有E(element)、K(key)、V(value)等。当然不用这些字母指代类型参数也完全可以。
实例化泛型类的时候,我们只需要把类型参数换成具体的类型即可,比如实例化一个Pair<T, U>类我们可以这样:
1
|
Pair<String, Integer> pair = new Pair<String, Integer>(); |
3. 泛型方法
所谓泛型方法,就是带有类型参数的方法,它既可以定义在泛型类中,也可以定义在普通类中。例如:
1
2
3
4
5
|
public class ArrayAlg { public static <T> T getMiddle(T[] a) { return a[a.length / 2 ]; } } |
以上代码中的getMiddle方法即为一个泛型方法,定义的格式是类型变量放在修饰符的后面、返回类型的前面。我们可以看到,以上泛型方法可以针对各种类型的数组调用,在这些数组的类型已知切有限时,虽然也可以用过重载实现,不过编码效率要低得多。调用以上泛型方法的示例代码如下:
1
2
|
String[] strings = { "aa" , "bb" , "cc" }; String middle = ArrayAlg.getMiddle(names); |
4. 类型变量的限定
在有些情况下,泛型类或者泛型方法想要对自己的类型参数进一步加一些限制。比如,我们想要限定类型参数只能为某个类的子类或者只能为实现了某个接口的类。相关的语法如下:
<T extends BoundingType>(BoundingType是一个类或者接口)。其中的BoundingType可以多于1个,用“&”连接即可。
5.Java泛型中的标记符含义
E - Element (在集合中使用,因为集合中存放的是元素)
T - Type(Java 类)
K - Key(键)
V - Value(值)
N - Number(数值类型)
? - 表示不确定的java类
S、U、V - 2nd、3rd、4th types
6. 注意事项
(1)不能用基本类型实例化类型参数
也就是说,以下语句是非法的:
1
|
Pair< int , int > pair = new Pair< int , int >(); |
不过我们可以用相应的包装类型来代替。
(2)不能抛出也不能捕获泛型类实例
泛型类扩展Throwable即为不合法,因此无法抛出或捕获泛型类实例。但在异常声明中使用类型参数是合法的:
1
2
3
4
5
6
7
8
|
public static <T extends Throwable> void doWork(T t) throws T { try { ... } catch (Throwable realCause) { t.initCause(realCause); throw t; } } |
(3)参数化类型的数组不合法
在Java中,Object[]数组可以是任何数组的父类(因为任何一个数组都可以向上转型为它在定义时指定元素类型的父类的数组)。考虑以下代码:
1
2
3
|
String[] strs = new String[ 10 ]; Object[] objs = strs; obj[ 0 ] = new Date(...); |
在上述代码中,我们将数组元素赋值为满足父类(Object)类型,但不同于原始类型(Pair)的对象,在编译时能够通过,而在运行时会抛出ArrayStoreException异常。
基于以上原因,假设Java允许我们通过以下语句声明并初始化一个泛型数组:
1
|
Pair<String, String>[] pairs = new Pair<String, String>[ 10 ]; |
那么在虚拟机进行类型擦除后,实际上pairs成为了Pair[]数组,我们可以将它向上转型为Object[]数组。这时我们若往其中添加Pair<Date, Date>对象,便能通过编译时检查和运行时检查,而我们的本意是只想让这个数组存储Pair<String, String>对象,这会产生难以定位的错误。因此,Java不允许我们通过以上的语句形式声明并初始化一个泛型数组。
可用如下语句声明并初始化一个泛型数组:
1
|
Pair<String, String>[] pairs = (Pair<String, String>[]) new Pair[ 10 ]; |
(4)不能实例化类型变量
不能以诸如“new T(…)”, “new T[...]“, “T.class”的形式使用类型变量。Java禁止我们这样做的原因很简单,因为存在类型擦除,所以类似于”new T(…)”这样的语句就会变为”new Object(…)”, 而这通常不是我们的本意。我们可以用如下语句代替对“new T[...]“的调用:
1
|
arrays = (T[]) new Object[N]; |
(5)泛型类的静态上下文中不能使用类型变量
注意,这里我们强调了泛型类。因为普通类中可以定义静态泛型方法,如上面我们提到的ArrayAlg类中的getMiddle方法。关于为什么有这样的规定,请考虑下面的代码:
1
2
3
4
5
6
|
public class People<T> { public static T name; public static T 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
|
public static void printName(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 = new Pair<Student>(student1, student2); Pair<? extends People> wildchards = students; wildchards.setFirst(people1); |
以上代码的第三行会报错,因为wildchards是一个Pair<? extends People>对象,它的setFirst方法和getFirst方法是这样的:
1
2
|
void setFirst(? extends People) ? extends People 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方法都是非法的。