8.1 自定义泛型类和泛型方法
Java5中,泛型的引入成为Java程序设计语言发行以来最显著的变化。Java引入泛型类之前,泛型程序设计是用继承实现的。泛型类维护一个Object的引用,使用的时候进行强制类型转换。
这种方法带来的问题主要有两个:
- 当获取一个值时必须进行强制类型转换。
- 没有类型检查,可以向泛型类里添加任何类型。只有当运行的时候才会报错。
泛型提供了一个很好的解决方案:类型参数。
泛型类就是有一个或者多个类型变量的类。泛型方法就是有一个或者多个类型变量的方法。
在Java中,使用变量E表示集合的元素类型。使用K和V分别表示表的键和值的类型。使用T、U、S表示任意类型。
泛型类或者方法定义的时候,可以采用关键字extends对泛型进行限定。要求传入泛型类或者方法的类型必须是某个接口或者某个类的子类。当有多个限定类或者接口的时候,使用&连接。
下面是一个简单的泛型程序:
import java.io.Serializable;
import java.util.Objects;
public class UniversalTest {
public static void main(String[] args){
//泛型方法的调用可以直接像常规方法一样,也可以用<T>指明类型 <T>functionName(T args)
System.out.println(minMax("string", "b", "c", "d"));
var r =minMax(1.0, 2.0, 5.12, 10.0);
System.out.println(r);
}
//泛型方法的声明格式是:public static <T> T FunctionName(T args)
//<T>表明这是一个泛型方法 T是返回值 args是泛型参数
//下面这个泛型方法含参数较多。注意分析,因为返回的UniversalPair类中包裹的泛型T必须实现那两个接口,所以你传进来的泛型T也要实现那两个接口。
//所以这里的三个T表达的是同一种类型。
public static <T extends Comparable & Serializable> UniversalPair<T> minMax(T ...a){
if (a == null || a.length == 0) return null;
T min = a[0];
T max = a[0];
for (T aa : a){
if (min.compareTo(aa) > 0) min = aa;
if (max.compareTo(aa) < 0) max = aa;
}
return new UniversalPair<>(min, max);
}
}
//定义一个泛型类时要在后面用<T>指明该类为泛型类
class UniversalPair<T extends Comparable & Serializable> implements Comparable, Serializable{
private T first = null;
private T second = null;
public UniversalPair(T first, T second) {
this.first = first;
this.second = second;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
@Override
public int compareTo(Object o) {
var no = (UniversalPair) Objects.requireNonNull(o);
if (this.getFirst().compareTo(no.getFirst()) > 0) return 1;
else if (this.getFirst().compareTo(no.getFirst()) < 0) return -1;
else if (this.getSecond().compareTo(no.getSecond()) > 0) return 1;
else if (this.getSecond().compareTo(no.getSecond()) < 0) return -1;
else return 0;
}
@Override
public String toString() {
return "UniversalPair{" +
"first=" + first +
", second=" + second +
'}';
}
}
8.2 泛型代码和虚拟机
虚拟机没有泛型类型对象——所有的对象都属于普通类。当传入泛型类时,虚拟机编译时会把没有限定的泛型参数<T>转换成Object对象,这叫做类型擦除。会将有限定的<T extends A & B>转换成A对象。注意虚拟机对于有限定的泛型类型,总是会在编译的时候把泛型转换成第一个限定类型。因此,对于含有标记型接口的限定类型的泛型,要把标记接口放到最后。不然在运行的时候,虚拟机还得将标记接口的类进行强制类型转换成非标记接口的类。
例如,Pair<T>在虚拟机编译后的原始类型如下:
public class Pair{
private Object first;
...
public Pair(Object first, Object second){
this.first = first;
this.second = second;
}
...
}
这个时候如果出现泛型表达式,如:
Pair<Empliyee> buddies= ...;
Employee buddy = buddies.getfirst();
在虚拟机编译时会进行类型擦除,并加入类型转换。
Pair buddyies = ...;
Employee buddy = (Employee) buddies.getfirst();
当获取一个泛型属性的时候,也会进行强制类型转换。
再比如以下泛型方法:
public static <T extends Comparable> T min(T[] a)
在虚拟机编译进行类型擦除后会变成:
public static Comparable min(Comparable[] a)
类型擦除也会带来多态实现上的问题,解决方法是引入一个桥方法。例如以下代码:
class DateInterval extends Pair<LocalDate>{
public void setSecond(LocalDate second)
...
}
这段代码类型擦除后变成:
class DateInterval extends Pair{
public void setSecond(LocalDate second)
...
public void setSecond(Object second)
}
为什么会出现一个setSecond(Object second)方法?
这个方法是一个桥方法,因为泛型导致了子类继承的时候没法确定重写方法的类型。这样在发生多态调用的时候就没法调用子类重写的方法。而引入桥方法之后就完美的解决了这个问题,可以在子类的桥方法中调用重写后的方法。这样看来桥方法才是父类的相同方法的重写方法。
桥方法在方法重写的时候也有应用。例如,子类重写父类方法的时候,方法的返回值类型可以使用父类返回值类型的子类,这个内部就是用桥方法实现的。
总之,Java的泛型转换有以下几点:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都会替换成他们的限定类型
- 使用桥方法来保持多态
- 为了保持类型的安全性,必要时会插入强制类型转换。
8.3 泛型的限制和局限性
Java泛型使用的时候需要慎重的考虑限制和局限性。大多数限制和局限性都是类型擦除引起的,所以认真的考虑类型擦除很重要。
-
不能用基本类型实例化泛型类的泛型参数T。
考虑Pair<int>类型擦除后会变成Pair<Object>。而int是基本类型,其不是Object的子类。但是Pair<Int>是合法的。
-
不能实例化类型变量T
考虑以下代码:
public Pair(){ first = new T(); second = new T(); }
类型擦除之后都变成了new Object(),显然这不是我们想要的结果。
Java9之后的解决方法是让调用者提供一个构造器表达式。
public static <T> Pair<T> makePair(Supplier<T> constr){ return new Pair<>(conster.get(), conster.get()); }
而调用的时候可以传入一个构造器引用:
Pair<String> p = Pair.makePair(String::new)
-
运行时类型查询只能用于原始类型。
- 考虑
a instanceof Pair<T>
或者a instanceof Pair<String>
,其类型擦除之后都变成了Pair,所以类型检查只可用于查看a是否是Pair的子类。 - 另一个T.getClass或者T.Class的使用,这两个在Java类型擦除之后都变成了Object.Class。包括Pair<String>.Class也是没有意义的,类型擦除之后都是Pair.Class。
- 考虑
-
不能创建泛型类的数组。
考虑
var table = new Pair<String>[10]
类型擦除之后会变成var table = new Pair<String>[10]
数组table是Pair类型的,但是其内部存储的T类型的值全部擦除成了Object。这样会导致数组记住他的元素类型是Object,如果再存入其他类型就会抛出ArrayStore-Exception类型。 -
不能创建类型变量T的数组
如同不能实例化类型变量T一样,也不能创建类型变量T的数组。如果要创建可以参考实例化类型变量T一样传入一个数组构造器String[]:new,Java的数组类型也都是Object的实例。
-
Varargs警告
考虑可变参数列表的方法function(T …a),其内部实现是将a变成了数组的形式传给了方法。由于T是泛型参数,如果传递String等非泛型类是没有问题的。但是当传递泛型类例如Pair<String>就会创建泛型类的数组,这几产生了之前提到的问题。不过对于这种情况编译器的规则会比较放松,会返回一个警告而不是错误。
可以采用两种方法抑制这个警告:
- 使用注解@SuppressWarnings(“unchedked”)抑制警告。
- 使用注解@SafeVargrs注解方法,对于任何只需读取参数数组元素的、声明中有static、final或(Java9)private的方法,都可以使用这个注解。
注意补充一点,所有的类名型的调用Pair<T>都会在编译器中擦除为Pair,而所有参数类型的Pair<T>都会在编译器中擦除为Pair<Object>。
判断擦除之后的结果,关键是看这个调用的含义。
-
泛型类的静态上下文中类型变量无效
不能在静态字段或方法中引用类型变量T。因为编译器擦除之后都会是Object,而且静态字段和方法是在类加载的时候就创建的,之后就不可改变了。所以之后的调用相当于全程在使用Object类型。
public class Singleton<T>{ private static T singleInstance; //error public static getSingleInstance(){ //error ... } }
-
不能抛出活捕捉泛型类的实例
-
可以取消对检查型异常的检查
-
注意擦除后的冲突
8.4 泛型类型的继承规则
泛型的继承规则注意两点:
- 如果A是B的父类,Pair<A>和Pair<B>无父子关系,两者是没有区别的类。
- 泛型的原始类型可以继承,例如List<T>是ArrayList<T>的父类。
8.5 通配符––“?”
8.5.1 带限定的通配符
已经有泛型了,类型变量T可以表示所有类型,为什么又要引入通配符“?”呢?
类型参数T不管怎么说,他都是代表一种类型,在方法调用的时候一旦确定了就不能改变了。为了解决这一问题,所以引入了通配符,允许类型参数发生变化。
假设A是B的父类,那么通配符语法Pair<? extends A>
,这个表示?是所有A的子类。它的方法如下:
? extends A getFirst() //返回类型为A
void setFirst(? extends A) //error
这样将不可以调用setFirst,编译器只知道是A的子类型,但是不知道具体是什么类型,所以编译器拒绝传递任何特定的类型。
而getFirst就不一样了,将getFirst的返回值赋给一个A的引用是完全合法的。
同样,对于通配符语法Pair<? super B>
这个表示?是B的所有超类。
? super B getFirst() //返回值为Object
void setFirst(? super B) //传入类型必须为B
这样当调用getFirst的时候,返回值赋给B的超类,但是不知道赋给B的哪一级别的超类,因为可能是多层次继承关系,最终只能赋值给Object类。
而对于setFirst就不一样了,传入的是B的超类,但是不知道具体的类型。所以只能接受参数类型为B的对象。
综上,
- 带有超类型限定的通配符super,允许你写入一个泛型对象。
- 带有子类型限定的通配符extends,允许你读取一个泛型对象。
8.5.2 无限定通配符
还可以使用根本无限定的通配符。
同样的分析方法,分析Pair<?>,它有以下方法:
? getFirst()
void setFirst(?)
getFirst的返回值会赋给一个Object对象,而setFirst则完全不能调用。
8.5.3 通配符捕获
通配符不是类型变量,因此,不能在编写代码中使用“?”作为一种类型。
下面是一种通配符捕获技巧解决的通用交换对组元素的方法。
public static void swap(Pair<?> p){
swapHelper(p);
}
public static <T> void swapHelper(Pair<T> p){
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}