JDK1.5引入的泛型语法特性,而为了与先前版本(没有泛型的版本)兼容,Java采用了一种类型擦除的方式,将编译后的字节码向前兼容;
概念
泛型的本质是为了 类型参数化,能避免创建新的类型,支持多种数据类型执行相同的代码(代码复用)
泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会进行类型检查)
若指定泛型的类型为A,那么所有使用到泛型的地方都会被指定为A,这些过程不需要强制类型转换;
不需要强制类型转换时什么意思?举个例子:
List<Integer> list:在取元素的时候不用
Integer a = (Integer) list.get(0)这种写法,而是直接Integer a = list.get(0)
JDK1.5前
当没有泛型,是如何处理多个类型的呢?
要么针对每个类型都写一个类;
// 针对 Integer 的类
class BoxInteger {
Integer value;
void setValue(Integer value) { this.value = value; }
Integer getValue() { return this.value; }
}
// 针对 String 的类
class BoxString {
String value;
void setValue(String value) { this.value = value; }
String getValue() { return this.value; }
}这种写法需要实现多个类,在代码维护时需要改动多个地方;
要么使用 Object 或者共同父类来写一个类,在使用时进行类型强转
class BoxCommon {
private Object value;
void setValue(Object value) { this.value = value; }
Object getValue() { return this.value; }
public static void main(String[] args) {
// 针对 String 的
BoxCommon stringBox = new BoxCommon();
stringBox.setValue("123");
String value = (String) stringBox.getValue();
// 针对 Integer 的
BoxCommon intBox = new BoxCommon();
intBox.setValue(1234);
Integer intValue = (Integer) intBox.getValue();
}
}这种写法虽然不需要多个类,但是需要类型强转,如果当类型不匹配时,会出现
ClassCastException异常;对于类型的转换需要额外的小心;
而泛型的引进,就是为了消除 ClassCastException ;
使用
在 <> 中声明泛型的名称,通常使用一个大写字母表示;
允许同时声明多个泛型,多个泛型之间使用
,区分:允许使用
extends指定泛型的上界(不支持指定下界)允许使用
&同时指定泛型需要实现的接口(支持同时指定多个)
泛型类
单个泛型:
class Group<T> {
T[] members;
}
Group<Person> group = new Group<>();
group.members = new Person[10];多个泛型:
// 多个泛型
class Pair<K, V> {
K key;
V value;
}
Pair<Integer, Double> pair = new Pair<>();
pair.key = 1;
pair.value = 2d;泛型上界:
class Box<T extends Number> {
T value;
public static void main(String[] args) {
Box<Integer> intBox =new Box<>();
Box<String> stringBox = new Box<String>();
}
}& 声明泛型实现的接口:
class Box<T extends Number & Comparable & Serializable> {
T value;
}这个例子指 泛型T需要是Number的子类,同时需要实现 Comaparable 和 Serializanl 接口
静态方法和静态变量不能使用泛型声明的参数:
泛型是在定义对象时指定的:
new Calculation<Integer>()而静态方法和静态变量不需要使用对象来调用,无法确定泛型的具体类型;
class Calculation<T extends Number> {
// 编译错误
static double add(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
}泛型接口
interface Usable<T> {
void use(T obj);
}
class Mirror implements Usable<Mirror> {
@Override
public void use(Mirror obj) {
System.out.println("照镜子");
}
}泛型方法
在调用方法时指定类型,与类是否指定泛型没有关系;
class Calculation {
// 泛型上界
static <T extends Number> double add(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
public static void main(String[] args) {
System.out.println(add(10, 100f));
}
}型变
型变(variance)是指在使用泛型时,不同类型之间如何相互兼
主要分为:
协变(covariance)
逆变(contravariance)
不变(invariance)
不变invariance
不变 表示泛型类型之间不能相互赋值;
也就是说,泛型类型不同,即使存在继承关系, 也会视作为不同的类型。
List<Number> numberList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
numberList = intList; // 编译错误要取消这种限制,可以使用泛型通配符 <?> 表示接收任何类型的泛型:
List<String> list = new ArrayList<>();
List<?> allList = new ArrayList<>();
allList = list; // 正确协变covariance
协变 表示可以将一个泛型类型的 子类型 对象赋值给 父类型 对象(看作是继承关系,泛型子类可以看做泛型父类的子类)
使用 <? extends T> 表示
List<? extends Number> list = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
list = intList; // 正确
intList = list; // 编译错误逆变invariance
逆变 表示可以将一个泛型类型的 父类型 对象赋值给 子类型 对象(看作是反过来 "逆" 的继承关系,泛型父类可以看做泛型子类的子类)
使用 <? super T> 表示
List<? super Integer> intList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
intList = numberList; // 正确
numberList = intList; // 编译错误作用
协变 是对子类型的约束宽松,让 子类型 的泛型能够赋值给 父类型 的泛型
代价是 只能取不能存,也就是只能作为 生产者(不能消费)
static void add(List<? extends Number> list) { list.add(1); // 错误! }如果外部传递的是一个
List<Double>类型的List,若add函数中只要是继承了 Number 的对象都能添加,就会导致 List 中数据类型不一致,在使用时就会出现ClassCastException
协变 是对父类型的约束宽松,让 子类型 的泛型能够赋值给 父类型 的泛型
代价是 只能存不能取,也就是只能作为 消费者(不能生产)
static void add(List<? super Integer> list) { Integer a = list.get(0); // 无法确定list的元素就是 Integer }如果外部传递的是
List<Number>类型的 List,如果拿出来当Integer使用,也会出现ClassCastException;但是允许添加元素,因为 Integer 允许赋值给 Number 类型的引用;
类型擦除
伪泛型
泛型特性是在 JDK1.5 引入的,为了兼容先前的版本,Java使用了 伪泛型;
也就是在语法上可以使用泛型,但是编译的时候会进行 类型擦除(Type Erasure):
将泛型替换成具体的类型,就好像没有泛型一样;
这个替换并不是替换成指定的类型,而是根据上下界推导出的类型(指定的类型被擦除了)
可以测试下:
class A { }
class B { }
class Box<T> {
T value;
public static void main(String[] args) {
Box<Integer> intBox = new Box<>();
Box<Number> numberBox = new Box<>();
A a = new A();
B b = new B();
System.out.println(a.getClass() == b.getClass());
// 输出 false
System.out.println(intBox.getClass() == numberBox.getClass());
// 输出 true
}
}不同的类 A 和 B 的 Class 对象不同,输出 false 正常;
直觉上 Box<Integer> 和 Box<Number> 是不同的类,其 Class 对象应该是不同的,但是上面输出的 true,说明 Box 的类型被擦除了,擦除后的类型都是 Box;
类型擦除原则
删除泛型参数声明,也就是删除掉
<>及其内部内容;根据泛型上下界推导出所需要替换的类型
如果是无限制通配符
<?>或者没有上下界限定<T>,那么替换为 Object如果指定了上界
<? extend A>则会替换为 A;如果指定了下界
<? super A>则会替换为 Object(取父类,A的父类不确定只能取 Object)
确保类型安全,必要时插入强制类型转换代码;
自动产生 “桥接方法” 以保证类型擦除后的代码仍然具有泛型的 “多态性”
对于无上下界的泛型,类型擦除:
将
<T>和<?>无上下界的类型推断为 Object,并进行替换
class Box<T> {
T value;
Box(T initial) {
value = initial;
}
}
// ------ 类型擦除后 ----------------
class Box {
Object value;
Box(Object initial) {
value = initial;
}
}对于有上下界的泛型,类型擦除:
将类似
<T extends Number>和<? extends Number>推断为 Number,进行替换;将类似
<T super Number>推断为 Object,进行替换;
class Box<T extends Number> {
T value;
Box(T initial) {
value = initial;
}
}
// ------ 类型擦除后 ----------------
class Box {
Number value;
Box(Number initial) {
value = initial;
}
}编译期检查
Java的编译器先通过检查代码中泛型的类型,通过类型检查后,才进行类型擦除,最后才编译;
下面这个例子,list类型擦除后类型是 Object,应该是允许添加字符串的,但是因为编译期对类型的检查,导致编译无法通过;
List<Integer> list = new ArrayList<>();
list.add(1234);
list.add("hello")如果通过反射在 运行时 插入数据,是能够成功被调用并插入的
从获取
add方法需要传入Object类可以再次确认类型已经被替换为 Object;可以尝试替换成
Integer.class,会发现抛出NoSuchMethodException异常;
List<Integer> list = new ArrayList<>();
Method addMethod = list.getClass().getDeclaredMethod("add", Object.class);
addMethod.invoke(list, "1234");
System.out.println(list.get(0));
// 输出 1234在运行时取出数据并赋值给不匹配的类型,会抛出 ClassCastException:
List<Integer> list = new ArrayList<>();
Method addMethod = list.getClass().getDeclaredMethod("add", Object.class);
addMethod.invoke(list, "1234");
Integer firstElement = list.get(0);
System.out.println(firstElement);因为编译后的字节码会在赋值处自动做一次
checkcast类型转换,如果不能成功转换,则会抛出该异常;换在 JDK1.5 版本前,需要显式使用类型强转;
反射获取类型信息
既然类型擦除了,那泛型的类型信息是否就已经消失了呢?
并不是,从字节码中可以看到保持了带有泛型信息的 Signature
编写一个简单的代码:
public class Main {
public static void main(String[] args) {
List<Number> list = new ArrayList<>();
list.add(1);
Number a = list.get(0);
}
}使用 javap -s -v xxx.class 查看字节码信息:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #7 // class java/util/ArrayList
3: dup
4: invokespecial #9 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: invokeinterface #16, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
18: pop
19: aload_1
20: iconst_0
21: invokeinterface #22, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
26: checkcast #26 // class java/lang/Number
29: astore_2
30: return
LineNumberTable:
line 15: 0
line 16: 8
line 17: 19
line 18: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 list Ljava/util/List;
30 1 2 a Ljava/lang/Number;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 23 1 list Ljava/util/List<Ljava/lang/Number;>;可以看到,new 的时候只有
ArrayList这个类,而不是ArrayList<Number>,说明类型 Number 被擦除了;但是 Number 类型信息并没有消失,在 LocalVariableTypeTable 地方存在有
Ljava/util/List<Ljava/lang/Number;>——Number的类型信息;所以类型擦了,也等于没擦?同时在取出数据的时候,会进行一个
checkcast类型转换,将从 ArrayList 取出的数据检查会否能转换为 Number,如果不能则会抛出ClassCastException
因此可以通过反射来获取这些泛型信息;
Type
java.lang.reflect.Type是Java中所有类型的公共高级接口,代表了Java中的所有类型。
Type 体系中类型的包括(这些类型都实现Type接口):
数组类型(GenericArrayType)
参数化类型(ParameterizedType)
类型变量(TypeVariable)
通配符类型(WildcardType)
原始类型(Class)、基本类型(Class)
而泛型就是 ParameterizedType
桥接方法(多态)
父类 Box 在类型擦除之后,会被替换为 Object;
class Box<T> {
private T value;
void setValue(T value) { this.value = value; }
T getVaule() { return this.value; }
}
// ---------- 类型擦除 ---------------------
class Box {
private Object value;
void setValue(Object value) { this.value = value; }
Object getVaule() { return this.value; }
}
如果子类指定了 Box 父类的泛型类型(如下代码),所期待的父类方法应该是以 String 作为类型的,但是父类被类型擦除了变成 Object;
此时子类 重写 的方法和父类的方法不相同了(子类的是String作为返回值,而父类是 Object),而是变成了 重载(参数不同,返回值不同)
class BoxImpl extends Box<String> {
@Override
String getVaule() {
return super.getVaule();
}
}
// 所期待的父类,此时重写父类的方法正确 (例子假设)
class Box {
private String value;
void setValue(String value) { this.value = value; }
String getVaule() { return this.value; }
}重载方法并不能实现 多态,这就导致了多态和类型擦除的冲突,因此JVM使用了 “桥接方法” 来解决这种冲突
使用 javap -c BoxImpl.class 查看字节码,能够发现,BoxImpl 有4个方法,
其中前两个都是代码对应的,
而后面的 Object 相关的方法就是编译器生成的方法,而这些内部的方法调用了对应的 String 相关方法
当多态发生时,调用
Box#getValue(): T实际上会调用BoxImpl#getValue(): Object,进而调用BoxImpl#getValue(): String
class BoxImpl extends Box<java.lang.String> {
BoxImpl();
Code:
0: aload_0
1: invokespecial #1 // Method Box."<init>":()V
4: return
java.lang.String getVaule();
Code:
0: ldc #7 // String something
2: areturn
void setValue(java.lang.String);
Code:
0: aload_0
1: aload_1
2: invokespecial #9 // Method Box.setValue:(Ljava/lang/Object;)V
5: return
java.lang.Object getVaule();
Code:
0: aload_0
1: invokevirtual #13 // Method getVaule:()Ljava/lang/String;
4: areturn
void setValue(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #19 // class java/lang/String
5: invokevirtual #21 // Method setValue:(Ljava/lang/String;)V
8: return
}Java 内部使用了这种桥接方法,解决了多态和类型擦除的冲突问题;(你以为你重写了方法就真的是重写了方法吗)需要生成桥接方法来连接父类和子类;
如果细心可以发现,
String getValue()和 桥接方法Object getValue()只有返回值不同,而 Java 中不能把返回值作为判断重载方法的依据的,这是不是就相矛盾了?如果允许返回值作为判断根据,那编译器不就生成了相同的桥接方法了么?
同时Jvm是支持通过返回类型和参数类型来确定一个方法的;
注意点
基本数据类型不能作为泛型参数
因为类型擦除后是 Object(无指定的话),Object 是无法存储基本数据类型的,只能存储引用类型,
所以只能使用引用类型作为泛型的参数;
无法实例化泛型
下面的例子就是错误的:
class Box<T> {
T value;
Box() {
value = new T(); // 编译错误
}
}编译期间无法确定泛型的具体类型(类型被擦除了),导致找不到对应的类字节码文件;
本来是 new Persion() 的实例化代码,类型擦除后变成 new Object() ,很明显是不对的;
可以通过反射来实现实例化:
class Box<T> {
T value;
Box(Class<T> clazz) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
value = clazz.getDeclaredConstructor().newInstance();
}
}实际上 clazz 提供了对应类的信息
泛型数组
泛型数组不能实例化
T[]类型的数组
指的是 new Xxx[] 的普通数组,这种泛型数据不能实例化;
不能实例化的原因和上面泛型实例化的原因相同,无法确定实例化的类(更准确的应该是无法确定申请内存的大小);
static <T> T[] init(int size) {
return new T[size];
}可以通过反射类生成:
public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}或者通过可变接受参数类实现:
static <T> T[] init(T ...arg) {
return arg;
}Xxx<T>类型的数组
Java 中是不允许创建确切泛型类型的数组的
List<String>[] strLists = new ArrayList<String>[10]; // 编译错误: Generic array creation
为什么呢?
指定了类型,而类型擦除后就变成了 ArrayList[10] ,此时无论赋值什么都不会报错,比如:
List<Integer> intList = new ArrayList<Integer>();
strLists[0] = intList;
// 因为类型擦除后,数组的类型是 List[], intList 的类型是 List,
// 所以 intList 能赋值给 strLists;如果取出数据就会自动进行类型转换,于是就会抛出 ClassCastException ;
这个和泛型用于消除 ClassCastException 的目的相矛盾,因此不允许;
但是若使用类型通配符 <?>,就可以创建数组;
这是因为使用 <?> 在取出的时候需要 显式 进行类型强转(不是自动进行的)
也就是说需要承担类型强转的潜在问题;
List<?>[] commonLists= new ArrayList<?>[10];
List<String> lists = (List<String>) commonLists[0];异常捕获无法使用泛型
异常捕获无法使用泛型,如果允许了则会出现一系列问题
但是允许抛出泛型类型的异常
class ApiException<T extends Throwable> extends Exception {
// 编译错误 Generic class may not extend 'java.lang.Throwable'
}首先要了解 catch 子句的规则:越前出现的异常需要越具体,不能出现相同异常的捕获
例子1:会出现相同的异常捕获子句
try {
} catch (ApiException<RuntimeException> e) {
} catch (ApiException<IllegalArgumentException> e) {
}
//------ 类型擦除后 ------
try {
} catch (ApiException e) {
} catch (ApiException e) { // 编译错误 Exception 'ApiException' has already been caught
}例子2:违反越前出现的异常需要越具体
static <T extends Throwable> void catchBlock(Consumer<Integer> consumer) {
try {
consumer.accept(10);
} catch (T e) { // 编译错误:Cannot catch type parameters
} catch (NumberFormatException e) {
}
}
//------ 类型擦除后 ------
static void catchBlock(Consumer<Integer> consumer) {
try {
consumer.accept(10);
} catch (Throwable e) {
} catch (NumberFormatException e) { // 编译错误:Exception 'java.lang.NumberFormatException' has already been caught
}
}例子3:允许抛出泛型类型的异常
// 正确用法
static <T extends Throwable> void catchBlock(T customException, Consumer<Integer> consumer) throws T {
try {
consumer.accept(10);
} catch (NumberFormatException e) {
throw customException;
}
}