泛型是Java
中一项十分重要的特性,在Java 5
版本被引入,在日常的编程过程中,有很多依赖泛型的场景,尤其是在集合容器类的使用过程中,更是离不开泛型的影子。
泛型的作用
泛型提供的功能有:参数化类型,以及编译期类型检查。
1 参数化类型
在方法的定义中,方法的参数称为形参,在实际调用方法时传递实参。泛型的使用中,可以将类型定义为一个参数,在实际使用时再传递具体类型。将泛型这种使用方式称之为参数化类型。
在集合类的使用中,若不使用泛型,则需要对每一种元素类型设计相同的集合操作,例如:
class ListInteger{
//...
}
class ListDouble{
//...
}
通过泛型的使用,可以避免这种重复定义的现象,定义一套集合操作,来应对所有元素类型,例如:
class List{
//...
}
在使用中传递不同的元素类型给List
即可。
这里使用的字符
E
并无特殊含义,只是为了便于理解而已。泛型中通常使用的字符及表示意义为:
K:
键值对中的key
V:
键值对中的value
E:
集合中的element
T:
类的类型type
2 编译期类型检查
对于集合ArrayList
而言,若不指定具体元素类型,则使用过程中可能出现以下情况:
List list = new ArrayList();
list.add("abc");
list.add(123);
for (Object obj : list) {
String e = (String) obj;//ClassCastException
}
这段代码在编译期没问题,运行时会报出java.lang.ClassCastException
。
这种对集合的使用方式存在两个问题:一是add
添加元素时,因为元素声明为Object
类型,任意类型元素都可以添加到集合中,所以在添加元素时需要使用者自己注意选择的元素类型;二是get
取元素时需要强制类型转换,需要开发人员记住操作的元素类型,否则可能抛出ClassCastException
异常。
在声明集合时指定元素类型则可以避免以上两种问题:
List list = new ArrayList();
list.add("abc");
//list.add(123); compile error
for (String obj : list) {
String e = obj;
}
通过泛型的使用,指定集合元素的类型,则可以在编译期就进行元素类型检查,并且get
获取元素时无需进行强制类型转换。
这里称获取元素无需进行强制类型转换,其实并不准确,严格来讲,使用泛型在进行获取元素操作时,进行的是隐式类型转换,所以仍然存在强制类型转换的操作。
ArrayList
中的隐式类型转换:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
泛型的使用
泛型可以应用于定义泛型类、泛型接口和泛型方法。
1 泛型类
泛型类的定义方式较为简单,通过将类型抽象为参数,附加在类名称后,即可完成泛型类的定义,示例:
public class Test {
public static void main(String[] args) {
User user = new User<>();
user.setAttribute(123);
// user.setAttribute("abc");compile error
Integer attribute = user.getAttribute();
}
}
class User {
private T attribute;
public User() {
}
public T getAttribute() {
return this.attribute;
}
public void setAttribute(T attribute) {
this.attribute = attribute;
}
}
通过使用泛型类,可以在编译期进行参数类型检查,并且使用时无需进行强制类型转换。
2 泛型接口
泛型接口的使用与泛型类较为相似,在接口名称后添加表示类型的字符即可,示例:
interface Person {
T getAttribute();
void setAttribute(T attribute);
}
3 泛型方法
在前面的泛型类中定义的如下方法:
public T getAttribute() {
return this.attribute;
}
public void setAttribute(T attribute) {
this.attribute = attribute;
}
虽然使用了参数化类型,但是并不算是泛型方法,因为这些方法中使用的参数类型是泛型类定义的。泛型方法中定义了自己使用的类型,示例:
public void genericsMethod(T parameter){
//...
}
泛型与继承
在泛型的使用中,关于继承方面需要注意,示例:
public class Test {
public static void main(String[] args) {
A aNumber = new A<>();
A aInteger = new A<>();
// aNumber = aInteger; compile error
System.out.println(aNumber.getClass() == aInteger.getClass()); // true
}
static class A{}
}
虽然Integer
是Number
的子类型,但是A
并不是A
的子类型。
事实上,编译器会在编译阶段进行类型检查后,会擦除泛型的类型信息,也就是说在运行期
A
和A
是同一个类。
对于泛型容器类List
,在进行泛型擦除后,记录的元素类型为其声明的最左边父类型,此处即为Object
类型,示例:
public class Test {
public static void main(String[] args) throws Exception {
List integers = new ArrayList<>();
integers.getClass().getDeclaredMethod("add", Object.class).invoke(integers, "abc");
}
}
代码在编译期和运行期都没问题,在编译生成的.class
文件中,Integer
元素类型被擦除后,容器的元素类型记录为Object
类型。
泛型使用中的继承定义方式如下:
public class Test {
public static void main(String[] args) {
A a = new A<>();
B b = new B<>();
a = b;
}
}
class A{}
class B extends A{}
在继承关系中使用同一个参数类型,以此实现泛型类的继承。在JDK
中ArrayList
、List
与Collection
采用的就是这种方式。
但是这种继承方式依然不能满足前面提到的使用场景,例如如下使用List
方式:
public class Test {
public static void main(String[] args) {
List numberList = new ArrayList<>();
List integerList = new ArrayList<>();
// numberList = integerList; compile error
}
}
虽然Integer
是Number
的子类型,但List
却不是List
的子类型,问题与前面的示例中相同。
通配符
通配符号?
是一种实参类型,表示类型不确定的意思,或者表示任意一种类型,选择?
作为类型的目的是为了匹配更大范围的类型,所以这里?
是一种具体的类型。
这里称
?
类型不确定,又称?
是一种具体的类型,这种说法是相对于前面的类型参数T
而言的,T
表示类型形参,使用时被替代为传入的具体类型,而?
就是一种具体类型,不会被别的具体类型替代。
在前面有关泛型的继承关系中,遇到List
不是List
的子类型问题,可以使用通配符号?
表示具体类型,这样则可以匹配任意的参数类型,示例:
public class Test {
public static void main(String[] args) {
List> numberList = new ArrayList<>();
List integerList = new ArrayList<>();
numberList = integerList;
}
}
既然?
可以表示所有类型,当然也可以表示Integer
类型,所以代码可以编译通过。
在平常的使用中,类型的选择范围并非如此随意,更多时候在定义泛型类、接口或方法时,限定了能够使用的类型范围。
1 限定上界
使用extends
关键字限定参数类型能够选择的上界,示例:
public class Test {
public static void main(String[] args) {
GenericsClass integerObj = new GenericsClass<>();
// GenericsClass stringObj = new GenericsClass<>(); compile error
Test.genericsMethod1(new ArrayList());
// Test.genericsMethod1(new ArrayList()); compile error
Test.genericsMethod2(new ArrayList());
// Test.genericsMethod2(new ArrayList()); compile error
}
static class GenericsClass{
//...
}
static void genericsMethod1(List list) {
// list.add(1); compile error
}
static void genericsMethod2(List extends Number> list) {
// list.add(1); compile error
}
}
GenericsClass
类中通过
限定参数类型为Number
的子类型,genericsMethod1、genericsMethod2
同样使用extends
关键字限定类型上界。
genericsMethod1
与genericsMethod2
分别使用了T
和?
作为参数类型符号,在限定类型范围上,两者作用相同。不同之外在于,使用T
表示类型形参,在genericsMethod1
方法体内可以引用T
类型相关的操作,但是?
则无法引用。
这里需要注意一点,若使用具有上界的泛型来作为集合的元素类型时,因为此时无法确定集合的元素类型,所以无法向集合中添加元素,示例:
static void genericsMethod1(List list) {
// list.add(1); compile error
}
static void genericsMethod2(List extends Number> list) {
// list.add(1); compile error
}
2 限定下界
使用super
关键字限定参数类型能够选择的下界,示例:
public class Test {
public static void main(String[] args) {
Test.genericsMethod2(new ArrayList());
// Test.genericsMethod2(new ArrayList()); compile error
}
// static class GenericsClass super Integer>{ compile error
// //...
// }
// static void genericsMethod1(List list) { compile error
// //...
// }
static void genericsMethod2(List super Integer> list) {
list.add(1);
}
}
由示例可知, super Integer>
的形式限定元素的下界为Integer
类型,则此时可以对集合进行添加Integer
元素操作。
由示例同样可知,使用super
关键字限定参数类型下界,与使用extends
关键字限定参数类型的上界有所不同,最大的区别就是:类型形参T
不能与super
关键字配合使用。若可以配合使用,则会存在以下问题:
表示T
类为Integer
的子类型,则T
类型属性可以访问Integer
类型中的部分属性;
的描述表示T
类为Integer
的父类,则T
类型属性不确定其父类为何类,也可能为Serializable
,那么此时将不具备任何属性,因为不确定,所以无法进行操作;
在编译时进行类型擦除后,则T
属性将默认为extends
继承的父类中最左边一个,这里即为Integer
;而
描述的类,在进行类型擦除后将无法确定其类型。
根据以上两点,在类的描述中,不能使用
的形式限定参数类型的下界。
通配符的上下界使用有
PECS(producer extends, consumer super)
原则,producer
可以根据上界进行元素读取,但是不确定类型,所以无法添加元素;consumer
可以根据下界进行元素添加,但是不确定类型,所以无法读取元素。
泛型数组
在普通数组的使用中,存在如下的情况:
public class Test {
public static void main(String[] args) {
Integer[] integers = new Integer[5];
Object[] objects = integers;
objects[0] = "abc";
}
}
这段代码在编译期是没问题的,在运行时会报出ArrayStoreException
异常。这种情况称之为数组的协变(covariant)
,即S
类型为T
类型的子类型,则S
类型数组为T
类型数组的子类型。
为了避免这种协变的情况发生,Java
禁止创建具体类型的泛型数组,否则对于泛型数组有如下情况,示例来源Java 指导手册:
// Not really allowed.
List[] lsa = new List[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List li = new ArrayList();
li.add(new Integer(3));
// Unsound, but passes run time store check
oa[1] = li;
// Run-time error: ClassCastException.
String s = lsa[1].get(0);
如果Java
中允许创建具体类型的泛型数组,则以上代码在编译期通过类型检查,在运行期获取元素时会报出ClassCastException
异常,即无法通过泛型元素的隐式类型转换。
Java
虽然禁止创建具体类型的泛型数组,但并不禁止创建通配符形式的数组,如下所示,示例来源Java 指导手册:
// OK, array of unbounded wildcard type.
List>[] lsa = new List>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List li = new ArrayList();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);
虽然发生运行期错误,但是因为通配符的使用,所以在获取元素时,需要进行显示类型转换,也就是将元素的类型操作交给开发人员进行控制。
参考
Type Parameters
Difference between super T> and extends T> in Java
The Java™ Tutorials