[ING] Generic Programming

Posted by 阿呆 on 2019-01-01

前言

什么是泛型?
泛型机制是在 Java5.0中新增的,那么肯定你会有疑问,5.0之前是如何实现泛型程序设计的,这是我们第二个关心的问题

什么是泛型

泛型程序设计**Generic programming**
编写的代码可以被很多不同类型的对象所重用

Before Generic

Sample

1
2
3
4
5
6
7
8
9
10
11
public class ArrayList{	// before geberic classes
private Object[] elementData;
...
public Object get(int i){
reurn elementData[i];
}

public void add(Object o){
elementData[index++]=o;
}
}

两个问题

1.获取的时候需要进行强制转换
2.添加的时候没有类型检查,可能出现多种类型在一个ArrayList里面,编译和运行的时候都没问题,但是当获取后强制转换就可能出错

泛型

类型参数 | type parameters

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

省略的类型参数可以从变量的类型推断出

编译器可以很好地利用这个信息
存入:进行类型检查(编译器将知道add方法有一个String参数)
取出:不需要再类型转换

泛型类的设计

泛型类的使用可能很简单,大多数 Java 程序员都使用 ArrayList这样的类型,就好像它们已经构建在语言内部

但是,实现一个泛型类并不容易;例如,程序员可能想要ArrayList中的所有元素添加到ArrayList中去,然而,反过来就不行。How?

通配符 | wildcard type

它解决了上述的问题

泛型程序设计(Generic programming)分为三个能力级别

  • 仅仅使用泛型类
  • 学习Java泛型类来解决程序中出现的一些错误信息
  • 实现自己的泛型类与泛型方法

应用程序员(Application programmer)很可能不喜欢编写太多的泛型代码,JDK开发人员已经做出最大的努力,为所有的集合类提供了类型参数

定义简单泛型类

一个泛型类(generic-class)就是具有一个或多个类型变量的类,本章以Pair类作为例子

Pair.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 first){ this.first = first;}
public void setSecond(T second){ this.second = second; }
}

一个包含"T"的"<>"并放在类名的后面,类型参数也可以为多个,Pair<T,U>
类定义中的类型变量指定方法的返回类型以及域和局部变量的类型

规范: 在Java中,通常用E表示集合的元素类型,K和V分别表示表的关键字与值的类型
T表示"任意类型"(需要时还可以用临近的U和S)

用具体的类型替换类型变量就可以实例化泛型类型,换句话说,可以把泛型类看成普通类的工厂

1
Pair<String>

要搞清楚什么时候是调用,什么时候是定义,二者别搞混了,Test类是调用,如果一个类中出现了泛型方法(非静态方法的话),那么这个类就是一个泛型类,必须在类名上加类型参数。

泛型方法

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

由上面可以看出,定义单纯的泛型方法,而不定义泛型类的话,只需要把它定义为静态方法即可
当然,泛型方法也可以定义在泛型类中

规范
放在方法名后面,返回类型前面(不要奇怪为什么两个T挨在一块,修饰的泛型类,返回值却不一定是 T)

泛型方法的调用:

1
2
3
4
String middle = ArrayAlg.<String>getMiddle("john","Q.","Public");

编译器会根据 names的类型来推断出T的类型
所以 String middle = Array.getMiddle("john","Q.","Public");

大部分情况都不会出现问题,但是有时候会出现问题

1
double middle = ArrayAlg.getMiddle(3.14,1729,0);

这里可以通过将它们都写成 double 类型来解决问题,需要更深入的分析的请阅读 Core Java P313
也可以了解为什么类型参数要放在方法名的前面,这里暂且不表

类型变量的限定

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

Sample

1
2
3
4
5
6
class ArrayAlg
{
public static <T> T min(T[] a){
...
}
}

这里有一个问题,只有实现了Comparable接口的类的对象,才可以调用compareTo方法
解决这个问题的方法是将T限制为实现了Comparable接口的类型(注意T也不一定是类,可能也是一个接口)

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

原则:

1.一个类型变量或者通配符可以有多个限定

1
T extends COmparable & Serializable

2.可以有多个接口超类型,但是类至多有一个,并且要放在限定列表中的第一个

限定符Smaple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 = 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<T>(min,max);
}

泛型代码与虚拟机

虚拟机没有泛型对象-所有对象都属于普通类

那么我是不是可以理解为:泛型是编译器层面的行为

类型擦除

无论何时定义一个泛型类型,都自动提供一个相应的原始类型(raw type);原始类型的名字就是删去类型参数的泛型类型名。
擦除类型变量,并替换为限定类型(无限定的就用Object)

1.无限定的变量

Pair的原始类型

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Pair
{
private Object first;
private Object second;

public Pair(Object first,Object second)
{
this.first = first;
this.second = second;
}
...
}
// like before generic

这点而言,Java泛型与C++模板就有很大的区别,C++中每个模板的实例化产生不同的类型

2.有限定的类型变量,其原始类型呢,用第一个限定的类型来替换
Sample

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
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;
}
}
}

其对应的原始类型如下:
public class Interval implements Serializable
{
private Comparable lower;
private Comparable upper;
....
public Interval(Comparable first,Comparable second)
{
...
}
}

翻译泛型表达式

当调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换,例如

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

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

1.对原始方法 Pair.getFirst 的调用

2.将返回的Object类型强制转换为Empolyee类型

翻译泛型方法

类型擦除也会出现在泛型方法中,程序员通常认为下述的泛型方法是一个完整的方法族

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

而在擦除泛型过后,只剩下一个方法:

1
public static Comparable min(Comparable[] a)

泛型擦除带来的一些问题

泛型约束和局限性

继承泛型类型的多态麻烦(子类没有覆盖住父类的方法)

1
2
3
4
5
class SonPair extends Pair<String>{
public void setFirst(String fir){
.....
}
}

很明显,类的设计者的本意是想在 SonPair类中覆盖父类Pair的setFirst(T fir)这个方法。但是事实上,SonPair 中的setFirst(String fir)方法根本没有覆盖住Pair中的这个方法,因为 Pair在编译阶段已经被擦除为 Pair 了,它的setFirst方法变成了 setFirst(Object fir); 那么 SonPair中的 setFirst(String)自然无法覆盖父类的setFirst(Object)方法了,函数原型不一样(参数类型不绝对相等),肯定无法覆盖哈!

那么我们来看一下编译器如何来解决这个问题:

1
2
3
4
// 编译器会自动在 SonPair 中生成一个桥方法(bridge method)
public void setFirst(Object fir){ // 这样就覆盖了擦出了泛型的父类的方法了
setFirst((String) fir)
}

而且桥方法内部其实调用的是子类字节 setFirst(String) 方法,对于堕胎来说就没有问题了。

桥方法解决了多态中的方法覆盖问题,却引入了另一个问题,如果我们还想再SOnPair中覆盖 getFirst()方法呢?

我们继续使用桥方法:

1
2
1. String getFirst()	// 自己定义的方法
2. Object getFirst() // 编译器生成的桥方法

难道,编译器允许方法签名相同的多个方法存在于一个类中么?

事实上,也只有编译器可以这么做!JVM会用参数类型和返回类型来确定一个方法

泛型类型中的方法冲突

1
2
3
4
5
6
public class Pair<T> {
public boolean equals(T value){
return (first.equals(value))
}
}
//【Error】 Name clash: The method equals(T) of type Pair<T> has the same erasure as equals(Object) of type Object but does not override it。

编译器说你的方法与Object中的方法冲突了,这是为什么?

因为一开始,编译器认为equals(T)没有覆盖住父类Object中的equals(Object)方法

接着,泛型擦除(T->Object),突然发现与父类中的equals一样了,基于开始确定没有覆盖这样一个想法,编译器精神分裂了,冲突报错。

不能创建参数化类型的数组

不能实例化参数化类型的数组,如:

1
Pair<String>[] table = new Pair<String>[10]; // Error

这有什么问题呢?擦出之后,table的类型是 Pair[]。

Object[] objArray = table; // 这样赋值没有问题吧?

数组会记住它的元素类型,如果视图存储其它类型的元素,就会抛出一个 ArrayStoreExecption异常

1
Object[0] = "Hello"// Error-component type is Pair

但是对于泛型类型,擦除会使这种机制无效,以下赋值:

1
objArray[0] = new Pair<Empolyee>();

能够通过数组存储检查,不过仍会导致一个类型错误。出于这个原因,不允许创建参数化类型的数组。

需要说明的是,只是不允许创建这些数组,而声明类型为 Pair[] 的变量仍是合法的,不过不能用 new Pair[10]初始化这个变量

解决方案有两个

1.声明通配符类型的额数组,然后进行类型转换,但这是不安全的

Pair[] table = (Pair[] new Pair<?>[10])

2.如果要收集参数化类型对象,只有一种安全且高效的方法:使用 ArrayList<Pair>

REF

Core Java