Java 泛型
一、为什么使用泛型
泛型之前ArrayList
的使用:
public class Main {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("Hello");
String s = (String)list.get(0);
list.add(new File("..."));
}
}
当我们希望 list 只存储 String 类型的对象时,原始的 ArrayList 存在两个问题:
- 当获取一个值时,必须进行
强制类型转换
- 编译器不能进行
类型检查
,可以向数组列表中添加任何类的值
而泛型可以解决这两个问题,下面介绍泛型的使用。
二、泛型类和泛型方法
1. 泛型类
定义泛型类,类型参数用尖括号括起来,放在类名之后:
class Pair<T> {...}
创建对象时,可以使用菱形语法省略构造器中的类型参数:
Pair<String> p = new Pair<>();
2. 泛型方法
定义泛型方法,类型参数放在修饰符之后,返回类型之前
:
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名之前:
String middle = Main.<String>getMiddle("John", "Q.", "Public");
不过,在大多数情况下,方法调用可以省略类型参数:
String middle = Main.getMiddle("John", "Q.", "Public");
三、类型参数的限定
Java 库使用参数 E 表示集合的元素类型,K 和 V 分别表示表的键和值的类型,T(或者 U 和 S)表示"任意类型"。
我们可以使用extends
关键字对类型参数进行限定:
<T extends BoundingType>
以上写法,表示 T 应该是限定类型(bounding type)的子类型(subtype),T 和限定类型可以是类或接口。
一个类型参数或通配符可以有多个限定,限定类型用&
分隔,而类型参数用逗号分隔:
<T extends Comparable & Serializable>
需要注意的是,如果有一个类作为限定,它必须是限定列表中的第一个限定
。
四、类型擦除
在虚拟机中没有泛型类型对象,所有对象都属于普通类。
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型
,这个原始类型的名字就是去掉类型参数后的泛型类型名。类型参数会在编译时被擦除
,并替换为其第一个
限定类型(对于无限定的类型参数则替换为Object
)。
1. 擦除泛型类
下面我们定义一个泛型类:
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; }
}
}
该泛型类的原始类型如下所示:
class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
...
public Interval(Comparable first, Comparable second) {...}
}
需要注意,如果将限定换为<T extends Serializable & Comparable>
,会用 Serializable 替换 T,而这可能导致在调用 compareTo 方法时需要进行强制类型转换
。为了提高效率,应该将标签接口
放在限定列表的末尾
。
2. 编译器的工作
① 当调用一个泛型方法时,如果擦除了返回类型,编译器会插入强制类型转换
。
示例代码:
Pair<Employee> pair = new Pair<>(...);
Employee first = pair.getFirst();
对字节码文件进行反编译:
Code:
...
32: invokespecial #22 // Method com/company/Pair."<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
35: astore_3
36: aload_3
37: invokevirtual #25 // Method com/company/Pair.getFirst:()Ljava/lang/Object;
40: checkcast #7 // class com/company/Employee
43: astore 4
45: return
可以看到在调用 getFirst 方法后,进行了checkcast
强制类型转换的检查。
② 类型擦除与多态存在冲突,为了解决这个问题,编译器会生成桥方法
。
示例代码:
class Pair<T> {
private T first;
private T second;
public Pair() {};
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; }
}
class DateInterval extends Pair<LocalDate> {
public DateInterval(LocalDate first, LocalDate second) {
super(first, second);
}
public void setSecond(LocalDate second) {
if(second.compareTo(getFirst()) >= 0) {
super.setSecond(second);
}
}
}
DateInterval 类描述一个日期区间,我们覆盖了 Pair 类的 setSecond 方法来确保第二个值不小于第一个值。
类型擦除后变成:
class Pair {
private Object first;
private Object second;
public Pair() {};
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 first) { this.first = first; }
public void setSecond(Object second) { this.second = second; }
}
class DateInterval extends Pair {
public DateInterval(LocalDate first, LocalDate second) {
super(first, second);
}
public void setSecond(LocalDate second) {
if(second.compareTo((LocalDate)getFirst()) >= 0) {
super.setSecond(second);
}
}
}
这时,我们发现 DateInterval 类中 setSecond 方法的签名为setSecond(LocalDate second)
,而 Pair 类中的签名为setSecond(Object second)
,不构成覆盖方法
。
但是,考虑以下代码:
Pair pair = new DateInterval(...);
pair.setSecond(...);
setSecond 的调用应该具有多态性,pair.setSecond 应该调用 DateInterval 的 setSecond 方法。但是,类型擦除与多态产生了冲突,实际上会调用 Pair 的 setSecond 方法。 为了解决这个问题,编译器在 DateInterval 类中生成了一个桥方法
:
public void setSecond(Object second) { setSecond((LocalDate) second);}
这时,桥方法覆盖
了 Pair 类中的 setSecond 方法,并且会调用 DateInterval 的 setSecond(LocalDate) 方法,保证了 Java 的多态。
我们对 DateInterval 类文件进行反编译,进行验证:
class com.company.DateInterval extends com.company.Pair<java.time.LocalDate> {
com.company.DateInterval();
public void setSecond(java.time.LocalDate);
public void setSecond(java.lang.Object);
}
可以看到,确实多出了一个 setSecond(Object) 方法。
五、泛型的限制(待续)
六、泛型类型的继承规则
继承规则
:无论 S 与 T 有什么关系,通常 Pair<S> 与 Pair<T> 都没有
任何关系。
如上所示,Manager 是 Employee 的子类,但是 ArrayList<Manager> 和 ArrayList<Employee> 没有任何关系。
七、通配符的限定
① 子类限定
子类限定<? extends Type>
,表示将泛型对象的类型参数限制为 Type 类型或其子类型。类型参数为子类限定的泛型对象引用,可以读取,但不能
写入。
示例代码:
Pair<Manager> p1 = new Pair<>(m1, m2);
Pair<? extends Employee> p2 = p1;
对象变量 p2 引用的对象的类型为 Pair<Manager>,但是 p2 本身被声明为 Pair<? extends Employee> 类型。从 p2 角度看,我们只知道 p2 引用的 Pair 类型的对象存储的是 Employee 类或其子类的对象,可以读取这个对象并向上转型为 Employee 类型;但是不能进行写入,因为不知道子类的具体类型。
② 超类限定
超类限定<? super Type>
,表示将泛型对象的类型参数限制为 Type 类型或其超类型。类型参数为超类限定的泛型对象引用,可以写入,但只能
读取为 Object。
示例代码:
Pair<Employee> p1 = new Pair<>(e1, e2);
Pair<? super Manager> p2 = p1;
对象变量 p2 引用的对象的类型为 Pair<Manager>,但是 p2 本身被声明为 Pair<? super Manager> 类型。从 p2 角度看,我们只知道 p2 引用的 Pair 类型的对象存储的是 Manager 类或其超类的对象,可以写入 Manager 类型或其子类的对象,它会自动向上转型为特定的超类类型;但是因为不知道读取到的超类的具体类型,只能将读取结果赋值为 Object。
③ 无限定
无限定<?>
,表示不限定泛型对象的类型参数。类型参数为无限定的泛型对象引用,只能
读取为 Object,不能
写入。
补充:PECS( Producer Extends, Consumer Super )原则
,即读取数据使用子类限定,写入数据使用超类限定。
八、自限定的类型
在 Java 泛型中,有一种经常出现的写法:
class SelfBound<T extends SelfBound<T>> {...}
我们称这种类型为自限定的类型
,selfBound 接收类型参数 T,而 T 限定为 selfBound<T> 的子类。
示例代码:
class A extends SelfBound<A> {...}
class B extends SelfBound<A> {...}
class C extends SelfBound<B> {...} // Error
A 继承 selfBound<A> ,使得 A 可以作为 selfBound 的类型参数;而 B 没有继承 selfBound<B>,所以不能将 B 作为 selfBound 的类型参数。
自限定类型的主要使用方式为class A extends SelfBound<A> {...}
,它的目的是保证 SelfBound 类的类型参数为当前定义的类
。你可能会说,B 继承的 SelfBound 类的类型参数是 A 而不是当前定义的 B 类,但是一般情况下并不会这样使用。
如果看不懂,可以考虑没有自限定类型的情况:
class SelfBound<T> {...}
class A extends SelfBound<Any-Type> {...}
如果 selfBound 没有自限定,A 类在继承 selfBound 时,类型参数可以是任意类型。
如有错误,欢迎指正。.... .- ...- . .- -. .. -.-. . -.. .- -.-- -.-.--