泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
为什么我们需要泛型?
- 当一组方法因为参数的多种数据类型不同,而执行相同的代码时,使用泛型进行统一处理.
- 将一些
java.lang.ClassCastException
类型转换异常在代码编译期暴露出来.
泛型类的定义
引入一个变量 T(其他任意字母都可以),并用<>括起来,放到类的后面,1
2
3public class GenericType<T> {
private T data;
}
泛型方法的辨析
普通方法
1 | public T getData() { |
虽然这个方法使用了泛型,但是这只是一个普通方法.
只不过在返回值是在声明泛型类时已经声明过的泛型.
泛型方法1
2
3
4public static <T> T genericMethod(T t) {
System.out.println(t.getClass().getSimpleName());
return t;
}
首先在 public
与返回值之间声明一个泛型 T ,表明这是一个泛型方法.
这个 T 可以出现在泛型方法的任意位置.
泛型的数量可以是任意多个<K,V>
限定类型变量
当我们需要对类型变量进行约束,如需要这两个变量一定要有 compareTo 方法时,可以使用T extends Comparable 将 T 限制为实现某个接口或类.
1 | public static <T extends Comparable> T min(T a, T b) { |
同时extends左右都允许有多个,如 T,V extends Comparable & Serializable
注意限定类型中,只允许有一个类,而且如果有类,这个类必须是限定列表的第一个。
这种类的限定既可以用在泛型方法上也可以用在泛型类上。
1 | public static <T extends Comparable & Serializable> T min(T a, T b) { |
泛型中的约束和局限性
1. 不能使用基本类型实例化类型参数
2. 运行时类型检查只适用于原始类型1
2
3
4
5
6
7
8
9GenericType<String> stringGenericType = new GenericType<>();
GenericType<Integer> integerGenericType = new GenericType<>();
//编译出错
//if (stringGenericType instanceof GenericType<String>) {}
//相等
System.out.println(stringGenericType.getClass() == integerGenericType.getClass());
//都是 com.caimuhao.examples.generic.wildchar.GenericType
System.out.println(stringGenericType.getClass().getName());
System.out.println(integerGenericType.getClass().getName());
3. 泛型类的静态上下文中类型变量失效1
2
3
4
5public class GenericType<T> {
// 编译出错
//静态域或方法里不能引用泛型类型变量
private static T instance;
}
4. 不能创建参数化类型的数组1
2//编译报错
GenericType<Double>[] doubles = new GenericType<Double>[10];
5. 不能实例化类型变量1
2
3
4private T data;
public GenericType(){
data = new T();
}
泛型类型的继承规则
首先我们有一个类和他的子类1
2
3public class Employee {}
public class Work extends Employee {}
有一个泛型类
1 | public class Pair<T> {} |
虽然 Work 继承 Employee,但是他们没有关系1
2//编译报错
Pair<Employee> employeePair = new Pair<Work>();
但是泛型类可以继承或扩展其他泛型类,比如 List 和 ArrayList1
2public class ExtentPair<T> extends Pair<T>{}
Pair<Employee> pair = new ExtendPair<>();
通配符类型
? extend X
表示传递给方法的参数,必须 X 的子类(包括 X 本身)
1 | public static void print2(GenericType<? extends Fruit> f) { |
当泛型类 GenericType
中存在 set 方法时, set 方法是不允许调用的,会出现编译错误.
1 | public void setData(T data) { |
get 方法返回正确,会返回一个 Fruit 类型的值.1
Fruit data = c.getData();
原因?? extends X
表示类型的上界,类型参数是 X 的子类,那么可以肯定 get 方法返回的一定是 X(不管是 X 还是 X 的子类),编译器是可以确定的.但 set 方法只知道传入的是个 X,不知道具体是那个子类.
总结:
主要用于安全地访问数据.可以访问 X 及其子类,并不能传入 null .
? super X
表示传递给方法的参数,必须是 X 的超类(包括 X 本身)
1 | public static void printSuper(GenericType<? super Apple> g) { |
当泛型类 GenericType
,提供 get 和 set 泛型参数方法,set 方法是可以调用,且只能传入 X 或 X 的子类.
1 | private T data; |
原因?? super X
表示类型的下界,类型参数是 X 的超类(包括 X 本身),get方法返回的 一定是 X 的超类,但是不知道是那个超类,所以返回的 Object.对于 set 方法,编译器不知道确切类型,但是 X 和 X 的子类都可以安全的转型为 X.
总结
主要用于安全的写入数据,可以写入 X 及其子类型.
虚拟机是如何实现泛型的?
在 Java 版本早期是没有泛型的,只能通过 Object 是所有类的父类和类型强制转换两个特点来实现泛型化,之后为了兼容早期的版本,Java在实现泛型时,使用了与 C++不同的方式,只将泛型保留在源代码中,当编译器进行编译时,对泛型类型进行了擦除,在一些地方使用了类型强转.所以 Java 中的泛型技术实际上只是语法糖.
1 | public static String method(List<String> stringList){ |
上面两个方法是不能编译的,因为 List<String>
和 List<Integer>
编译后都被擦除了,变成了一样的原生类型List<E>
,擦除导致两种方法签名变成一样的.
由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响和新的需求,如在泛型类中如何获取传入的参数化类型等。因此,JCP组织对虚拟机规范做出了相应的修改,引入了诸如Signature
、LocalVariableTypeTable
等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature
是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名[3],这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别49.0以上版本的Class
文件的虚拟机都要能正确地识别Signature
参数。
另外,从Signature
属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的Code
属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。