《Java核心技术卷I》泛型篇笔记(四) 泛型通配符

写在最前:本笔记全程参考《Java核心技术卷I》,添加了一些个人的思考和整理

泛型与继承

1. 泛型与继承和多态

EmployeeManager的父类,但是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组为一组

看得出,上面这个代码最后推理出来的逻辑不太合理,employeePairmanagerPair共享同一个内存区域,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方法的具体类型,因此不接受参数类型为EmployeeObject的方法参数,但是可以接收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();

超类型通限定的配符的继承关系

直观地讲,带有超类型限定的通配符可以写入一个泛型对象,而带子类型限定的通配符允许你读取一个泛型对象

关于通配符的上下界限定,还可以参考:

JAVA泛型知识<? extends T><? super T>

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<?>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值