Java 泛型

泛型是 Java SE 1.5 的新特性。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,即在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。

泛型可以用在类、接口和方法中使用,分别被称为泛型类、泛型接口、泛型方法.

泛型的好处是在编译时检查类型安全,并且所有的强制转换都是自动的和隐式的,提高了代码的重用率。

泛型的使用

java.util.Collection 为例(JDK 1.8 版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface Collection<E> extends Iterable<E> {
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean contains(Object o);
boolean containsAll(Collection<?> c);
// 可以添加 E 以及是 E 子类的集合。
boolean addAll(Collection<? extends E> c);

default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
}

泛型类

泛型类是在实例化类的时候指明泛型的具体类型。

1
2
3
4
5
public class GenericClass<A, B, C> {
private class InnterClass {
A a;
}
}

继承泛型类:

1
2
3
4
5
6
7
8
9
10
11
// 忽略泛型
public class GenericClassChild extends GenericClass{
}

// 指定具体类型
public class GenericClassChild extends GenericClass<String, Integer, Date>{
}

// 继承泛型信息
public class GenericClassChild<A, B, C, D> extends GenericClass<A, B, C>{
}

静态的属性、静态方法、和静态内部类是无法使用类的泛型参数的。如果要使 static 方法具有泛型能力,可以使用泛型方法。

泛型接口

1
2
3
4
5
public interface GenericInterface<A, B, C> {
void methodA(A args);
void methodB(B args);
void methodC(C args);
}

泛型方法

泛型方法是在调用方法的时候指明泛型的具体类型 。只有声明了 <T> 的方法才是泛型方法。静态方法无法访问类上定义的泛型。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class GenericClass<E> {

/**
* 声明泛型方法
*
* @param t 泛型参数
* @param <T> 声明一个泛型 T
* @return 返回 T 的实例
*/
public static <T> T getObject(T t) {
return t;
}

/**
* 成员方法。和类上的泛型信息一致。
*/
public E getObject1(E e) {
return e;
}

/**
* 泛型方法。和类上的泛型信息没有关系。
*/
public <T> T getObject2(T t) {
return t;
}

/**
* 显示警告:Type parameter 'E' hides type parameter 'E'
*/
public <E> E getObject3(E t) {
return t;
}

/**
* 静态方法。虽然和类中定义的泛型变量一致(都是 E),但是没有警告。
*/
public static <E> E getObject4(E t) {
return t;
}

public static <T> void print(T... ts) {
for (T t : ts) {
System.out.println(t);
}
}

public static <K, V> Map<K, V> newMap() {
return new HashMap<K, V>();
}
}

类型擦除

泛型是通过类型擦除(type erasure)来实现的,在编译时将泛型类型转换为原始类型,并在运行时丢失泛型类型的具体信息。Java 使用擦除的方式来实现泛型是为了保持与旧版本的兼容性,并且避免增加额外的负担和复杂性。

泛型擦除具体来说就是找到用来替换类型参数的具体类。这个具体类一般是 Object,如果指定了类型参数上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类,同时去掉出现的类型声明,即去掉<>的内容。接下来就可能需要生成一些桥接方法(bridge method),这是由于擦除了类型之后的类可能缺少某些必须的方法。如果调用泛型方法的返回类型被擦除则在调用该方法时强制类型转换。

在 Java 中,虽然在编译时会将泛型类型擦除为原始类型,但是在运行时,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
public class GenericType<T> {
private T item;

public void set(T t) {
item = t;
}

public T get() {
return item;
}

public static void main(String[] args) {
GenericType<Integer> type1 = new GenericType<>();
GenericType<String> type2 = new GenericType<>();
// 匿名内部类
GenericType<String> type3 = new GenericType<String>() {
};
System.out.println(type1.getClass().getName());// GenericType
System.out.println(type2.getClass().getName());// GenericType
System.out.println(type3.getClass().getName());// GenericType$1

Type type = type3.getClass().getGenericSuperclass();
Type typeArgument = ((ParameterizedType) type).getActualTypeArguments()[0];
System.out.println(typeArgument.getTypeName());// java.lang.String
}
}

泛型类的原生类型与所传递的泛型无关,无论传递什么类型,原生类是一样的。

1
2
3
4
5
6
7
// HelloWorld.java
import java.util.List;

public class HelloWorld<T>{
private List<String>[] array;
private T[] tArray;
}

“javap -l -p -v -c HelloWorld” 反编译如下:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class HelloWorld<T extends java.lang.Object> extends java.lang.Object
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // HelloWorld
#3 = Class #20 // java/lang/Object
#4 = Utf8 array
#5 = Utf8 [Ljava/util/List;
#6 = Utf8 Signature
#7 = Utf8 [Ljava/util/List<Ljava/lang/String;>;
#8 = Utf8 tArray
#9 = Utf8 [Ljava/lang/Object;
#10 = Utf8 [TT;
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 <T:Ljava/lang/Object;>Ljava/lang/Object;
#16 = Utf8 SourceFile
#17 = Utf8 HelloWorld.java
#18 = NameAndType #11:#12 // "<init>":()V
#19 = Utf8 HelloWorld
#20 = Utf8 java/lang/Object
{
private java.util.List<java.lang.String>[] array;
descriptor: [Ljava/util/List;
flags: ACC_PRIVATE
Signature: #7 // [Ljava/util/List<Ljava/lang/String;>;

private T[] tArray;
descriptor: [Ljava/lang/Object;
flags: ACC_PRIVATE
Signature: #10 // [TT;

public HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
}
Signature: #15 // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "HelloWorld.java"

通配符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit{}
class RedApple extends Fruit {}

List<Fruit> fruits = new ArrayList<>();
List<? extends Fruit> fruits2 = new ArrayList<>();
List<Apple> apples = new ArrayList<>();
fruits = apples; //编译不通过,提示 “Required type: List<Fruit> Provided: List<Apple>”
fruits2 = apples; //编译通过

Fruit[] fruitsArray = new Apple[3];
fruitsArray[0] = new Apple();
//可以编译通过,但是提醒 ArrayStoreException;运行时出现 ArrayStoreException 异常。
fruitsArray[1] = new Orange();
fruitsArray[2] = new RedApple();

逻辑上装水果的盘子当然可以装苹果,但实际上 Java 编译器不允许这个操作,会报incompatible types错误,“装苹果的盘子”无法转换成“装水果的盘子”。

编译器是这样认为的:

  • 苹果 IS-A 水果
  • 装苹果的盘子 NOT-IS-A 装水果的盘子

所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的,所以我们不可以把 List<Apple> 的引用传递给 List<Fruit>。要通过 <? extends T> 和 <? super T> 来让”水果盘子“和”苹果盘子“之间发生关系。

通配符 <?> 和类型参数 <T> 的区别在于,对编译器来说所有的 T 都代表同一种类型。Plate<?> 单纯的就表示盘子里放了一个东西,是什么不知道。

1
2
3
4
5
6
7
8
List<?> list = new ArrayList<>();
list.add(""); //编译出错
Object obj = list.get(0);

List<Object> listObj = new ArrayList<>();
listObj.add(new Date());
listObj.add(new Object());
Object o = listObj.get(0);

如果 Apple 是 Fruit 的子类型,那么 Apple[] 也是 Fruit[] 的子类型,我们称这种性质为协变(covariance)。

Java 中的数组是协变的,泛型是不变(invariance)的。通过 extends 关键字实现协变,通过 super 关键字实现逆变。

上界通配符(Upper Bounds Wildcards)

上界通配符

List<?extends Fruit> 表示一个能放水果以及一切是水果派生类的集合,即什么水果都能放的集合。

不能添加任何具体类型的对象,但是可以添加 null,因为 null 没有具体的类型

1
2
3
4
5
6
7
8
List<? extends Fruit> fruits = new ArrayList<>();
fruits.add(new Object()); //编译错误,不能存数据
fruits.add(new Fruit()); //编译错误,不能存数据
fruits.add(new Apple()); //编译错误,不能存数据
fruits.add(null); //编译通过,可以存 null
Fruit fruit = fruits.get(0); //编译通过
Object obj = fruits.get(0); //编译通过
Apple apple = (Apple)fruits.get(0); //编译通过

下界通配符(Lower Bounds Wildcards)

下界通配符

List<?super Apple> 表示一个能放苹果以及一切是苹果基类的集合。

super 不能往里存,只能往外取。编译器无法确定 List 所持有的类型,所以无法安全的向其中添加对象。

存数据只能是 Apple 或 Apple 的子类对象,因为编译器并不知道 List 的内容究竟是 Apple 的哪个超类,因此不允许加入特定的任何超类。取数据编时译器在不知道是什么类型的情况下只能返回 Object 对象,但是元素的类型信息就全部丢失了

1
2
3
4
5
6
7
List<? super Apple> fruits = new ArrayList<>();
fruits.add(new Object()); //编译错误,不能存放 Apple 的父类对象
fruits.add(new Fruit()); //编译错误,不能存放 Apple 的父类对象
fruits.add(new Apple());
fruits.add(new RedApple());
fruits.add(null);
Object apple = fruits.get(0);

PECS 原则

Joshua Bloch 称那些只能从中读取的对象为生产者,那些只能写入的对象为消费者。并提出了以下助记符: PECS(Producer-Extends-取数据, Consumer-Super-写数据)

带有 extends 子类型限定的通配符可以向泛型对象读取,带有 super 超类型限定的通配符可以向泛型对象中写入。

  • “?” 不能添加元素,只能获取元素,所以它只能作为生产者(Producer);

  • extends 只能获取元素,所以是生产者(Producer);

    1
    2
    3
    4
    5
    List<? extends String> producerList = new ArrayList<>();
    // 能更精确地确认元素类型
    String first = producerList.get(0);
    // 编译报错
    producerList.add("1");
  • super 只能添加元素,所以是消费者(Consumer)

    1
    2
    3
    4
    List<? super String> consumerList = new ArrayList<>();
    consumerList.add("");
    consumerList.add(null);
    Object s = consumerList.get(0);

泛型的命名规范

各种常用泛型参数的意义如下:

  • E — Element,常用在 java Collection 里,如:List(E),Iterator(E),Set(E)
  • K,V — Key,Value,代表 Map 的键值对
  • N — Number,数字
  • T — Type,类型,如 String,Integer 等
  • S,U,V etc. - 2nd, 3rd, 4th 类型,和 T 的用法一样

参考

[1] Java 中 ? extends T 和 ? super T 的理解
[2] Java 泛型的协变与逆变