Java 中的泛型

关于 Java 中泛型的使用可以参考:Java-泛型

Effective Java 中的泛型要点

术语介绍

术语 示例
类型参数 E
泛型 List
参数化类型 List
原生态类型 List
无限制通配符类型 List<?>
有限制通配符类型 List<? extentds Object>
有限制通配符类型 List<? super Object>
有限制类型参数 List

不要使用原生态类型

声明中具有一个或者多个类型参数的类或者接口就是泛型类或者泛型接口,泛型类和泛型接口统称为泛型

每个泛型都对应一个原生态类型,即不带任何实际类型参数的泛型名称,例如 List<E> 对应的原生态类型是 List。原生态类型 List 和 Java 平台没有泛型之前的接口类型 List 完全一样。

没有泛型之前,你可以向原生态类型中添加任何类型的数据,并且可以成功编译运行,直到获取其中的数据并将其强制转化为指定的类型时才会出错。

使用泛型相比之前,有以下好处:

  • 错误前置,可以将运行时错误提前到编译期间,确保了编译期的类型安全,只能使用正确类型的对象。
  • 避免强制类型转换,获取数据直接是想要的类型数据

因此,新代码中不要使用原生态类型,它们逃避了泛型检查,没有类型安全性。

无限制通配符类型

在不确定或者不在意集合中元素类型的情况下,你也许会使用原生态类型。例如,假设想要编写一个方法,它有两个集合参数,需要从中返回它们共有的元素数量。一种实现如下:

1
2
3
4
5
6
7
8
9
public int numElementsInCommon(Set a, Set b) {
int num = 0;
for (Object o : a) {
if (b.contains(a)) {
num++;
}
}
return num;
}

这种实现是可以的,但它使用了原生态类型,这是危险的。如果要使用泛型,但是不确定或者不关心实际的参数类型,就可以使用一个 ? 代替。上面的代码安全实现方式如下:

1
2
3
4
5
6
7
8
9
public int numElementsInCommon(Set<?> a, Set<?> b) {
int num = 0;
for (Object o : a) {
if (b.contains(a)) {
num++;
}
}
return num;
}

Set<?> 相比 Set 肯定是安全的,Set 允许放入任何类型的元素,很容易破坏该集合的类型约束条件。但是你不能将任何元素(除了 null)放入 Set<?> 中,如果你做了,编译器会拒绝编译。

两个例外

先给出这两个例外情况:

  1. 在类文字中,必须使用原生态类型。也就是说 List.class、String[].class、int.class 是合法的,但是 List.class 是不合法的。
  2. instanceof 不可以用于参数化类型,但是可以用于无限制通配符类型和原生态类型,在这种情况下 <> 和 ? 就多余了,所以直接使用原生态类型。

注意:

  • 上面这两个例外都是由于泛型信息在运行时被擦除导致的,泛型擦除的内容可以看后面的内容。
  • 目前还允许使用原生态类型的原因是为了提供兼容性,为了兼容大量没有使用泛型的 Java 代码。

列表优先于数组

数组相比泛型有两个重要的不同点:

  • 数组是协变的(convariant),泛型是不可变的(invariant)
  • 数组是具体的(reified),泛型是可以被擦除的

首先说说数组是协变的,它的意思就是说如果 SubSup 的子类型,那么数组类型 Sub[] 就是 Sup[] 的子类型。但是针对 List<String>List<Object>,虽然 StringObject 的子类,但是 List<String>List<Object> 并没有任何关系,但是我们可以说 List<String> 是原生态类型 List 的一个子类。

看下面的代码:

1
2
3
4
5
Object oArray = new Long[0];
oArray[0] = "test";

List<Object> oList = new ArrayList<Long>()
oList.add("test");

上面两种方法都不能将 “test” 放入 Long 容器中,但是数组需要到运行时才能发现错误,而集合可以做到编译的时候就发现错误。

其次说说数组是具体化的,因此数组会在运行时才知道并检查它们的元素类型约束。如上所示,如果企图将 String 实例保存到 Long 数组中就会得到一个 ArrayStoreException。相比之下,泛型是通过擦除实现。因此泛型只在编译时强化它们的类型信息,并在运行时丢弃类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。

由于上述原因,数组和泛型并不能很好的混用,所以当混用后出现错误或者警告,第一反应是用列表代替数组。

有限制通配符的使用

上面说到泛型是不可变的,也就是说 List<String>List<Object> 是任何没有关系的,List<String> 不是 List<Object> 的子类型。可是,有时候我们的灵活性要比不可变类型所能提供的更多。先看下面一个例子:

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
public class Stack<E> {
private static final int DEFAULT_SIZE = 10;
private E[] elements;
private int size = 0;

@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_SIZE];
}

private void push(E e) {
if (size == DEFAULT_SIZE) {
throw new StackOverflowError();
}
elements[size++] = e;
}

private E pop() {
if (size == 0) {
throw new EmptyStackException();
}
E result = elements[size--];
elements[size] = null;
return result;
}

public void putAll(List<E> lists) {
for (E list : lists) {
push(list);
}
}

public void popAll(List<E> lists) {
for (E element : elements) {
lists.add(element);
}
}
}

先说说 putAll 方法,下面的使用从逻辑上来说应该也是可以的,但实际上会编译错误。

1
2
3
4
Stack<Number> stack = new Stack<>();
List<Integer> lists = new ArrayList<>();
...
stack.putAll(lists);

幸运的是,可以通过有限制的通配符类型来处理类似的情况。比如上面的问题,我们只需要对 putAll 方法进行简单的修改即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void putAll(List<? extends E> lists) {
for (E list : lists) {
push(list);
}
}

// 生产者:可以使用 lists 中的数据,不能向 lists 中添加数据
// 1.可以使用其中数据的原因:
// 因为满足条件 ? extends E,所以 lists 中的数据类型 ? 一定是 E 的子类型。
// 根据多态的特性,lists 中的数据肯定可以添加到 elements 中
// 2.不能向其中添加数据的原因:
// 只知道满足条件 ? extends E,假设 E 是 View,那么 ? 可以是 TextView,也可以是 Button 或其它子类。
// 编译器无法确定到底属于哪一种,所以不给执行。

注意:前面提到的有限制通配符类型 <?> 其实是 <? extends Object> 的缩写

再说说 popAll 方法,下面的使用从逻辑上来说应该也是可以的,但实际上也会编译错误。

1
2
3
Stack<Number> stack = new Stack<>();
List<Object> lists = new ArrayList<>();
stack.popAll(lists);

针对上面的情况我们同样可以使用有限制的通配符类型来解决,不过稍有不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void popAll(List<? super E> lists) {
for (E element : elements) {
lists.add(element);
}
}

// 消费者:可以向 lists 中添加数据,不能使用 lists 中的数据,
// 1.可以向其中添加数据的原因:
// 因为满足条件 ? super E,所以 E 一定是 lists 中数据类型 ? 的子类型。
// 根据多态的特性,elements 中的数据肯定可以添加到 lists 中。
//
// 2.不能使用其中数据的原因:
// 因为满足条件 ? super E,lists 中数据类型 ? 一定是 E 的父类。
// 因此 lists 中数据类型肯定不能加入到 elements 中。

至于选择 extends 还是 super 可以参照 PECS 准则:producter-extends, consumer-super。

<? extends Object><E extends Object> 的区别:

  • <E extends Object> 是有限制类型参数,它要求实际的类型参数必须是 Object 的子类,主要是为了 E 可以使用 Object 上的方法。
  • <? extends Object> 是有限制的通配符类型,和 <E extends Object> 没有直接关系。

泛型擦除

参考文章:Java 泛型擦除以及擦除带来的问题

Java 泛型是伪泛型,它是通过在编译期间将所有的泛型信息擦掉来实现的。Java 泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含类型信息的。

比如代码中使用的 List<String>List<Integer>,编译器是看不到添加的 StringInteger 信息的,它们在编译后都会变成 List。可以通过反射证明这个想法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
try {
ArrayList<Integer> lists = new ArrayList<>();
lists.add(1);
lists.getClass().getMethod("add", Object.class).invoke(lists, "test");
for (int i = 0; i < lists.size(); i++) {
System.out.println("lists" + i + ":" + lists.get(i));
}
} catch (Exception e) {
System.out.println("ss:" + e.getMessage());
}
}

//运行结果
lists0:1
lists1:test

上面的例子说明了 List<Integer> 类型信息在编译的时候被擦除了,只保留了原始类型。那么这个原始类型到底是什么呢?原始类型就是擦除了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型,其原始类型都会被自动提供。

类型擦除后,类型变量默认使用 Object 类型。如果有限定类型信息,类型变量就使用限定类型。 例如下面这个泛型以及对应的原始类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Pair<T> {  
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}

public class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}

因为在 Pair<T> 中,T 是一个无限定的类型变量,所以用 Object 替换,其结果就是一个普通的类,如同泛型加入 Java 之前已经实现的样子。

如果 T 有限定,那就用限定的类型来替换,例如下面就会用 Comparable 来代替类型变量的类型。

1
2
3
public class Pair<T extends Comparable> {
//...
}

面试中的问题

Java 泛型面试题

基于泛型实现 LRU 缓存

Kotlin 中的泛型

Kotlin 的泛型和 Java 的泛型一致,它们都是伪泛型,原理上十分相似,这里简单只说说 Kotlin 中不一样的地方。

out 和 in

先看下面的例子,这是刚才说 Java 有限制通配符类型时候提到的例子,改写成 Kotlin 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Stack<E> {
private val elements = mutableListOf<E>()

fun push(e: E) {
elements.add(e)
}

fun pop(): E {
val temp = elements.first()
elements.remove(temp)
return temp
}

fun putAll(lists: MutableList<E>) {
elements.addAll(lists)
}

fun popAll(lists: MutableList<E>) {
lists.addAll(elements)
}
}

out

这里的 putAll 和 Java 中一样,也会由于不协变的原因,导致下面的使用会出错:

1
2
3
val stacks = Stack<Number>()
val list = mutableListOf<Int>(1, 2, 3)
stacks.putAll(list)

在 Java 中使用 ? extends 解决这个问题,在 Kotlin 中使用 out 来解决:

1
2
3
fun putAll(lists: MutableList<out E>) {
elements.addAll(lists)
}

in

针对下面的使用,也会由于不协变的原因导致不可编译。

1
2
3
val stacks = Stack<Number>()
val list = mutableListOf<Any>()
stacks.popAll(list)

在 Java 中使用 ? super 解决这个问题,在 Kotlin 中使用 in 来解决:

1
2
3
fun popAll(lists: MutableList<in E>) {
lists.addAll(elements)
}

注意:

  • Java 中的 List 不支持协变的,但是 Kotlin 中的 List 是支持协变的,不支持协变的是 MutableList
  • Java 里面的数组是支持协变的,但是 Kotlin 里面的数组 Array 是不支持协变的。

*

前面有说到 Java 中有无限制通配符类型 ?,它相当于 ? extends Object。Kotlin 中与 ? 对应的符号是 *,它相当于 out Any

1
val list: List<*>

和 Java 不同的地方是,如果你的类型定义里已经有了 out 或者 in,那这个限制在变量声明时也依然在,不会被 * 号去掉。

比如你的类型定义里是 out T : Number 的,那它加上 <*> 之后的效果就不是 out Any,而是 out Number

1
2
3
4
5
6
interface Counter<out T : Number> {
//nop
}

// 下面这个实际上是:Counter<out Number>
val counter: Counter<*>

Kotlin 和 Java 不同,它不再保留原生态类型了。

where

前面 Java 说到有限制类型参数的时候,可以使用 extends 来设置上界:

1
2
3
4
// T 的类型必须是 Animal 的子类型
class Monster<T extends Animal>{
// nop
}

如果有多个上界,需要使用 & 连接:

1
2
3
class Monster<T extends Animal & Food>{ 
// nop
}

上面两个在 Kotlin 中对应的写法如下:

1
2
3
class Monster<T : Animal>

class Monster<T> where T : Animal, T : Food

reified

由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。

比如你不能检查一个对象是否为泛型类型 T 的实例:

1
2
3
4
5
<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // IDE 会提示错误
System.out.println(item);
}
}

正常情况下,在 Kotlin 中也不可以:

1
2
3
4
5
fun <T> printIfTypeMatch(item: Any) {
if (item is T) { // IDE 会提示错误
println(item)
}
}

面对这种情况,Java 的解决方案使传入一个显示的 Class 类型对象:

1
2
3
4
5
<T> void printIfTypeMatch(Object item, Class<T> type) {
if (type.isInstance(item)) {
System.out.println(item);
}
}

但是 Kotlin 中可以使用关键字 reified 配合 inline 来便捷解决:

1
2
3
4
5
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) {
println(item)
}
}

为啥需要在 inline 中可以参考这两篇文章:

可以阅读下面的例子,以及实际编译的结果体会 reified 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 正常代码
inline fun <reified T> doSomething(value: T) {
println("Doing something with type: ${T::class.java.simpleName}")
}

fun main() {
doSomething<String>("Some String")
}

// 实际编译: 方法被内联到调用的地方后,泛型 T 会被替换成具体的类型
fun main() {
println("Doing something with type: ${String::class.java.simpleName}")
}