Java 泛型
泛型的概念
JDK1.5引入的一种参数化类型特性,它提供编译时类型安全检测机制,使编译器能够在编译时检测到非法的类型。
泛型的好处
- 代码更健壮,将类型检查提前到编译期,避免了运行时类型转换错误
- 代码更简洁,避免了强制类型转换
- 代码更灵活,便于复用
参数化类型
把类型当作参数一样传递
Box<T>
中 T
称为类型参数或类型变量,整个被称为泛型类型
Box<Apple>
中的 Apple
称为实际类型参数,整个被称为参数化的类型 ParametrizedType
泛型的原理
JDK1.5 引入泛型特性,Jvm 其实是不支持泛型,为了向下兼容,所以 Java 的泛型实现是一种伪泛型机制,也就是在编译期擦除了所有的泛型信息,这样就不会产生新的类型被编译成字节码,所有的泛型类型仍然是原始类型,运行时根本就不存在泛型信息,自然也不会影响以前编写类库的运行,实现了向下兼容
泛型使用
泛型类
//泛型类的定义语法
class 类名<泛型标识,泛型标识,...>{
private 泛型标识 变量名;
}
//栗子
public class Box<T>{
//T是在实例化类时指明泛型参数的具体类型
private T t;
public void setT(T t){
this.t = t;
}
public T getT(){
return t;
}
}
泛型类继承
- 父类是泛型类型(泛型参数T没有传实际的类型参数),子类也要是泛型类型
class Child<T> extends Father<T>
- 父类是参数化的类型(泛型参数传了实际的类型参数),子类的实际类型参数可以不传
class Child extends Father<String>
泛型参数存在继承关系,并不代表泛型类型有继承关系
如:Integer 继承自 Number,而 List<Number> 和 List<Integer> 并没有任何关系
泛型的继承关系1
泛型的继承关系1
泛型接口
泛型接口和泛型类类似,这里就忽略了代码
泛型方法
泛型方法与仅仅使用的泛型参数的普通方法的区别就是,返回值前是否有声明泛型,栗子:
//泛型方法,返回值前<T>声明了泛型参数,调用时指明类型参数的具体类型
public <T> void setT(T t){
this.t = t;
}
//仅使用了泛型参数的普通方法
public void setT(T t){
this.t = t;
}
泛型擦除机制
Java 的泛型是在 JDK1.5 引入的,虚拟机并不支持泛型,所以 Java 实现的是一种伪泛型机制,即在编译器擦除所有的泛型信息,这样 Java 就不需要产生新的类型到字节码,所有的泛型类型都是原始类型。
编译器是如何擦除的
- 检查泛型类型,获取目标类型
- 擦除类型变量,并替换为限定类型
- 如果泛型类型的泛型变量没有限定 (
<T>
),则用Object
作为替换类型 - 如果有限定(
<? extends XClass>
,<? super XClass>),则用XClass
作为替换 - 如果有多个限定(
<? extend XClass1 & XClass2>
),则用第一个边界XClass1
作为替换类型
- 在必要时插入类型转换以保证安全
- 生成桥方法以在扩展的泛型类中保留多态
泛型擦除的副作用
- 任何基础类型不能作为实际类型参数
- 无法创建类型参数的实例
- 不可直接创建具体泛型类型的数组
- 无法对参数化类型使用转换或
instanceof
- 无法使用类型参数声明静态变量
- 泛型类型无法直接或间接基础
Throwable
类 - 当一个的所有重载方法的形参类型擦除后,如果他们具有相同的原始类型,那么此方法是不可重载的
桥方法
类型擦除的影响
下面的代码片段中,声明了一个泛型类型和他的一个扩展类,并在扩展类中传入了实际类型参数
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
//编译后泛型擦除
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
//与父类中的方法签名不同,这里并没有重写父类中的方法
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
看看使用时的两段代码
MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); //运行时,这里会抛出java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
Integer x = mn.data;
既然扩展类中看上去并未重写父类中的方法,那么n.setData("Hello")
应该是在调用从Note<T>
类中继承的方法,根据泛型擦除机制,T
会被擦除替换成 Object
,代码应该能够正确执行才对啊?
但是,实际上为解决这样的类型擦除后方法重写失败,并保持泛型的多态性,编译器会自动生成一个桥方法
class MyNode extends Node {
// 编译后生成的桥方法
//
// public void setData(Object data) {
// setData((Integer) data);
// }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
桥方法和类型擦除后 Node 中的 setData 方法有着同样签名。而这个桥方法也在委托调用子类方法时,对参数类型进行了强制类型转换。所以,ClassCastExcption 异常就是在这里抛出来的
泛型参数不能显式的用于运行时类型的操作。《Java编程思想》
受限的类型参数
作用
对泛型变量的范围作出限制
格式
单一限制:<T extends Number>
多种限制:<T extends A & B & C>
多种限制,语法要求如果上限类型是一个类,必须放到第一个位置,否则编译错误
interface A{}
interface B{}
class C{}
//C上限是类,必须放在第一个位置
class D<T extends C & A & B>{}
通配符
在通用代码中,称为通配符的 (?
) 表示未知类型。通配符可以在多种情况下使用:作为参数、字段或局部变量的类型;有时作为返回值类型。通配符从不用作泛型方法的调用,泛型类实例创建或超类型的类型参数。
上界通配符
用 <? extends T>
表示,泛型参数类型范围是 T
或其子类型
下界通配符
用<? super T>
表示,泛型参数的类型范围是T
或其父类型
无界通配符
用 <?>
来表示,泛型参数是一种未知类型,也可以称为类型通配符,相当于List<? extends Object>
,运行时和原始类型 List
没啥区别,但是在编译时List<?>
会进行类型安全检查,而原始类型 List
不会。有两种情况,无界通配符是非常有用的:
- 如果正在编写一个可以使用
Object
类中提供的功能实现的方法 - 当代码使用通用类中不依赖于类型参数的方法时。如:List.size或List.clean。阅读源码时,你可能经常见到
Class<?>
类型,Class
中之所以经常使用,是因为Class
中的大部分方法都不依赖于T
。
简而言之,就是代码中没有用到类型参数
考虑以下方法,printlist
:
// printList 的目标是打印任何类型的列表,但未能实现该目标(它仅打印 Object 实例的列表)
public static void printList(List<Object> list) {
for (Object elem : list) {
System.out.println(elem + " ");
System.out.println();
}
}
// printList 方法可以传入任意类型元素的List
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem + " ");
System.out.println();
}
}
通配符和子类型
通配符类继承关系1通配符类继承关系2
通配符捕获和帮助方法
在某些情况下,编译器会推断通配符的类型。例如,可以将列表声明为 List<?>
,但是在评估表达式是,编译器会从代码中推断出特定的类型(CAP#1
),这种情况称为通配符捕获。
//这里也无法通过编译,错误: 不兼容的类型: Object无法转换为CAP#1(捕获变量),
//编译器只能知道 i.get(0) 可以是一个 Object 作为 ?的上界,而编译器将 ?当作一种 CAP#1 的新的类型,扩展自 Object。
//例如,即便 B 和 C 是 A 的子类,你也不能往 List<? extends A> 里面添加 B 类后又添加 C 类,来个混搭。
pulic void foo(List<?> i){
i.set(0,i.get(0);
}
//为了解决这个问题,采用以下方法,显式指出参数 T,强制指出前后匹配性,这样就完成了编译检查。
public class WildcardFixed {
void foo(List<?> i) {
fooHelper(i);
}
// Helper method created so that the wildcard can be captured
// 编译器能够推断出 T 是 CAP#1(捕获变量)
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}
PESC原则
什么是 PESC
如果参数化的类型表示一个T
的生产者,就用<? extends T>
;如果他表示一个T
的消费者,就用<? super T>
productor -- extends
上界通配符限制的泛型类型,可以作为生产者,安全取元素,但不能add
consumer -- super
下界通配符,可以作为消费者,安全add元素(必须是下界及其派生类),不能取元素
通配符使用准则
为了便于讨论,变量视为提供以下两个功能之一:
输入变量:将数据提供给代码,作为数据源。如 copy(src,dest)
参数复制方法,要将 src
中的数据复制到 dest
中,则 src
就是输入参数。
输出参数: 保存要在其他地方使用的数据。在上面的 dest
参数就是接受数据,因此它是输入参数。
在考虑使用通配符以及哪种类型的通配符时,可以使用"输入"和"输出"原理,下面提供了要遵循的原则:
- 使用上界通配符
extends
定义输入变量 - 使用下界通配符
super
定义输入变量 - 如果可以使用
Object
类中定义的方法访问输入变量,使用无界通配符?
- 如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符
数据类型变化
Type Variance形式化定义
假设 A、B 是类型,f(·)
表示类型转换,≤
表示继承关系,如A ≤ B
,表示 A 继承 B
-
f(·)
是协变的,若A ≤ B
,则有f(A) ≤ f(B)
-
f(·)
是逆变的,若A ≤ B
,则有f(B) ≤ f(A)
-
f(·)
是不变的,若A ≤ B
,则f(A) ≤ f(B)
和f(B) ≤ f(A)
都不成立,即f(A)
和f(B)
没有关系 -
f(·)
是双变的,若A ≤ B
,则有f(A) ≤ f(B)
和f(B) ≤ f(A)
都成立
Java 数组是协变的
String 是 Object 的子类,String[] 是 Object[] 的子类
class A{}
class B extends A{}
class C extends B{}
//则
public void test(){
B[] array1 = new B[1];
array1[0] = new B();
A[] array2 = array1;
try{
// 编译时ok,运行时 error,编译看声明类型,运行时看实际类型,所以 B 类型数组里面,无法放父类 A
array2[0] = new A();
//数组协变,B[] arrayB = new C[1],所以可以当作子类的数组来用
array2[0] = new C();
}catch(Exception ex){
}
}
明确泛型参数的泛型是不变的
明确泛型参数的泛型是不变的,如List<String>
和 List<Object>
并没有任何关系
具有受限类型参数的泛型是可变的
class A{}
class B extens A{}
//协变
ArrayList<? extends A> listA = new ArrayList<B>();
//逆变
ArrayList<? super B> listA = new ArrayList<A>();
- JDK1.4 重写的方法参数和返回值要求一样
- JDK1.5以后,重写的方法,参数要求一样的,返回值可以是协变的,即如果重写方法时返回值是被重写方法返回值的子类也可以
泛型与反射
泛型参数虽然会在编译时被擦除,但是泛型的类型信息会保留类的常量池内,所以在运行时,仍然可以通过发射获取泛型的类型信息。
ParameterizedType
泛型类型,如Map<String,Integer>
类型的抽象,可以通过这个类提供的方法获取实际类型参数的类对象(Class对象)
public class GenericDemo {
private Map<String,Integer> map;
public static void main(String[] args) throws NoSuchFieldException {
Field f = GenericDemo.class.getDeclaredField("map");
//获取泛型类型
System.out.println(f.getGenericType());//java.util.Map<java.lang.String, java.lang.Integer>
ParameterizedType parameterizedType = (ParameterizedType) f.getGenericType();
//获取原始类型
Type rawType = parameterizedType.getRawType();//interface java.util.Map
System.out.println(rawType);
//获取实际类型参数的类类型对象数组,即Class<T>的数组,这里数组的元素分别是String.class,Integer.class
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
System.out.println(actualTypeArguments[1]);//class java.lang.Integer
}
}
编译后,泛型类型的泛型信息会被保留到 signature 注释中
/ class version 51.0 (51)
// access flags 0x21
public class com/nd/android/xx/java/demo/generic/GenericDemo {
// compiled from: GenericDemo.java
// access flags 0x2
// signature Ljava/util/Map<Ljava/lang/String;Ljava/lang/Integer;>;
// declaration: map extends java.util.Map<java.lang.String, java.lang.Integer>
private Ljava/util/Map; map
...
}
小结
以上介绍了下面几个方面的内容:
- 泛型的概念
- 泛型带来的好处
- 泛型分别可以使用在类、接口和方法中
- 介绍了泛型的实现原理
- 泛型擦除机制及其带来的问题
- 了解了受限类型参数及其意义
- 协变和协变相关的概念,以及泛型如何来支持协变的
- 介绍了如何通过反射获取实际的泛型参数
网友评论