前言
泛型是现代编程语言很重要的组成部分,它的出现有两个目的:
- 提供更严密的编译器类型检查
- 支持泛型编程
Java从JDK5开始添加了对泛型支持,本文将详述Java(JDK8)中泛型的语法,原理,高级用法和局限.
正文
泛型类和方法的定义
//非严谨定义,仅表示基本结构
/**
*泛型类
*/
public class class_name<T1, T2, ..., Tn> { /* ... */ }
/**
*泛型方法
*/
public <T1,T2,...,Tn> void method_name(T1 t1,T2 t2,...Tn){
/* ... */ }
//eg.
//创建单Parameter的泛型类
class A<T>{ /* ... */ }
//创建多Parameter泛型类
class B<T,V>{ /* ... */ }
class A<T> {
//使用类Type Parameter的方法
public void method1(T t) {
/* ... */
}
//泛型方法
public <R> void method2(R r){
/* ... */
}
//混合类Type Parameter和方法Type Parameter的方法
public <R> void method3(R r, T t) {
/* ... */
}
}
T1,T2,...Tn被称为 Type Parameter
(类型参数,详见Type Parameter 和 Type Argument )
泛型类和方法的使用
Parameterized Types和Raw Types
如果生成泛型类对象时指定了具体的Type Argument,就称之为Parameterized Types,例如下面这句就生成了Parameterized Types为Integer的ArrayList.
new ArrayList<Integer>();
如果生成时没有指定Type Argument,就称为 Raw Types,例如下标这句生成了Raw Types的ArrayLis
//编译时会提示 Unchecked assignment: 'java.util.ArrayList' to 'java.util.List<java.lang.Integer>'
List<Integer> list=new ArrayList();
为什么会有Raw Types呢? 假设现在有一个JDK5之前编写的方法,在Raw Types的支持下,老代码无需修改就能获得泛型的便利,我们可以直接这样写,一方面减轻了用户更新JDK版本的难度,另一方面保证了JDK的兼容性.
public List getList(){/*...*/}
List<String> result=getList()
基本用法
/**
*创建泛型类对象
*/
new class_name()<>();
/**
*手动指定Type Argument的泛型方法
*/
obj.<Type argument>method_name(args)
class A<T> {
//使用类Type Parameter的方法
public void method1(T t) {}
//泛型方法
public <R> void method2(R r){}
//混合类Type Parameter和方法Type Parameter的方法
public <R> void method3(R r, T t) {}
}
//创建泛型类对象
A<String> a = new A<>();
//1 Parameterized Types在泛型类对象创建的时候已经确定了,A的 Parameterized Types是String
a.method1("abc");
//2 Parameterized Types会根据传入argument的类型自动推导为String
a.method2("abc");
//3 显式指定了Parameterized Types,不再使用自动推导.签名为method1(Long t)
a.<Long> method2(0L);
//4 来自泛型类的Type Parameter由泛型类确定,方法自身的泛型参数由传入的argument决定
a.method3(0, "abc");
Java中的泛型实现原理
Java中的泛型使用类型擦除方式实现,JVM并不知道泛型,所有的泛型在编译阶段就已经被处理成了普通类和方法,并体现在生成字节码上
擦除原理代码演示
public static void eraseTest() {
List l1 = new ArrayList();
l1.add("abc");
l1.add(Integer.valueOf(123));
String s1 = (String) l1.get(0);
Integer i1 = (Integer) l1.get(1);
List<String> l2 = new ArrayList();
l2.add("abc");
// IDE报错提示: 类型不符
// l2.add(Integer.valueOf(123));
String s2 = l2.get(0);
List<String> l3 = new ArrayList<>();
l3.add("abc");
// IDE报错提示: 类型不符
// l3.add(Integer.valueOf(123));
String s3 = (String) l1.get(0);
}
//对应的字节码
public static void eraseTest();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=7, args_size=0
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_0
8: aload_0
9: ldc #4 // String abc
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: aload_0
18: bipush 123
20: invokestatic #6 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
23: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
28: pop
29: aload_0
30: iconst_0
31: invokeinterface #7, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
36: checkcast #8 // class java/lang/String
39: astore_1
40: aload_0
41: iconst_1
42: invokeinterface #7, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
47: checkcast #9 // class java/lang/Integer
50: astore_2
51: new #2 // class java/util/ArrayList
54: dup
55: invokespecial #3 // Method java/util/ArrayList."<init>":()V
58: astore_3
59: aload_3
60: ldc #4 // String abc
62: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
67: pop
68: aload_3
69: iconst_0
70: invokeinterface #7, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
75: checkcast #8 // class java/lang/String
78: astore 4
80: new #2 // class java/util/ArrayList
83: dup
84: invokespecial #3 // Method java/util/ArrayList."<init>":()V
87: astore 5
89: aload 5
91: ldc #4 // String abc
93: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
98: pop
99: aload_0
100: iconst_0
101: invokeinterface #7, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
106: checkcast #8 // class java/lang/String
109: astore 6
111: return
使用绑定的类型(或Object)替换泛型中的Type Parameter,这样生成的字节码中,只包含原始的类,接口和方法
首先看 0~4,51~55,80~84
三种创建泛型类对象的方式生成的字节码是相同的,都是
// Method java/util/ArrayList."<init>":()V
再看11,62,93
,add方法的签名也都相同,参数都是Object而非具体的Type Argument
InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
为了保证类型安全,在必要的地方加入类型转换
而36,75,106
中 36对应的字节码checkcast
是由源代码编译出的,而75,106都是编译器对泛型处理后自动生成的
在有继承的泛型类中使用桥接方法(bridge methods)保证多态
class A {
public <T> void method(T t) {
}
}
class B extends A {
@Override
public <T> void method(T t) {
}
}
//类A对应字节码
public void method(T);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 15: 0
Signature: #14 // (TT;)V
//类B对应字节码
public void method(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 23: 0
//桥接方法
public void method(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/String
5: invokevirtual #4 // Method method:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 19: 0
泛型擦除后,A中method签名为 method(Object)
而B继承Parameterized Types为String的A后,重写的method签名为method(String)
,这就导致多态出现问题, 为了解决这个问题,编译器会自动生成一个桥接方法, 签名为 method(Object)
,这个方法内部去调用B重写的method. 保证多态.
高级用法
定义时限定Type Parameter上界
限定上界,extends后如果跟随多个,第一个必须为类或抽象类,后面的必须为接口,使用时Type Argument 必须同时满足所有限定条件
/**
*限定上界的类定义
*/
class class_name <T extends class_or_ abstract_class_name&interface_name&interface_name>
/**
* 限定上界的方法定义
*/
public <T extends class_or_ abstract_class_name&interface_name&interface_name> void method2(T t)
//eg.
class C<T extends Number> extends ArrayList<T> {
}
public <T extends Number & Serializable> void method2(T t) {
}
没有限定下届的语法,个人认为是意义不大,所以没有.
通配符?
在泛型中的应用
PECS(Producer Extends Consumer Super)规则
- 如果一个对象频繁向外读取数据的,就被称为 "in" ,适合用上界 extends
- 如果一个对象经常向里插入数据,就被称为"out"适合用下界super
场景设定: Plate可以装载物品, Food,Fruit,Apple,Banana有继承关系.
static class Plate<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
static class Food {
}
static class Fruit extends Food {
}
static class Apple extends Fruit {
}
static class Banana extends Fruit {
}
限定上界
Plate<Apple> applePlate = new Plate<>();
applePlate.setT(new Apple());
Plate<? extends Fruit> p = applePlate;
// 编译错误,类型不符
// p.setT(new Apple());
Fruit f = p.getT();
f.printName();
不能存,只能取, 且取出的是上界对应的对象. <? extends Fruit>只知道里面存的是Fruit或Fruit的子类,标记为#A,具体是什么不知道,想插入数据时不知道和#A是否匹配,所以存不进去,取出时同理,只能当做Fruit取出.
限定下界
Plate<? super Fruit> plate = new Plate<>();
plate.setT(new Fruit());
Object obj = plate.getT();
((Fruit) obj).printName();
能存,存的必须是Fruit或Fruit的直接/间接父类, 可以取,因为Object是所有类的基类,因此只能当做Object取
无界
Plate<Apple> applePlate = new Plate<>();
applePlate.setT(new Apple());
Plate<?> plate = applePlate;
Object obj=plate.getT();
((Apple) obj).printName();
不能存,只能取,且取出时只能为Object.它有两个用途
- 泛型类仅用到Object中定义的方法, 例如 Object.hashcode().
- 泛型类不依赖Type Parameter的类型.
<?>和<Object>是有差别的,<?>只能取,不能存, 而<Object>是可以作为Object存的.
为什么用了通配符?某些场景下不能存,某些可以
上面讲过类型擦除为了保证类型安全,在必要的地方加入类型转换,这就要有有一个确定的类型,<? extends X> 可以确保是X或X的子类,由于类的继承原理: X的子类可以强转为X,添加强转是使用X即可.因此可以存,而<? super X>和<?> 都没有确定的类型,都表示一个范围,因此不能存.
泛型的继承关系
泛型不会继承普通类的继承关系
Untitled.png要完成泛型中的继承,需要使用 ?, ?在继承中对泛型的作用类似于Object.
Untitled 1.png擦除的弊端
上面说过,擦除法是在编译进阶擦除类型信息,这也就导致运行时获取不到类型信息.
- reifiable type: 运行时全部信息都可以获取到的types,包含primitive,no-generic,raw(未指定实际类型的泛型),无界类型(如List<?>)
- no-reifiable type: 信息已在编译时因类型擦除被移除的类型,例如List<String>和List<Number>,JVM无从得知两者运行时的差别,很多场景下 non-reifiable是无法使用的
泛型弊端演示
- 无法编译的
<?>
下面这段代码是无法编译通过的,原因上面的完全wildcard讲过
void foo(List<?> i) {
i.set(0, i.get(0));
}
要解决这个问题,可以添加一个辅助方法
void foo(List<?> i) {
forHelp(i);
}
<T> void forHelp(List<T> i) {
i.set(0, i.get(0));
}
- WildcardErrorBad
void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
Number temp = l1.get(0);
l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
// got a CAP#2 extends Number;
// same bound, but different types
l2.set(0, temp); // expected a CAP#1 extends Number,
// got a Number
}
这边这段代码从语法上就是错误的,但是报错信息可能会让人迷惑,现在假设有这些数据
List<Integer> l1 = Arrays.asList(1, 2, 3);
List<Double> l2 = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(l1, l2);
很明显, l2中的数据是Double类型,而l1中的数据时Integer类型,当然无法set.
- Cannot Instantiate Generic Types with Primitive Types原始类型无法作为泛型实例化Type
//compile error!
List<int,int> il
- Cannot Create Instances of Type Parameters无法创建Type Parameters的实例
public static <E> void append(List<E> list) {
E elem = new E(); // compile-time error
list.add(elem);
}
一种可行的解决方案
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
- Cannot Declare Static Fields Whose Types are Type Parameters不能声明泛型静态域
//compile error
public class MobileDevice<T> {
private static T os;
// ...
}
- Cannot Use Casts or instanceof With Parameterized Types不能对Parameterized Type使用cast或instanceof
//ArrayList<Integer>, ArrayList<String> ,LinkedList<Character>都可能被传递进去,而runtime并不会追踪Type Parameters
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
// ...
}
}
// compile-time error
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li;
//OK
List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1;
- Cannot Create Arrays of Parameterized Types不能创建Parameterized Type的数组,编译期被禁止,因为运行时无法分辨
Object[] stringLists = new List<String>[2]; // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>(); // OK
stringLists[1] = new ArrayList<Integer>(); // An ArrayStoreException should be thrown,
// but the runtime can't detect it.
Cannot Create, Catch, or Throw Objects of Parameterized Types不能创建,捕捉或抛出Parameterized Type
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // compile-time error
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
class Parser<T extends Exception> {
public void parse(File file) throws T { // OK
// ...
}
}
- Cannot Overload a Method Where the Formal Parameter Types of Each Overload Erase to the Same Raw Type不能重载擦除后raw type相同的方法
// compile error
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}
后记
写这篇文章的起因是在做一个需求的时候,同事用泛型很好的封装了一个基础组件,优雅简洁,我却完全没有想到,还在傻傻的强转类型转换. 很早之前我就研究过泛型并且做了笔记, 趁这次机会回顾实践一下并写了这篇文章.
网友评论