JavaCoreOne-C08Generic
泛型类和泛型方法具有 类型参数,泛型程序设计(generic programming)意味着编写的代码可以对多种不同类型的对象重用. 例如, 在Java引入泛型之前, ArrayList 接受 Object 参数并创建数组, 由于继承的特性, 使得一个 ArrayList 对象可以存放任意类型的对象. 其问题在于, 一是取出元素时需要进行强制转换, 二是无法保证容器内实际的对象类型一致, 这可能导致异常. 因此可以使用泛型指明类型参数(type parameter)
ArrayList<String> strArr = new ArrayList<>();
这使得代码具有更好的可读性, 且解决了上述两个问题. 以上, 实际上你已经了解的泛型的概念和使用语法, 但若要创建自己的泛型类, 则需要更多的知识. 本质主要讨论创建自己泛型类的方法和注意事项 – 这同时也为下一章的泛型集合做好了基础
1. 泛型类与泛型方法
定义一个Pair泛型类, 他具有 first 和 second 字段, 两个字段是不同的类型 T. 有两个构造方法 无参和全参, 以及各字段的getter setter.
// Pair.java
package code.gernic;
public class Pair<T>{
private T first;
private T second;
Pair(){
first = null;
second = null;
}
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;}
}
现在做一个使用测试, 从这一角度看, 泛型类就像是普通类的工厂, 可以指定任何具体类型生成不同的 Pair<> 类型:
public static void test1(){
Pair<String> pair1 = new Pair<>("小米", "冰箱");
System.out.println(pair1.getFirst() + ", " + pair1.getSecond());
// 小米, 冰箱
}
再继续考虑泛型方法, 它在具有类似如下的定义和调用方式:
public class AAA{
public static <T> void f(T... a){
// ...
}
}
// other program
AAA.<String>f("a", "b", "c");
// or we can remove the type explannation
AAA.f("a", "b", "c");
注意此处, 通常来说, 调用时其类型指定可以省略, 编译器可以从传入参数推断出具体类型, 例如 String. 然而也可能出现无法推断的情况, 例如传入的各参数类型不同, 且无法找到合理的公共父类.
假设你要设计一个数组算法工具类, 该类包含一个程序 minmax([]), 它接受一个T类型数组, 找出数组中的最小值first和最大值second, 封装为Pair返回. minmax 方法是一个泛型方法, 它的返回值和参数包含类型参数, 其次这样一种类型还必须满足一定的限制条件, 例如: 可比较性. 元素之间是有大小之分的.
public class ArrayAlg {
public static <T extends Comparable> Pair<T> minmax(T[] a){
if(a==null || a.length==0) return null;
T first=a[0];
T second=a[0];
for (int i=1; i<a.length; i++) {
if(first.compareTo(a[i])>0) first = a[i];
if(second.compareTo(a[i])<0) second = a[i];
}
return new Pair<T>(first, second);
}
}
接下来进行测试
String[] strings = {"Jenny", "Kitty", "Julier", "Mike", "John"};
Pair<String> pair1 = ArrayAlg.minmax(strings); // 省略了类型指定
System.out.println(pair1.getFirst() + ", " + pair1.getSecond());
// Jenny, Mike
正常编译运行, 不过会有标黄警告: 也即comparable是一个原生接口, 你需要为其指定类型参数, 关于这一点会在后面解释
2. 类型擦除–限制与局限性
2.1 类型擦除
类型擦除的核心在于: JVM层面并不提供泛型的支持, 也即JVM中没有泛型类型对象, 所有对象都是普通类。因此, 任何时候定义一个泛型类型时, 都会有对应的原始类型(raw type), 也即没有类型参数的类名, 例如Pair. 而参数类型会被擦除, 替换为限定类型或者Object(无限定).例如对于 Pair<T>
这一实现, 其原始类型变为:
public class Pair{
private Object first;
private Object second;
Pair(){
first = null;
second = null;
}
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;}
}
又比如另一个泛型类 Interval<T extends Comparable & Serializable>
. 则其原始类中的参数T就会被替换为 Comparable
请考虑原始Pair类中的getFirst方法, 该方法在擦除后返回的是Object值, 因此实际执行String a = strPair.getFirst()
语句时, 应至少包含两步:
1. 对原始类型方法Pair.getFirst的调用
2. 将返回值Object对象强转为String对象
考虑这样的情况: 类Interval
继承了Pair<Integer>
, 执行语句其含义是表示一个区间, 因此second
要保证比first
大.
考虑Pair<Integer> a = new Interval()
, 以setSecond()
为例, Interval
类对其进行了重写:
void setSecond(Integer second)
在进行类型擦除后, 从父类继承了方法:
class Interval extends Pair{
public void setSecond(Object second){ ... }
}
此时, 就有了多态的性质, 我们期望调用a.setSecond()
方法时是子类重写后的方法, 编译器会生成一个桥方法public void setSecond(Object second){ setSecond((Integer) second); }
以满足期望。具体过程是:
变量a被声明为Pair<Integer>
, 该类包含一个setSecond(Object)
方法, 虚拟机在引用a的对象上调用该方法, 而该对象是Interval
类型, 进而调用Interval.setSecond(Object)
方法, 该方法是桥方法, 它会继续调用setSecond(Integer)
方法。
然而如果再考虑getSecond()
方法呢, 它们仅仅是返回值类型不同, 实际上, 在虚拟机中, 会有参数类型和返回类型共同指定
小结
- JVM中没有泛型类和泛型方法,只有普通类和普通方法
- 泛型类或者泛型方法的参数类型会被擦除,使用限定类型或者Object替代
- 必要时通过类型强制转换保证类型一致
- 编译器会生成桥方法以保持多态
2.2 限制与局限性
- 不能使用基本类型而是只能使用引用类型作为类型参数, 例如
Pair<Integer>
是合法的而Pair<int>非法。 - 运行时类型查询只适用于原始类型。也即你无法去判断一个类是否为某一个泛型,例如你可以去判断一个类是否为Pair类型而无法去判断一个类型是否为Pair<String>或者Pair<Integer>.这是由于类型擦除。
- 不能在静态字段或方法中引用类型变量
2.2.1 不能创建参数化类型的数组
例如 var table=new Pair<String> [10]
是非法的。数组必须保证各个元素类型的一致性, 不允许插入不同类型的元素, 而泛型类型擦除后失去了类型参数, 只知道table是一个Pair数组, 由于类型参数的可变性, 这违背数组的数据类型一致性, 因此不允许这么做, 也即这从语法上就是错误的. 然而这样的声明或者创建是合乎语法的, 尽管它并不安全: var table = (Pair<String>) new Pair<?> [10];
声明调配类型数组, 然后进行强制转换, 但他无法保证数组元素一致性. 结论是, 可以简单地使用ArrayList等集合容器存放参数化类型对象.
2.2.2 不能实例化类型变量
例如在构造器中这样做是错误的:
public Pair(){first=new T(); second=new T();}
这是由于T会被擦除为 Object, 一个最好的解决方法是 提供构造器表达式
public static <T> Pair<T> makePair(Supplier<T> constr){
// 传入参数是一个构造器表达式, 例如 String::new
return new Pair<>(constr.get(), constr.get());
}
or 使用传统的方法反射 Constructor.newInstance
方法:
public static <T> Pair<T> makePairReflection(Class<T> cl){
// 传入参数是一个类型, 例如String.class, 通过反射机制创造该类型的对象
try{
return new Pair<>(cl.getConstructor().newInstance(), cl.getConstructor().newInstance());
}catch (Exception e){
return null;
}
}
从另一个角度想, 对于使用了类型参数的函数, 要创建该类型的对象, 就必须先明确这个类型具体是什么类型, 而构造器表达式这一形式就十分简洁明确.
2.2.3 不能构造泛型数组
不同于第一小节, 第一小节是说不能创建参数化类型的数组例如Pair<String>[10]
, 这里是说在泛型方法设计的过程中不能创建泛型数组, 例如minmax(T[])
中假如不借助于Pair类存储min max, 而是使用一个数组返回, 例如new T[2]; // Error
, 同样由于类型擦除这里非法, 可以通过传入构造器类型解决
// minmax(IntFunction<T[]> constr, T... a)
2.2.4 不能抛出或者捕获泛型类的实例
既不能抛出泛型类型异常也不能捕获泛型类型异常, 泛型类型也不被允许扩展Throwable接口
2.2.5 可以取消检查型异常
Java异常处理机制的原则是必须为所有的检查型异常提供一个处理器
3. 泛型类型的继承规则
如果Manager
是Employee
的子类, 那么Pair<Manager>
是Pair<Employee>
的子类吗?直觉上讲似乎不太是, 实际上确实后面两者并没有什么关系。
不过另一方面, 泛型类可以扩展或实现其他的泛型类, 例如一个 ArrayList<Manager>
可以转为 List<Manager>
.
4. 通配符?
例如: Pair<? extends Employee>
表示任何参数类型为Employee的子类型及本身构成的Pair
通配符形式上有点类似于泛型编程中的类型限定, 但在前者的领域, 他是为了保证类型参数满足一定的限制条件以确保数据安全, 而在前者通配符的观念里, 它是为了解除特定类型的限制, 将可用类型拓宽.
在引入通配符之前, 假设一个函数printBuddies
接收Pair<Employee>
类型, 它打印了对象的字段内容信息, 但由于Pair<Manager>
与Pair<Employee>
并无关系, 所以printBuddies
并不接受Pair<Manager>
参数, 然而, 在使用通配符之后, 函数printBuddies(Pair<? extends Employee>)
就可以接收Pair<Manager>
类型的参数了. 这听起来似乎十分完美, 且合乎情理, 然而实现时有一个细节是:
? extends Employee
并非一个具体类型, 不可能创造后者的一个引用, 例如? extends Employee emp = xxxx;
, 因此我们只能使用Employee这个引用, 该引用可以指向子类型及本身的对象, 形象地说, 可以用父类的指针去接收(读取)子类型的对象, 进而打印它
super超类型限定
例如: Pair<? super Manager>
表示任何参数类型为Manager的超类型及本身的Pair
。
类比extends
通配符, 假设一个函数接受Pair<? super Manager> pairA
为参数, 那么实际上函数仅能操作 Manager
引用, 并且知道 pairA
的字段应该是 Manager
的父类型, 函数可以在进行一些处理后得到需求的 Manager
对象, 并把它设置到 pairA
的字段, 这同样是由于父类的指针可以去接收(读取)子类型的对象, 也即一个原是一种形状.