Java
泛型本质是参数化类型 Parametersized Type
的应用,也就是说操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别被称为泛型类、泛型接口和泛型方法。
我们使用尖括号 <>
来表示泛型。Java 7
及以后版本,运行构造器后不需要带完整的泛型信息,只要给出一对尖括号 <>
即可,Java
可以推断出尖括号里应该是什么泛型信息。比如:List<String> list = new ArrayList<>();
。两个尖括号看起来很想菱形,也称为这种用法为菱形语法。
基本概念
类型变量和参数化类型
它们是组成泛型的两个最基本的概念,如泛型 List<T>
:
- 类型变量
type variables
T
,即为类型变量,它只能在类,构造方法,普通方法中定义它。 - 参数化类型
parameterized type
List<T>
这个整体表示参数化类型,可以用来定义变量,如List<String> lists
。在类型擦除后,并不会保留泛型信息,所以参数化类型实际上是类类型(包含接口)。
相关概念从反射角度也有解释:Type
类型
类型形参和实参
泛型允许在定义类、接口、方法时使用类型参数(type parameter
),这个类型参数在声明变量、创建对象、调用方法时动态的指定。
- 类型形参
泛型在定义时的参数,如<K, V>
。 - 类型实参
泛型在使用时指定的参数,即具体参数,如<K, V
在使用时实际指定的<Integer, String>
。类型实参只能是类类型(包含接口),不能是基本数据类型。
1 | // 1. 定义时,为类型形参 E |
泛型接口
1 | public interface GenericInterface<K, V>{ |
定义接口时使用泛型声明,指定两个类型形参 K, V
,在接口中形参名 K, V
可以作为类型来使用。
泛型类
1 | public class GenericClass<T> { |
定义类时使用了泛型声明,指定类型形参为 T
,类中使用形参 T
定义了变量 t
,并使用形参 T
定义了构造方法。
在定义构造方法时不需要使用类型形参,只有在实例化话时才需要。
泛型方法
泛型方法的类型参数不需要在定义类或者接口时声明,只需要在方法返回值前面声明就可以了。
1 | 修饰符 <类型形参列表> 返回值 方法名(方法形参列表){ |
1 | // 1. 泛型方法定义 |
定义方法时使用泛型声明,指定类型形参为 M, N
,方法的形参列表和方法体中都可以将 M, N
作为类型使用。
值得注意的是,构造方法也可以使用泛型。
泛型继承和子类型
重要概念
在面向对象中有个非常重要的使用方式:向上转型,也就是父类变量引用子类对象。比如:
1 | Object someObject = new Object(); |
在方法的形参中,也可以使用向上转型。
1 | public void someMethod(Number n) { /* ... */ } |
在泛型中我们也可以使用向上转型,这也是集合中非常常用的一种方式。
1 | Box<Number> box = new Box<Number>(); |
但是请注意:方法的形参列表在使用泛型时,传入的参数必须完全匹配。
1 | Box<Number> box = new Box<Number>(); |
形参并不能传入 Box<Integer>, Box<Double>
。也就是说 Box<Integer>, Box<Double>
并不是 Box<Number>
的子类型。
泛型使用的重要概念:给定的两个类型
A
和B
,不管A, B
是否存在继承关系,MyGenericClass<A>
和MyGenericClass<B>
都没有任何直接关系,他们的共同父类是Object
。
泛型类型的继承和接口的实现
泛型接口的实现类,泛型类的子类,在定义泛型时需要注意:
- 指定具体类型实参
- 不指定类型参数
- 指定相同的类型形参
1 | // 指定具体类型实参 |
示例中泛型接口的实现类指定具体类型实参 Integer, String
,继承泛型类时不指定类型参数,默认将类型实参设置为 Object
,或者指定相同的类型参数。在实际使用中推荐指定相同的类型参数。
泛型类的类型实参如果不同,则不存在继承关系,即使类型实参之间有继承关系。也就是说泛型中只有类型参数相同时,泛型类才存在继承关系。
1 | interface PayloadList<E,P> extends List<E> { |
PayloadList<String,String>, PayloadList<String,Integer>, PayloadList<String,Exception>
三者的关系如下:
这部分参考教程:Generics, Inheritance, and Subtypes
泛型中类型变量边界限定
语法格式
类型变量 T
可以表示任何类型,通过关键字 extends
可以限制类型变量的边界,语法格式:
1 | // 1. 单个限定 |
表示 T
是“边界类型”的子类型,“边界类型”可以是类也可以是接口,如果不做边界限定,默认边界为 Object
。但是在多限定中,只能有一个类,而且必须放在限定表的第一位;接口的数量和位置没有限制。
示例
1 | public class GenericTypeParaBounder<T extends Number>{ |
泛型通配符
通配符
为了解决类型不能动态根据实例来确定的缺点,引入“通配符泛型 ?
”。但通配符并不是具体的类型,所以代码中不能使用 ?
作为一种类型。
- 不限定通配符
使用?
代替类型实参,即它可以是任何类型,如Class <?>
。 - 限定性通配符上界
extends
<? extends Father>
表示上界,即参数化类型的可能是Father
或是Father
的子类。也可以看出不限定通配符实际上是<? extends Object>
。 - 限定性通配符下界
super
<? super Son>
表示下界,表示参数化类型是Son
的超类型(父类型),直至Object
。
通配符泛型继承
使用通配符可以解决泛型类向上转型时的问题。
- 无限定通配符
List<Number>, List<Integer>
都是List<?>
的子类。
- 限定通配符
List<Number>, List<Integer>, List<Long>, List<Float>
都是List<? extends Number>
的子类。List<Integer>, List<Number>, List<Object>
都是List<? super Integer
的子类。
- 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 泛型中使用向上转型时,Integer 继承 Number
// 但是 List<Number> 和 List<Integer> 并没有继承关系
List<Integer> li = new ArrayList<>();
List<Number> ln = li; // compile-time error
// 1. 无限定通配符
List<?> l = li; // OK
// 2. 上界
List<? extends Integer> eli = new ArrayList<>();
List<? extends Number> eln = eli; // OK
// 3. 下界
List<? super Number> sln = new ArrayList<>();
List<? super Integer> sli = sln; // OK
通配符集合操作注意事项
在集合中,对于边界,上界不能
add
,下界不能get
。
首先我们需要明白集合中限定性通配符的意义:
1 | List<? extends Number> ln = new ArrayList<>(); |
- 上界不能
add
边界
1 | List<? extends Number> ln = new ArrayList<>(); |
将 get/add
的类型展开:
1 | ? extends Number get(int i); |
ln = {List<Number>, List<Integer>, List<Long>, List<Float>...}
,ln
是其中某一类型,边界是 Number
。在 get
时不管拿到的是哪种类型 Number, Integer, Long, Float
,都可以向上转型到边界 Number
。而如果 ln
当前是 List<Float>
,在 add
时添加边界 Number
会存在不安全的向下转型,导致编译报错。
- 下界不能
get
边界
1 | List<? super Integer> li = new ArrayList<>(); |
将 get/add
的类型展开:
1 | ? super Integer get(int i); |
li = {List<Integer>, List<Number>, List<Object>...}
,li
是其中某一类型,边界是 Integer
。在 add
边界 Integer
时,不管 li
是哪种类型,Integer
都能正确的向上转型到 Integer, Number, Object
。而如果 li
当前是 List<Object>
,在 get
时转换为边界 Integer
是不安全的向下转型,导致编译错误。
类型擦除
Java
的泛型只在源码中存在,在编译后的字节码文件中都会被替换为原始类型 Raw Type
,并且在相应的地方插入了强制转型代码。 Java
语言泛型的实现方法称为类型擦除(Type Erasure
),这种实现也被称为伪泛型。
类型擦除后的原始类型为限定类型(如果是不限定则为 Object
)。
类型擦除示例
1 | // 1. 源码 |
JVM
中关于方法的几个基本概念
Class
文件格式中方法表,包含的几个重要字段:名称、描述符、属性组等。
- 特征签名
方法特征签名:仅仅包括方法名称、参数类型以及参数顺序(不包含返回值)。 - 描述符
描述符的作用用来描述字段的数据类型、方法的参数列表(包含数量、类型和顺序)和返回值,只有描述符不一致的两个方法才能再同一个Class
文件中共存。 Sigature
属性
在Java
语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或者参数化类型,则Signature
属性会记录泛型签名信息。所以类型擦除仅仅是对方法的字节码进行了擦除,实际上Signature
属性保留了泛型信息。
泛型方法不能重载
如下示例中方法 mytest
重载,但是参数类型都是泛型 List<E>
。
1 | import java.util.List; |
关于泛型方法的重载,Java 6
和以后的版本表现完全不一样:
Java 6
能通过编译并正常运行
在Java 6
只需要class
文件中方法描述符(返回值不同)不一致就可以共存,并且Singture
属性保留了泛型信息,所以能正常编译和运行。Java 7
及以后版本不能正常编译
从Java 7
开始,编译开始就检查泛型的特征签名(不包含返回值)。根据特征签名的定义,以及泛型的类型擦除特性,mytest
特征签名是一样的,所以在编译过程中报错。
1 | // Java 7 及以上不能编译通过 |
泛型方法重写
泛型方法支持重写,支持的原因是 Java
编译器会自动为重写方法添加一个适配的方法,这个方法称为桥方法。
源码
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
27
28
29
30
31
32public 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 void testGenericOverride(){
MyNode mn = new MyNode(5);
// A raw type - compiler throws an unchecked warning
Node n = mn;
n.setData("Hello");
// Causes a ClassCastException to be thrown.
Integer x = mn.data;
}
}类型擦除后
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
27
28public 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);
}
}
public void testGenericOverride(){
MyNode mn = new MyNode(5);
//A raw type-compiler throws an unchecked warning
Node n = (MyNode)mn;
n.setData("Hello");
//Causes a ClassCastException to be thrown.
Integer x = (String)mn.data;
}
}
类型擦除后,可以看出 setData
的参数类型并不一样,也就说 setData
在重载。那 Java
中是如何实现重写的呢?
- 桥方法
反编译MyNode
的字节码: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
27
28
29
30
31
32
33xmt@server005:~/$ javap -v Node\$MyNode.class
...
public void setData(java.lang.Integer);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String MyNode.setData
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: aload_1
10: invokespecial #6 // Method Node.setData:(Ljava/lang/Object;)V,调用桥方法
13: return
LineNumberTable:
line 19: 0
line 20: 8
line 21: 13
// Java 编译器自动添加的 桥方法
public void setData(java.lang.Object);
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #7 // class java/lang/Integer
5: invokevirtual #8 // Method setData:(Ljava/lang/Integer;)V
8: return
LineNumberTable:
line 14: 0
}
...
从反编译的字节码中可以看出,多了一个 setData(Object)
,这个就是编译器自动添加的桥方法,从而实现了重写。
泛型数组
不能直接创建泛型数组
1 | // 1. Normal Array. |
数组在使用中,数组元素的类型必须一致,否则在运行时会报错 An ArrayStoreException is thrown
,比如字符串数组 strings[1] = 100;
赋值一个整数导致运行时报错。同样,如果创建了泛型数组,虽然类型不一致,但是类型擦除后,无法确认具体的类型,运行时并不能检测到错误,所以泛型中禁止直接创建泛型数组。
通配符创建泛型数组
1 | // 不安全的向下转型 List<String> 是 List<?> 的子类 |
The Java™ Tutorials: Generics ,官网教程给出使用通配符来创建泛型数组。这个示例在运行时,最后一句会抛出类型转换错误 Integer
不能直接转换为 String
。
反射创建泛型数组
1 | List<String>[] lsa = (List<String>[])Array.newInstance(ArrayList.class, 4); |
使用反射创建数组,并强制转换为泛型数组,此处会给出 Uncheck
警告,没有做类型检查。
显性转换
不管是通配符还是反射创建的泛型数组,在都需要做一次显示的类型转换。通配符是在使用时做转换,反射是在创建时转换。
注意事项 Restrictions on Generics
Java
在泛型使用中有如下约束规则和限制,大都是类型擦除引起,参考Restrictions on Generics
不能使用基本数据类型定义类型参数
泛型类型形参只能是类类型,不能是基本数据类型。
1 | Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error |
不能使用类型参数创建实例
不能使用类型参数来创建实例,但是可以通过反射调用 Class.newInstance
方法来构造泛型对象。
1 | // 1. Error |
泛型类的静态上下文
即在类上定义的泛型,不能在当前类的静态属性,静态方法,静态代码块,静态类中出现。因为 static
属于类的,只加载一次,在类型擦除后,无法转换为每个具体的类型实参。
1 | public class MobileDevice<T> { |
根据上面示例,os
将出现混淆,无法确定具体的实际类型。所以泛型类中不允许出现在静态上下中。其他错误示例:
1 | public class GenericRestrictions<Z> { |
但是我们可以脱离静态上下文(也就是不使用类上定义的泛型),重新指定泛型,这样就可以出现在泛型方法和静态类中了:
1 | // 静态泛型方法 |
泛型中类型转换 cast
和 instanceof
cast
因为泛型中只有类型参数是同一类型时,才可能存在继承关系;类型参数不同,泛型类没有任何关系。
1 | // 参数类型存在继承关系,泛型却没有任何关系 |
instanceof
类型擦除后,并不知道运行时参数类型具体是什么?
1 | public static <E> void rtti(List<E> list) { |
方法 rtti
在运行时,传入的参数类型可能是这些:S={ArrayList<Integer>, ArrayList<String>, LinkedList<Character>...}
,而根据泛型重要概念了解到只有参数类型完全一致才存在继承关系,所以 instanceof
中泛型并不允许直接使用类型实参。
1 | public static void rtti(List<?> list) { |
即:instanceof
中只能使用通配符的泛型。
不能创建泛型数组
正常的数组定义和使用中,如果类型不匹配会抛出异常:
1 | Object[] strings = new String[2]; |
但是在泛型中,类型擦除后,并不能检查出 ArrayList<Integer>()
的错误,所以泛型禁止创建数组。
但是可以使用泛型数组?????
泛型异常
泛型类不能直接或间接继承
Throwable
1
2
3
4
5// Extends Throwable indirectly
class MathException<T> extends Exception {} //compile-time error
// Extends Throwable directly
class QueueFullException<T> extends Throwable {} //compile-time error不能
catch
泛型实例1
2
3
4
5
6
7
8public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs){}
// ...
} catch (T e) { // compile-time error
// ...
}
}
但是我们可以 throws
泛型实例。
1 | class Parser<T extends Exception> { |
泛型比较
1 | public final class Algorithm { |
泛型不支持直接使用关系运算符 ><
等。通常限定类型变量边界为 Comparable
并通过 compareTo
来比较。
参考文档
Java
核心技术卷 1:第 12 章 泛型程序设计- 深入理解
Java
虚拟机:JVM
高级特性与最佳实践 第 2 版 - 10.3.1 泛型与类型擦除 - The Java™ Tutorials – Lesson: Generics
- The Java™ Tutorials – Lesson: Generics: advanced users
- Java 泛型重载 jdk 1.7
- Java泛型的实现:泛型数组
- Java泛型 - 桥方法
- Java泛型: 泛型的内部原理