Java核心技术1-泛型1

  |  

摘要: 本文是《Java核心技术 10th》中关于泛型的要点总结第一部分,关键概念包括泛型类、泛型方法、类型擦除与原始类型、桥方法。

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


使用泛型机制编写的程序代码要比那些杂乱地使用 Object 变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类尤其有用,例如,ArrayList 就是一个无处不在的集合类。

泛型很像 C++ 中的模板。与 Java 一样,在 C++ 中,模板也是最先被添加到语言中支持强类型集合的。后续发现模板还有其他的用武之地。Java 也一样,泛型在程序中有很多除了集合外的其它用途。

但注意,从表面上看,Java 的泛型类虽然类似于 C++ 的模板类,唯一明显的不同是 Java 没有专用的 template 关键字。但是,在本文中可以看到,这两种机制有着本质的区别。

本文是《Java核心技术1》第10版 Chap8 中关于泛型的要点总结的第一部分,主要的要点如下:

  • 泛型类
  • 泛型方法
  • 类型擦除、原始类型
  • 类型限定
  • 桥方法

泛型类的引入

Java 中新增泛型类之前,泛型程序用继承来实现。例如 ArrayList,只维护一个 Object 引用的数组。

1
2
3
4
5
6
public class ArrayList {
private Object[] elementData;
...
public Object get(int i) {...}
public void add(Object o) {...}
}

有两个问题:

(1) 当获取一个值时必须进行强制类型转换,例如 String filename = (String) file.get(0)
(2) add 方法没有错误检查,可以向数组列表中添加任何类的对象。例如 files.add(new File("...")),对于这个调用,编译和运行都不会出错。然而在其他地方,如果将get的结果强制类型转换为String类型,就会产生一个错误。

泛型提供了一个更好的解决方案:类型参数(type parameters)

1
ArrayList<String> files = new ArrayList<String>();

此后当调用 get 的时候,不需要进行强制类型转换,编译器就知道返回值类型为 String,而不是 Object:

1
String filename = files.get(0);

编译器还知道 ArrayList<String> 中add方法有一个类型为String的参数。这将比使用 Object 类型的参数安全一些。

一般来讲,那些原本涉及许多来自通用类型(如 Object 或 Comparable 接口)的强制类型转换的代码一定会因使用类型参数而受益。


泛型类

以 Pair 为例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Pair<T> {
private T first;
private T second;

public Pair() {
first = null;
second = null;
}

public Pair(T first, T second) {
this.first = first;
this.second = second;
}

public T getFirst() {return first;}
public T getSecond() {return second;}

public void setFirst(T newValue) {first=newValue;}
public void setSecond(T newValue) {second=newValue;}
}

泛型类可以有多个类型变量:

1
2
3
public class Pair<T, U> {
...
}

类型参数可以用于指定方法的返回类型,域和局部变量的类型。

在 Java 库中,一般使用变量 E 表示集合的元素类型,K 和 V 分别表示表的关键字与值的类型。T(需要时还可以用临近的字母 U 和 S)表示“任意类型”。

从设计模式的角度,泛型类可以看做普通类的工厂。

下面是 Pair 的测试代码,用静态的 minmax 方法遍历数组并返回最小值和最大值。

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
package pair1;

public class PairTest1 {
public static void main(String[] args) {
String[] words = {"Mary", "had", "a", "little", "lamb"};
Pair<String> mm = ArrayAlg.minmax(words);
System.out.println("min = " + mm.getFirst());
System.out.println("max = " + mm.getSecond());
}
}

class ArrayAlg {
public static Pair<String> minmax(String[] a) {
if(a == null || a.length == 0) {
return null;
}
String min = a[0];
String max = a[0];
for(int i = 1; i < a.length; i++) {
if(min.compareTo(a[i]) > 0) {
min = a[i];
}
if(max.compareTo(a[i]) < 0) {
max = a[i];
}
}
return new Pair<>(min, max);
}
}

泛型方法

在普通类中,也可以定义泛型方法。

1
2
3
4
5
class ArrayAlg {
public static <T> T getMiddle (T... a) {
return a[a.length / 2];
}
}

类型变量 (<T>) 放在修饰符 (public static) 后面,返回类型 (T) 的前面。

当调用一个泛型方法时,方法名前的 <> 放入具体的类型:

1
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");

对类型变量加以约束的场景

有时,类或方法需要对类型变量加以约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ArrayAlg {
public static <T> T min(T[] a) {
if(a == null || a.length == 0) {
return null;
}
T smallest = a[0];
for(int i = 1; i < a.length; i++) {
if(smallest.compareTo(a[i]) > 0) {
smallest = a[i];
}
}
return smallest;
}
}

min 是一个泛型方法,注意 smallest 的类型是 T,也就是它可以使任何一个类的对象,但注意:如何确保 T 所属的类有 compareTo 方法

解决方案是将 T 限制为实现了 Comparable 接口的类,写法如下:

1
public static <T extends Comparable> T main(T[] a)
  • extends 后面的内容既可以是类也可以是接口。

  • 一个类型变量或通配符可以有多个限定,例如:

1
T extends Comparable & Serializable

Java 的继承中,可以有多个接口超类型。限定中也是至多有一个类,如果用一个类作为限定,它必须是限定列表的第一个。

例子: <T extends Comparable>

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
java.time.LocalDate;

public class PairTest2 {
public static void main(String[] args) {
LocalDate[] birthdays = {
LocalDate.of(1906, 12, 9),
LocalDate.of(1815, 12, 10),
LocalDate.of(1903, 12, 3),
LocalDate.of(1910, 6, 22)
};
Pair<LocalDate> mm = ArrayAlg.minmax(birthdays);
System.out.println("min = " + mm.getFirst());
System.out.println("max = " + mm.getSecond());
}
}

class ArrayAlg {
public static <T extends Comparable> Pair<T> minmax(T[] a) {
if(a == null || a.length == 0) {
return null;
}
T min = a[0];
T max = a[0];
for(int i = 0; i < a.length; i++) {
if(min.compareTo(a[i]) > 0) {
min = a[i];
}
if(max.compareTo(a[i]) < 0) {
max = a[i];
}
}
return new Pair<>(min, max);
}
}

C++ 的区别

C++ 中不能对模板参数的类型进行限制。如果用一个不适当的类型实例化一个模板,将会在模板代码中报告一个(通常是含糊不清的)错误消息。


泛型与虚拟机

虚拟机中没有泛型对象,所有对象都是普通类。

类型擦除

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。

  • 原始类型的名字就是删去类型参数后的泛型类型名。
  • 擦除类型变量,并替换为第一个限定类型(无限定的变量用 Object)。

例子1: 泛型类型 Pair<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Pair<T> {
private T first;
private T second;

public Pair() {
first = null;
second = null;
}

public Pair(T first, T second) {
this.first = first;
this.second = second;
}

public T getFirst() {return first;}
public T getSecond() {return second;}

public void setFirst(T newValue) {first=newValue;}
public void setSecond(T newValue) {second=newValue;}
}

Pair<T> 的原始类型为去除类型参数后的 Pair;

T 没有限定,所以用 Object 替换,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Pair {
private Object first;
private Object second;

public Pair() {
first = null;
second = null;
}

public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}

public Object getFirst() {return first;}
public Object getSecond() {return second;}

public void setFirst(Object newValue) {first=newValue;}
public void setSecond(Object newValue) {second=newValue;}
}

Java 程序中可以包含不同类型的 Pair,例如 Pair<String>Pair<LocalDate>擦除类型后就变成原始的 Pair 类型了。

例子2: Interval<T extends Comparable & Serializable>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
...
public Interval(T first, T second) {
if(first.compareTo(second) <= 0) {
lower = first;
upper = second;
} else {
lower = second;
upper = first;
}
}
}

Interval<T extends Comparable & Serializable> 的原始类型为去除类型参数后的 Interval;

T 有限定,用第一个限定 Comparable 替换 T,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
...
public Interval(Comparable first, Comparable second) {
if(first.compareTo(second) <= 0) {
lower = first;
upper = second;
} else {
lower = second;
upper = first;
}
}
}

C++ 的区别

从类型擦除这个特性来看,Java 泛型与 C++ 模板有很大的区别。C++ 中每个模板的实例化产生不同的类型,这一现象称为“模板代码膨胀”。Java 不存在这个问题的困扰。

翻译泛型表达式

当程序调用泛型方法时,返回的类型是经过擦除后的,然后编译器会插入强制类型转换,以下面的调用为例:

1
2
Pair<Employee> buddies;
Employee buddy = buddies.getFirst();

擦除 getFirst 返回类型,将返回 Object 类型。编译器会自动插入 Employee 的强制类型转换。也就是编译器把这个方法调用范围为两条虚拟机指令

  • 对原始方法 Pair.getFirst 的调用。
  • 将返回的 Object 类型强制转换为 Employee 类型。

当存取一个泛型域时,也要插入强制类型转换。例如:

1
Employee buddy = buddies.first;

假设 first 域是公有的,以上代码会在结果字节码中插入强制类型转换。

翻译泛型方法

类型擦除也会出现在泛型方法中,例如下这个泛型方法:

1
public static <T extends Comparable> T min(T[] a)

该泛型方法可以理解为一个方法族,擦除类型之后,就只剩下了一个方法:

1
public static Comparable min(Comparable[] a)

方法的类型擦除与多态的冲突

方法的类型擦除会遇到与多台冲突的复杂问题,看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if(second.compareTo(getFirst()) >= 0) {
super.setSecond(second);
}
}

DateInterval() {
super();
}
DateInterval(LocalDate first, LocalDate second) {
super(first, second);
}
}

日期区间是一对 LocalDate 对象,其中覆盖了 setSecond 这个方法,因为需要保证第二个值不小于第一个值,而原始的 Pair 买这个要求。

上面这个 DateInterval 类,在类型擦除后变为以下这个类:

1
2
3
4
5
class DateInterval extends Pair {
public void setSecond(LocalDate second) {
...
}
}

但注意:还存在另一个从 Pair 继承的 setSecond 方法,(这与多态有关),如下:

1
public void setSecond(Object second)

下面的一段代码中,我们希望 setSecond 方法的调用具有多态性,并调用最合适的那个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DateIntervalTest {
public static void main(String[] args) {
LocalDate[] dates = {
LocalDate.of(1906, 12, 9),
LocalDate.of(1903, 12, 3),
LocalDate.of(1910, 6, 22)
};

DateInterval interval = new DateInterval(dates[1], dates[0]);
Pair<LocalDate> pair = interval; // 赋值到超类
pair.setSecond(dates[2]);
}
}

执行 pair.setSecond() 时,由于 pair 引用 DateInterval 对象,所以应该调用 DateInterval.setSecond。问题在于类型擦除与多态发生了冲突。

桥方法

对于上面提到的类型擦除与多态发生了冲突问题。编译器会在 DateInterval 类中生成一个桥方法

1
2
3
public void setSecond(Object second) {
setSecond((LocalDate) second);
}

桥方法的工作过程可以通过跟踪 pair.setSecond() 来了解。

通过代理对象跟踪方法调用的原理和代码可以参考这篇文章:Java核心技术1-代理对象

由于这里要追踪的 setSecond 的类并没有接口,因此需要额外手写一个接口,下面是追踪 setSecond 的的完整代码:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Method;
import java.time.LocalDate;

public class DateIntervalTest {
public static void main(String[] args) {
LocalDate[] dates = {
LocalDate.of(1903, 12, 3),
LocalDate.of(1906, 12, 9),
LocalDate.of(1910, 6, 22)
};

DateInterval interval = new DateInterval(dates[0], dates[1]);
Pair<LocalDate> pair = interval; // 赋值到超类

// 为了追踪 setSecond 的代理对象
PairProxy proxy_pair = (PairProxy)Proxy.newProxyInstance(pair.getClass().getClassLoader()
,pair.getClass().getInterfaces()
,new TraceHandler(pair)
);
proxy_pair.setSecond(dates[2]);
}
}

// 为了追踪 setSecond 的额外的接口
interface PairProxy {
void setSecond(LocalDate second);
}

class DateInterval extends Pair<LocalDate> implements PairProxy {
public void setSecond(LocalDate second) {
if(second.compareTo(getFirst()) >= 0) {
super.setSecond(second);
}
}

DateInterval() {
super();
}
DateInterval(LocalDate first, LocalDate second) {
super(first, second);
}
}

class TraceHandler implements InvocationHandler {
private Object target;

public TraceHandler(Object t) {
target = t;
}

public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
// 打印隐式参数
System.out.print(target);
// 打印方法名
System.out.print("." + m.getName() + "(");
// 打印显式参数
if(args != null) {
for (int i = 0; i < args.length; i++) {
System.out.print(args[i]);
if(i < args.length - 1) {
System.out.print(", ");
}
}
}
System.out.println(")");

return m.invoke(target, args);
}
}

上面代码的 18 ~ 20 行中的 pair 改成 interval 后,运行结果不变,均为:

1
DateInterval@54bedef2.setSecond(1910-06-22)

变量 pair 已经声明为类型 Pair,并且这个类型只有一个简单的方法叫 setSecond,即 setSecond(Object)。虚拟机用 pair 引用的对象调用这个方法。这个对象是 DateInterval 类型的,因而将会调用 DateInterval.setSecond(Object) 方法。这个方法是合成的桥方法。它调用 DateInterval.setSecond(LocalDate),这正是我们所期望的操作效果。

桥方法的返回类型问题

假设 DateInterval 也覆盖了 getSecond 方法:

1
2
3
4
5
class DateInterval extends Pair<LocalDate> {
public LocalDate getSecond() {
return (LocalDate) super.getSecond().clone();
}
}

此时 DateInterval 类中有两个 getSecond 方法:

1
2
LocalDate getSecond() // 在 DateInterval 中定义的
Object getSecond() // 桥方法,覆盖在 Pair (Pair<T> 经过类型擦除) 中定义的

其中第二个是桥方法。

注意,自己写代码是不能想上面两行这样写的,因为有相同参数类型和相同方法名的方法是不合法的。但在虚拟机中,用参数类型和返回类型确定一个方法,因此编译器产生两个金返回类型不同的方法字节码,虚拟机可以处理。

桥方法不仅用于泛型类型。在一个方法覆盖另一个方法时可以指定一个更严格的返回类型 (参考 Java核心技术1-继承)。例如:

1
2
3
4
5
public class Employee implements Cloneable {
public Employee clone() throws CloneNotSupportedException {
...
}
}

Object.clone 和 Employee.clone 方法称为具有协变的返回类型。此时 Employee 有两个 Clone 方法:

1
2
Employee clone() // 前面定义的
Object clone() // 桥方法,覆盖 Object.clone

合成的桥方法调用了新定义的方法。

Java 泛型转换的一些总结

  • 虚拟机中没有泛型,只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成来保持多态。
  • 为保持类型安全性,必要时插入强制类型转换。

Share