1. 泛型简介
在泛型(Generics)出现之前,下面的代码很常见:
- public static void main(String[] args) {
- List list = new ArrayList();
- list.add("abc");
- list.add("def");
- System.out.println((String)list.get(0));
- }
List里面的内容是Object类型的,因此可以存入或获取任何类型的对象,但是这种代码存在两个问题:
- 当一个对象放入集合时,集合不会记住此对象的类型,当再次从集合中取出此对象时,该对象的编译类型变成了Object;
- 运行时需要手动强制转换类型为具体目标类型,可能会出现java.lang.ClassCastException类型转换异常。
在泛型出现之后,可以在一定程度上避免上面的两个问题:
- public static void main(String[] args) {
- List<String> list = new ArrayList<String>();
- list.add("abc");
- list.add("def");
- System.out.println(list.get(0));
- }
泛型的引入极大地增强了Java语言的类型功能,带来了很多的好处:
- 类型安全。类型错误现在在编译期间就被捕获到了,而不是在运行时当作java.lang.ClassCastException展示出来,将类型检查从运行时挪到编译时有助于开发者更容易找到错误,并提高程序的可靠性;
- 消除了代码中许多的强制类型转换,增强了代码的可读性;
- 为较大的优化带来了可能。
泛型是JDK 1.5的一项新增特性,类似于C++语言的模板(Template),它的本质是参数化类型(Parametersized Type)的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
2. 泛型的类型擦除
在介绍泛型的使用之前,我们先看一个泛型相关的重要概念:泛型擦除,也叫做泛型的类型擦除
Java语言中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<Integer>
与ArrayList<String>
就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
我们可以观察下面的代码:
- public static void main(String[] args) {
- List<String> stringList = new ArrayList<String>();
- List<Integer> integerList = new ArrayList<Integer>();
- System.out.println(stringList.getClass() == integerList.getClass()); // true
- }
从输出结果可知ArrayList<String>
和ArrayList<Integer
对象的类型其实是相同的,这意味着,通过改变泛型的方式试图定义不同的重载方法也是不可以的,如下面两个重载方法:
- public void sort(List<String> list);
- public void sort(List<Integer> list);
这种方式的重载是不被允许的,编译器也会报错。
注1:在Sun JDK 1.6中,可以通过给这两个方法指定不同的返回值类型即可通过编译,这是由于虽然方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说,两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个Class字节码文件中的。如:
- public String sort(List<String> list);
- public Integer sort(List<Integer> list);
注2:正是由于Java的泛型是伪泛型,运行时期还是存在着强制类型转换,因此会有一定的性能损耗的影响。
3. 泛型类
泛型类可以提供给使用者一种限定类型的方式,最常见的例子便是Java集合类如ArrayList、HashSet、HashMap等类大量运用到了泛型。下面是一个泛型类的简单定义:
- package com.coderap.generic;
- // 泛型类
- class GenericClass<T> {
- public T value;
- public GenericClass(T value) {
- this.value = value;
- }
- public T getValue() {
- return value;
- }
- }
- public class GenericTest {
- public static void main(String[] args) {
- GenericClass<Integer> integerGeneric = new GenericClass(123);
- Integer integerValue = integerGeneric.getValue();
- System.out.println("integerValue = " + integerValue); // integerValue = 123
- GenericClass<String> stringGeneric = new GenericClass("abc");
- String stringValue = stringGeneric.getValue();
- System.out.println("stringValue = " + stringValue); // stringValue = abc
- GenericClass<Float> floatGeneric = new GenericClass(3.14f);
- Float floatValue = floatGeneric.getValue();
- System.out.println("floatValue = " + floatValue); // floatValue = 3.14
- }
- }
需要注意的是,在给泛型类指定泛型创建相应的对象时,泛型类型不能是基本类型如int、long、short等。
4. 泛型接口
泛型接口也非常常见,比如java.lang包提供的Comparable接口就是一个泛型接口:
- public interface Comparable<T> {
- public int compareTo(T o);
- }
定义泛型接口的方式与定义泛型类类似,但在使用时则不同。当我们自定义一个类实现了某个泛型接口时,该类存在两种情况:
- 具体类实现泛型接口,但不明确泛型类型;这种方式实现的具体类,泛型类型的确定会放在类被实例化的过程中,此时我们定义的类应该保留泛型的定义,如下:
- // 实现了泛型接口的泛型类,定义时不明确泛型类型
- class GenericClass<T> implements Comparable<T> {
- public T value;
- public GenericClass(T value) {
- this.value = value;
- }
- public T getValue() {
- return value;
- }
- @Override
- public int compareTo(T o) {
- return 0;
- }
- }
可以看出,在这种方式下,具体类类名后面依旧需要添加与接口泛型标示符一致的<T>
用于表明此时具体类依旧是一个泛型类。
- 在实现泛型接口的具体类的定义时明确泛型类型;这种方式下,具体类的泛型类型在声明时就确定了,实例化过程中无法更改。如下:
- // 实现了泛型接口的泛型类,定义时明确泛型类型
- class GenericClass implements Comparable<String> {
- public String value;
- public GenericClass(String value) {
- this.value = value;
- }
- public String getValue() {
- return value;
- }
- @Override
- public int compareTo(String o) {
- return 0;
- }
- }
在这种方式下,具体类中实现的泛型接口中的方法已经明确了对应的泛型类型。
5. 泛型通配符
在泛型的使用中,List<Object>
不是List<String>
的父类型,List<Number>
不是List<Integer>
的父类型,试图用以下方式赋值是不允许的:
- public static void main(String[] args) {
- List<Number> numberList = new ArrayList<Number>();
- List<Integer> integerList = new ArrayList<Integer>();
- numberList = integerList; // #Error: Type mismatch: cannot convert from List<Integer> to List<Number>
- }
第4行将报错Type mismatch: cannot convert from List<Integer> to List<Number>
。针对这个问题,Java给开发者提供了通配符?
:
- public static void main(String[] args) {
- List<String> stringList = new ArrayList<String>();
- List<Integer> integerList = new ArrayList<Integer>();
- printList(stringList);
- printList(integerList);
- }
- private static void foreach(List<?> list) {
- for (Object o : list)
- System.out.println(o);
- }
<?>
是泛型类型通配符,表示是任何泛型的父类型,这样List<Object>
、List<String>
这些都可以传递进入foreach(List<?> list)
方法中,注意这里的参数不能写成List<E>
,这样就报错了,E未定义。当然<?>
也可以不加,不过如果传递一个List<E>
给List,相当于传递一个只承诺将它当作List(原始类型)的方法,这将会破坏使用泛型的类型安全,编译器会发出警告。
注意,此处?
是类型实参,而不是类型形参 。它是一种真实的类型,可以把?
看成所有类型的父类。另外,使用类型通配符,只能从中检索元素,不能添加元素。
6. 泛型方法
泛型方法和泛型类中具有泛型特性的方法并不一样。泛型类是在实例化类的时候指明泛型的具体类型,而泛型方法是在调用方法的时候指明泛型的具体类型。
我们先观察两个方法:
- class GenericClass<T> {
- public T value;
- public GenericClass(T value) {
- this.value = value;
- }
- public T getValue() {
- return value;
- }
- public <T> T genericMethod(Class<T> clazz) throws InstantiationException, IllegalAccessException {
- T instance = clazz.newInstance();
- return instance;
- }
- }
在上面的泛型方法的定义中,getValue()
虽然返回值定义为了泛型类型T,但它并不是泛型方法;而genericMethod(Class<T> clazz)
才是泛型方法,其中的<T>
表示声明此方法为泛型方法,而T
表示方法的返回值类型。只有在定义中声明了<T>
的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。<T>
表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。与泛型类的定义一样,此处T
可以随便写为任意标识。
需要注意的是,就算类没有被泛型化,它其中的方法也可以是泛型方法,下面的代码是正确的:
- class NormalClass {
- public <T> T genericMethod(Class<T> clazz) throws InstantiationException, IllegalAccessException {
- T instance = clazz.newInstance();
- return instance;
- }
- }
下面的代码对常见的出现泛型定义的方法做了详细的对比:
- // 泛型类
- class GenericClass<T> {
- public T value;
- public GenericClass(T value) {
- this.value = value;
- }
- /**
- * 普通方法,虽然在方法中使用了泛型,但是这并不是一个泛型方法
- * 这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型
- */
- public T getValue() {
- return value;
- }
- /**
- * 这个是泛型方法,其中的E可以换做任何其他的描述符
- * 甚至是类的泛型类型T,此时这两个T描述符代表的并非是同一种泛型类型
- */
- public <E> E genericMethod(Class<E> clazz) throws InstantiationException, IllegalAccessException {
- E instance = clazz.newInstance();
- return instance;
- }
- // 普通方法,只是使用了Generic<Number>这个泛型类型做形参
- public void foreach(List<Number> list) {
- for (Number number : list) {
- System.out.println(number);
- }
- }
- // 普通方法,只不过使用了Generic<?>这个泛型类型做形参
- public void iterate(List<?> list) {
- for (Object o : list) {
- System.out.println(o);
- }
- }
- }
需要注意的是,泛型类型也可以用作可变参数:
- public <T> void foreach(T... args){
- for(T t : args){
- System.out.println(t);
- }
- }
7. 泛型约束
在使用泛型的时候,我们还可以约束传入的泛型类型的上下边界。通过extends
约束上界,super
关键字约束下界。上界约束可以规定传入的泛型必须是某个类型的子类型,而下界约束用于规定传入的泛型必须是否个类型的父类型。
假设目前有A、B、C三个类,B继承于A,C继承于B:
- class A {
- }
- class B extends A {
- }
- class C extends B {
- }
- 定义class的时候不能用通配符
?
或下界约束super
,只可以使用extends
关键字进行上界约束,代码如下:
- // 泛型类
- class GenericClass<T extends B> {
- T value;
- public GenericClass(T value) {
- this.value = value;
- }
- public T getValue() {
- return value;
- }
- }
此时在使用GenericClass,只能传入B类型或B的子类C类型为泛型,传入A类型作为泛型将会报错:
- public class GenericTest {
- public static void main(String[] args) {
- // GenericClass<A> aGenericClass = new GenericClass(new A());
- // System.out.println("aGenericClass = " + aGenericClass.getValue().getClass());
- GenericClass<B> bGenericClass = new GenericClass(new B());
- System.out.println("bGenericClass = " + bGenericClass.getValue().getClass()); // bGenericClass = class com.coderap.generic.B
- GenericClass<C> cGenericClass = new GenericClass(new C());
- System.out.println("cGenericClass = " + cGenericClass.getValue().getClass()); // cGenericClass = class com.coderap.generic.C
- }
- }
- 作为方法的参数,泛型可以使用
? extends B
或者? super C
,前者表示实际类型只可以是B类对象或B的子类对象,后者表示实际类型只可以是C类对象或C的父类对象,以下两种写法都是正确的:
- public static void main(String[] args) {
- List<? super B> blist = new ArrayList<B>();
- blist.add(new C());
- blist.add(new B());
- iterate(blist);
- // List<? extends B> clist = new ArrayList<B>(); // 这种写法添加下面两个元素时会报错
- // clist.add(new B()); // 报错
- // clist.add(new C()); // 报错
- // 下面的写法可行
- List<B> clist = new ArrayList<B>();
- clist.add(new B());
- clist.add(new C());
- foreach(clist);
- }
- public static void foreach(List<? extends B> list) {
- for (B b : list) {
- System.out.println(b);
- }
- }
- public static void iterate(List<? super C> list) {
- for (Object o : list) {
- System.out.println(o);
- }
- }
- 作为局部变量的参数,泛型可以使用
? extends B
或者? super C
,不过前者没有什么意义,后者表示只可以传以C为父类的对象,所以以下的写法是正确的:
- List<? super B> blist = new ArrayList<B>();
- blist.add(new C());
- blist.add(new B());
- // List<? extends B> clist = new ArrayList<B>(); // 这种写法添加下面两个元素时会报错
- // clist.add(new B()); // 报错
- // clist.add(new C()); // 报错
- // 下面的写法可行
- List<B> clist = new ArrayList<B>();
- clist.add(new B());
- clist.add(new C());
- foreach(clist);
8. 泛型数组
看到了很多文章中都会提起泛型数组,经过查看Sun的说明文档,在Java中是不能创建一个确切的泛型类型的数组的。也就是说下面的这个例子是不可以的:
- List<String>[] ls = new ArrayList<String>[10];
而使用通配符创建泛型数组是可以的,如下面这个例子:
- List<?>[] ls = new ArrayList<?>[10];
这样也是可以的:
- List<String>[] ls = new ArrayList[10];
下面使用Sun的一篇文档的一个例子来说明这个问题:
- List<String>[] lsa = new List<String>[10]; // Not really allowed.
- Object o = lsa;
- Object[] oa = (Object[]) o;
- List<Integer> li = new ArrayList<Integer>();
- li.add(new Integer(3));
- oa[1] = li; // Unsound, but passes run time store check
- String s = lsa[1].get(0); // Run-time error: ClassCastException.
这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]
赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
- List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
- Object o = lsa;
- Object[] oa = (Object[]) o;
- List<Integer> li = new ArrayList<Integer>();
- li.add(new Integer(3));
- oa[1] = li; // Correct.
- Integer i = (Integer) lsa[1].get(0); // OK
推荐阅读
Java多线程 46 - ScheduledThreadPoolExecutor详解(2)
ScheduledThreadPoolExecutor用于执行周期性或延时性的定时任务,它是在ThreadPoolExe...