重新认识Java——泛型(基础、使用和实现原理)

泛型是Java中重要知识点,是必须要深刻掌握的内容。由于泛型相关的内容比较多,基于单一知识原则,笔者打算将泛型部分分开成两部分书写。本文主要的内容如下:

  1. 泛型基础概念
  2. 泛型的分类、定义和使用
  3. Java泛型的实现原理

泛型基础内容

泛型是Java中一个非常重要的内容,对于Java进阶学习是必须要掌握的知识点。

解决的问题

在开始介绍泛型的知识之前,先来了解一下泛型解决了什么问题,换句话说,为什么需要使用泛型。集合类是在开发过程中经常要用的类,下面是不使用泛型的代码示例。

1
2
3
4
5
6
7
// 定义一个List,add()可以存放Object及其子类实例
List list = new ArrayList();
list.add(123); // 合法
list.add("123"); // 合法
// 我们在编译时无法知晓list到底存放的什么数据,于是在进行强制转换时发生异常
int i = (Integer) list.get(1); // 抛出ClassCastException异常

上面的代码首先实例化一个ArrayList对象,它可以存放所有Object及其子类实例。分别add一个Integer类型对象和String类型对象,我们原本以为list中存放的全部是Integer类型对象,于是在使用get()方法获取对象后进行强制转换。从代码中可以看到,索引值为1的位置放置的String类型,很显然在进行强制转换时会抛出ClassCastException(类型转换异常)。由于这种异常只会发生在运行时,我们在开发时稍有不慎,就会直接掉到坑里,还很难排查出问题。

为什么会出现这种问题呢?

  1. 集合本身无法对其存放的对象类型进行限定,可以涵盖Java中的所有类型。缺口太大,导致各种蛇、蚁、虫、鼠通通都可以进来。
  2. 由于我们要使用的实际存放类型的方法,所以不可避免地要进行类型转换。小对象转大对象很容易,大对象转小对象则有很大的风险,因为在编译时,我们无从得知对象真正的类型。

泛型就是为了解决这类问题而诞生的。

泛型中的基本概念

泛型,即参数化类型,是在JDK1.5之后才开始引入的。所谓参数化类型,是指所操作的数据类型在定义是被指定为一个参数,然后在使用时传入具体的类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。还是以上一节的示例为例,下面是其泛型写法。

1
2
3
4
5
List<Integer> list = new ArrayList<Integer>();
list.add(123); // 合法
list.add("123"); // 不合法,编译不通过
int i = list.get(1); // 无需强制转换类型

在第1行代码中已经限定了List中仅可存放Integer类型对象,所以在第3行add一个String类型对象时,IDE会报错,并且无法编译通过。我们来看看JDK1.5之后的List接口定义如下:

1
2
3
4
5
6
public interface List<E> extends Collection<E> {
boolean add(E e);
E get(int index);
}

上面的尖括号中的E就是泛型的类型参数(或者称之为形参),而在调用List类时,传入了Integer这个具体的类型参数(或者称为实参)。下面详细介绍一下泛型中一些基本术语(以List为例):

  • List称为泛型类型
  • List中的E称为类型变量或者类型参数
  • List称为参数化的类型
  • List中的Integer称为类型参数的实例或者实际类型参数
  • List中的念为typeof Integer
  • List称为List的原始类型

泛型的定义和使用

泛型的参数类型可以定义类、接口和方法中,分别称为泛型类、泛型接口和泛型方法。

泛型类的定义和使用

一个泛型类(generic class)就是具有一个或多个类型变量的类。上面的例子中的List就是一个典型的泛型类。泛型类的定义结构类似下面的代码:

1
2
3
4
5
6
public class ClassName<T1, T2> { // 可以任意多个类型变量
public void doSomething(T1 t1) {
System.out.println(t1);
}
}

注意,在Java编码规范中,类型变量通常使用较短的大写字母,并且最好与其作用相匹配。譬如:List中的变量使用E,对应单词Element,Map中的K,V变量对应单词Key和Value。当然这些都是约定性质的东西,其实类型变量的命名规则与Java中的普通变量命名规则是一致的。

下面的代码使用上面定义的泛型类,就是这么简单。

1
2
ClassName<String, String> a = new ClassName<String, String>();
a.doSomething("hello world");

泛型接口的定义和使用

接口本质上来说就是一种特殊的类,所以泛型接口的定义和使用与泛型类相差无几。下面的代码是泛型接口的定义和使用。

1
2
3
4
5
6
7
8
9
10
11
public interface InterfaceName<T1, T2> { // 可以任意多个类型变量
public void doSomething(T1 t1);
}
public class ConcreteName<T2> implements InterfaceName<String, T2> {
public void doSomething(String t1) {
System.out.println(t1);
}
}
1
2
InterfaceName<String, String> a = new ConcreteName<String>();
a.doSomething("hello world");

从上面的例子可以看出,如果实现一个泛型接口,可以在定义时直接传入具体的类型(如T1传入String),也可以继续传入一个类型,待使用时再确认具体的类型。

泛型方法的定义和使用

泛型类和泛型接口的类型变量都是定义在类型级别,其作用域可覆盖成员变量和成员方法。泛型方法的类型参数定义在方法签名中,一个典型的泛型方法定义如下:

1
2
3
4
5
6
7
8
9
10
/**
* 创建一个指定类型的无参构造的对象实例。
* @param <T> 待创建对象的类型。
* @param t 指定类型所对应的Class对象。
* @return 返回创建的对象。
* @throws Exception
*/
public <T> T getObject(Class<T> t) throws Exception {
return t.newInstance();
}

上面的代码中表示这是一个泛型方法,T是仅作用于getObject方法上的类型变量。在调用这个方法时,传入具体的类型。

1
String newStr = generic.getObject(String.class);

泛型变量的类型限定

假定我们有个需求,需要编写一个获取两个对象中较大的对象的泛型方法,利用上面的泛型知识,编写出下面的代码。

1
2
3
4
5
6
7
public <T> T getMax(T t1, T t2) {
if (t1.compareTo(t2) > 1) { // 编译错误
return t1;
} else {
return t2;
}
}

在上面的代码无法通过编译,由于我们都没有对类型变量对任何的约束限制,那么实际上这个类型可以是任意Object及其子类。那么在使用这个类型变量时,只能调用Object类中的方法。而Object本身就是Java中对顶层的类,没有实现Comparable接口,所以无法调用compareTo方法来比较对象的大小。这时候可以通过限定类型变量来达到目的。

1
2
3
4
5
6
7
public <T extends Comparable<T>> T getMax(T t1, T t2) {
if (t1.compareTo(t2) > 1) {
return t1;
} else {
return t2;
}
}

注意到上面的代码使用extends关键字限定了类型变量T必须继承自Comparable,于是变量t1和t2就可以使用Comparable接口中的compareTo方法了。

不管是泛型类、泛型接口还是泛型方法,都可以进行类型限定。类型限定的特点如下:

  1. 不管该限定是类还是接口,统一都使用extends关键字。
  2. 使用&符号进行多个限定,那么传入的具体类型必须同时是这些类型的子类。
1
2
3
public <T extends Serializable&Cloneable&Comparable> T getMax(T t1, T t2) {
...
}
  1. 由于Java中不支持多继承,所以不存在一个同时继承两个以上的类的类。所以,在泛型的限定中,&连接的类型最多只能有一个类,而接口数量则没有限制。同时,如果同时限定类和接口,则必须将类写在最前面。
1
2
3
public <T extends Object&Serializable&Cloneable&Comparable> T getMax(T t1, T t2) { // 合法
...
}
1
2
3
public <T extends Object&ArrayList> T getMax(T t1, T t2) { // 同时限定两个类,不合法
...
}
1
2
3
public <T extends Serializable&Cloneable&Comparable&Object> T getMax(T t1, T t2) { // 将类写在最后面,不合法
...
}

泛型的实现原理

前面介绍了泛型的基础知识以及使用方法,下面将更加深入地介绍泛型的底层原理。

Java中的泛型是伪泛型

泛型思想最早在C++语言的模板(Templates)中产生,Java后来也借用了这种思想。虽然思想一致,但是他们存在着本质性的不同。C++中的模板是真正意义上的泛型,在编译时就将不同模板类型参数编译成对应不同的目标代码,ClassName和ClassName是两种不同的类型,这种泛型被称为真正泛型。这种泛型实现方式,会导致类型膨胀,因为要为不同具体参数生成不同的类。

Java中ClassName和ClassName虽然在源代码中属于不同的类,但是编译后的字节码中,他们都被替换成原始类型(ClassName),而两者的原始类型的一样的,所以在运行时环境中,ClassName和ClassName就是同一个类。Java中的泛型是一种特殊的语法糖,通过类型擦除实现(后面介绍),这种泛型称为伪泛型。由于Java中有这么一个障眼法,如果没有进行深入研究,就会在产生莫名其妙的问题。值得一提的是,不少大牛对Java的泛型的实现方式很不满意。

类型擦除

Java中的泛型是通过类型擦除来实现的。所谓类型擦除,是指通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。

下面通过两个例子来证明在编译时确实发生了类型擦除。

例1分别创建实际类型为StringIntegerArrayList对象,通过getClass()方法获取两个实例的类,最后判断这个实例的类是相等的,证明两个实例共享同一个类。

1
2
3
4
5
6
7
8
9
// 声明一个具体类型为String的ArrayList
ArrayList<String> arrayList1 = new ArrayList<String>();
arrayList1.add("abc");
// 声明一个具体类型为Integer的ArrayList
ArrayList<Integer> arrayList2 = new ArrayList<Integer>();
arrayList2.add(123);
System.out.println(arrayList1.getClass() == arrayList2.getClass()); // 结果为true

例2创建一个只能存储Integer的ArrayList对象,在add一个整型数值后,利用反射调用add(Object o)add一个asd字符串,此时运行代码不会报错,运行结果会打印出1和asd两个值。这时再里利用反射调用add(Integer o)方法,运行会抛出codeNoSuchMethodException异常。这充分证明了在编译后,擦除了Integer这个泛型信息,只保留了原始类型。

1
2
3
4
5
6
7
8
ArrayList<Integer> arrayList3 = new ArrayList<Integer>();
arrayList3.add(1);
arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
for (int i = 0; i < arrayList3.size(); i++) {
System.out.println(arrayList3.get(i)); // 输出1,asd
}
arrayList3.getClass().getMethod("add", Integer.class).invoke(arrayList3, 2); // NoSuchMethodException:java.util.ArrayList.add(java.lang.Integer)

自动类型转换

上一节上说到了类型擦除,Java编译器会擦除掉泛型信息。那么调用ArrayListget()最终返回的必然会是一个Object对象,但是我们在源代码并没有写过Object转成Integer的代码,为什么就能“直接”将取出来的对象赋予一个Integer类型的变量呢(如下面的代码第12行)?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.List;
import java.util.ArrayList;
/**
* 泛型中的类型转换测试。
*/
public class Test {
public static void main(String[] args) {
List<Integer> a = new ArrayList<Integer>();
a.add(1);
Integer ai = a.get(0);
}
}

实际上,Java的泛型除了类型擦除之外,还会自动生成checkcast指令进行强制类型转换。上面的代码中的main方法编译后所对应的字节码如下。

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
26
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
18: pop
19: aload_1
20: iconst_0
21: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
26: checkcast #7 // class java/lang/Integer
29: astore_2
30: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 19
line 10: 30
}

看到第18行代码就是将Object类型的对象强制转换为Integer的指令。我们完全可以将上面的代码转换为下面的代码,它所实现的效果跟上面的泛型是一模一样的。既然泛型也需要进行强制转换,所以泛型并不会提供运行时效率,不过可以大大降低编程时的出错概率。

1
2
3
4
5
public static void main(String[] args) {
List a = new ArrayList();
a.add(1);
Integer ai = (Integer)a.get(0);
}

小结

本文介绍了泛型的基础知识,定义和使用以及泛型实现原理,涉及到的知识比较多,本文仅仅是开始,更多内容将在下一篇文章中做详细介绍。


参考文献:

http://blog.csdn.net/lonelyroamer/article/details/7864531
http://blog.csdn.net/lonelyroamer/article/details/7868820


本文由xialei原创,转载请说明出处http://hinylover.space/2016/06/25/relearn-java-generic-1/