写在最前:本笔记全程参考《Java核心技术卷I》,添加了一些个人的思考和整理
目录
泛型与继承
1. 泛型与继承和多态
Employee
是Manager
的父类,但是Pair<Manager>
与Pair<Employee>
没有(也不允许有)继承关系
原因:
假设现在允许Pair<Manager>
转换为Pair<Employee>
,那么下面的代码就会成立:
Pair<Manager> managerPair = new Pair<>(ceo, cfo); // CEO和CFO组为一组
Pair<Employee> employeePair = managerPair; // 现在假设这句代码成立
employeePair.setFirst(simpleEmployee); // 把一个普通员工对象与CFO组为一组
看得出,上面这个代码最后推理出来的逻辑不太合理,employeePair
和managerPair
共享同一个内存区域,CFO最后居然和一个普通员工组在一起了。这对于Pair<Manager>
来说是不可能的,Pair<Manager>
怎么会混进去一个Employee
?
回顾数组与多态
在java中,可以将一个Manager[]
赋值给Employee[]
,它们也是会共享同一片内存区域的。当你往Employee[]
中添加Employee
对象时,就等同于往Manager[]
添加一个Employee
元素。但是此时编译器不会报错,只是运行时会出现ArrayStoreException
,告诉你要加入的数组元素Employee
与数组类型Manager
并不一致。
解决这个问题,可以借助泛型的通配符类型
2. 泛型与原始类型
正如可以将Pair<Employee>
对象赋值给Pair
变量,可以将一个带泛型的类型转换成原始类型
这对于遗留代码的兼容非常重要。但是同时也存在一些问题,那就是它会丧失类型检查功能:
Pair<Manager> managerPair = new Pair<>();
Pair rawPair = managerPair;
// 往rawPair中存入一个File对象
// 注意此时rawPair与managerPair共享同一片内存空间
// 也就是说会往Pair<Manager>中存入File对象!
rawPair.setFirst(new File("..."));
泛型类的扩展和实现
正如ArrayList<Integer>
可以转换为List<Integer>
,一个泛型类可以扩展或实现其他类或接口。
泛型通配符
严格的泛型类型体统使用起来不那么令人愉快,java设计者提供了依琼巧妙(但很安全的)解决方案:通配符类型
1. 通配符上界-子类型限定
在通配符类型中,允许类型参数发生变化。
例如:Pair<? extends Employee>
表示类型参数是Employee
的任一个子类。它限定了泛型类型的上界为Employee
泛型及通配符类型的继承关系
假设我们要编写一下程序:
public static void printPair(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + ", " + second.getName());
}
因为Pair<Manager>
和Pair<Employee>
没有任何继承关系,所以不能将Pair<Manager>
类型的参数传入上述方法。
但是,可以将方法声明为如下:
public static void printPair(Pair<? extends Employee> p) { ... }
该方法参数允许传入任意一个类型参数为Employee
或其子类的Pair
泛型对象。
类型Pair<Manager>
是(Pair<? extends Employee>
的子类型,因此可以传入新声明的方法。
安全性
通配符解决了前面提到的泛型与继承和多态的问题:
Pair<Manager> managerPair = new Pair<>(ceo, cfo); // CEO和CFO组为一组
Pair<? extends Employee> wildcardPair = managerPair; // 没有问题
// 报错,不能把一个普通员工对象与CFO组为一组,安全性得到了保证
wildcardPair.setFirst(simpleEmployee);
由于Pair<? extends Employee>
是Pair<Manager>
的父类,所以上方第二行代码可行。
而第三行代码出错的原因是什么呢?我们看看Pair<? extends Employee>
中的方法:
// 实际上代码并不是这样,类型声明为T而不是? extends Employee,这里只是表达意思
// 因此,将“? extends Employee”看成一个数据类型即可:
? extends Employee getFrist() {}
void setFitst(? estends Employee t) {}
set
方法要求传入一个? estends Employee
类型。编译器只知道需要Employee
类型或某个子类型,但是并不清楚具体是哪个类型。它拒绝传入任何特定的类型,比较?
不能匹配。示例代码如下:
Pair<Manager> managerPair = new Pair<>();
Pair<? extends Employee> wildPair1 = managerPair;
Pair<Employee> employeePair = new Pair<>();
Pair<? extends Employee> wildPair2 = employeePair; // Pair<Employee>类型可以赋值
// 报错,要求传入? extends Employee类型,现在是Employee类型
wildPair1.setFirst(new Employee());
// 报错,要求传入? extends Employee类型,现在是Manager类型
wildPair1.setFirst(new Manager());
// 同上,Object也是一个特定类型
wildPair1.setFirst(new Object());
需要注意的是,上述代码中,使用get
方法将返回一个Employee
类型的实例:
Employee first = wildPair1.getFirst(); // OK
2. 通配符下界-超类型限定
可以为类型参数添加一个超类型限定(supertype bound):T super Manager
。这个限制为Manager的所有超类类型。也就是说,? super Manager
泛指Manager
类型及其所有父类型,它限定了通配符的下界。
与上面提到的继承限定相反的是,带有超类限定的通配符方法中,可以提供方法参数,但是不能提供返回值。例如,Pair<? super Manager>
方法有以下方法:
void setFirst(? super Manager) {}
? super Manager getFirst() {}
编译器无法得知set
方法的具体类型,因此不接受参数类型为Employee
或Object
的方法参数,但是可以接收Manager
或者Manager
的子类型对象
Pair<? super Manager> wildPair = new Pair<>();
wildPair.setFirst(new Object()); // 报错,超过限定范围
wildPair.setFirst(new Employee()); // 报错,超过限定范围
wildPair.setFirst(new Manager()); // 可行
wildPair.setFirst(new Executive()); // 可行
另外,使用get
方法时,因为不能保证返回对象的类型,所以只能将其赋值给最大的类型Object
Object first = wildPair.getFirst();
直观地讲,带有超类型限定的通配符可以写入一个泛型对象,而带子类型限定的通配符允许你读取一个泛型对象
关于通配符的上下界限定,还可以参考:
3. 复杂的泛型限定
Comparable接口本就是泛型,所以对于之前提到的比较变量的方法min
,我们可以这样声明:
public static <T extends Comparable<T>> T min(T... a) {}
假如现在要对String类型的变量进行比较,那么T的类型就是String,它是Comparable<String>
的子类,合情合理
但是!处理一个LocalDate
数组时,LocalDate
实现了ChronoLocalDate
,而ChronoLocalDate
继承于Comparable<ChronoLocalDate>
,因此,LocalDate
实现的应该是Comparable<ChronoLocalDate>
而不是Comparable<LocalDate>
。所以,为了兼容这种情况,我们应该这么声明方法:
public static <T extends Comparable<? super T>> T min(T... a) {}
这个限定显得些许复杂……?但是它更加严格,程序也会更加健壮
子类型限定的另一个常见作用是:作为一个函数式接口的参数类型。
例如:Comparable
接口有一个方法:
default boolean removeIf(Predocate<? super E> filter)
这个方法会删除所有满足给定条件的元素。
例如,如果你不喜欢有奇数散列码的员工对象,可以使用下面的代码将其剔除:
ArrayList<Employee> staff = ...;
Predicate<Object> oddHashCode = obj -> obj.hashCode() % 2 != 0;
staff.removeIf(oddHashCode);
你希望能传入一个Predicate<Object>
而不仅是Predicate<Employee>
,super通配符就可以满足这个需求。
4. 无限定的通配符
还可以使用没有任何限定的通配符,如Pair<?>
类型Pair<?>
有一下方法:
? getFirst() {}
void setFirst(? t) {}
getFirst
方法的返回值只能赋值给一个Object
,而setFirst
方法则无法调用,(不能使用Object
调用)
Pair<?>
和原始类型Pair
的最大区别在于:原始类型Pair
可以使用任意Object对象调用其setFirst
方法
这种类型局限性很大,但是在一些简单的操作中能够奏效,比如一些时候我们并不需要了解泛型的实际类型:
public static boolean hasNulls(Pair<?) {
return p.getFirst() == null || p.getSecond() == null;
}
但是,使用泛型T
可读性更好
5. 通配符捕获
通配符不是类型变量,不能再代码中使用?
作为一种类型:
// 交换Pair中的first和second
public static void swap(Pair<?> p) {
? t = p.getFirst(); //错误
p.setFirst(p.getSecond());
p.setSecond(t);
}
解决方案:提供一个辅助的方法swapHelper
public static <T> void swapHelper(Pair<T> p) {
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
public static void swap(Pair<?> p) {
return swapHelper(p);
}
在上述的方法中,swapHelper(Pair<T>)
方法捕获了通配符类型,它可以清楚地了解到方法中的T
是哪个类型
运用通配符捕获时,必须保证通配符只表示单个确定的类型。例如,ArrayList<Pair<T>>
中的T
永远不可能捕获ArrayList<Pair<?>>
中的通配符,因为ArrayList
对象中可以保存不同类型的Pair<?>