详解Java泛型(四)之通配符类型

1. 概述

固定的泛型类型系统使用起来不是特别的方便,所以java的设计者发明了一种很巧妙且安全的解决方案——通配符类型。

2. 泛型类型的继承规则

在讲通配符类型前,先来点预备知识。

现在我有一个Persno类和一个Student类,Student类是Person类的子类,那么Pair<Student>Pair<Person>的一个子类吗?不是,例如下面的代码是不能通过编译的

Pair<Person> pp  = null;
Pair<Student> ps = null;
pp = ps;

这个原因很明显,比如说我的Pair类是这样子的

public class Pair<T> {

    private T first;
    private T second;
    public void setFirst(T first) {
        this.first = first;
    }
    public void setSecond(T second) {
        this.second = second;
    }
    public T getFirst() {
        return first;
    }
    public T getSecond() {
        return second;
    }
}

在实例化的时候就可以确定T的具体类型是什么,Pair<Student>的话T就是Student,Pair类中存储的两个值的类型就是Student;Pair<Person>的话T就是Person,Pair类中存储的两个值的类型就是Person。这里面的内容不就有很大的不同了嘛,就像是两个人穿上一样的外套,看起来一样,但是他们就是两个不同的人。

所以总结来说,无论S和T有什么联系,通常Pair<S>Pair<T>没有什么联系。

还有一点就是,永远可以将参数化类型转换成一个原始类型。什么意思?比如说Pair<Student>的原始类型是Pair,那么Pair pRaw = new Pair<Student>();是可以的。那么这里就会有潜在陷阱可能会导致出错,如下面代码

Pair<Student> ps = new Pair<Student>();
Pair pRaw = ps;
pRaw.setFirst(new Date());
Student first = (Student) pRaw.getFirst();

Pair<Student>类型的变量转换成原始类型Pair,原始类型操作的是Object类型的,而Date类型可以转换成Object类型是毋庸置疑的,所以pRaw可以存储一个Date类型的值,那么现在问题来了,要是你还以为pRaw存储的都是Student类型,使用getFirst方法获取对象并赋值给Student类型的变量,就会抛出ClassCastException异常。这样子做就失去了Java泛型提供的安全性,本来人家泛型就可以帮你在编译的时候查出错误的,你硬是这么搞也是没办法。所以在转换成原始类型的来使用时候要注意一下这个问题

最后一点,泛型类可以扩展或者实现其他的泛型类。就这一点而言,与普通类没什么区别。比如我们常常这样子写 List<String> list = new ArrayList<String>(); 这样子ArrayList<T>就实现了List<T>接口。

3. 通配符的上界

好了,接下来就是本文的重点——通配符类型

现在我有一个打印Pair<Person>中内容的方法,先看看下面的代码

public static void printPerson(Pair<Person> p){
    Person first = p.getFirst();
    Person second = p.getSecond();
    System.out.println(first);
    System.out.println(second);
}

当然这段代码是没有问题的,问题就在于,这个方法不够通用,如果我想用这个方法来打印Pair<Student>的内容是不能行的,上一节就说过Pair<Student>是不能转换成Pair<Person>的。现在就可以考虑,把这个方法的参数范围扩大一点,让它也可以打印Pair<Student>的内容。

这时候通配符类型就上场了。我们可以把方法的形参类型从Pair<Person>改成Pair<? extends Person>,这样子就可以传递一个Pair<Student>类型的实参给方法了。

需要注意到的是尖括号里面的? extends Person中的问号,这个”?”就是通配符。通配听起来像什么,就是百搭,是一个不确定的因素,有可能是任何的东西,不过在上面加上了extends Person,这就把这个百搭限定了,这个”?”只能是Person的子类或者Person类本身。其中Person就是通配符的上界,即Person的父类Object类是不在? extends Person范围内的。

上一节提到参数化类型转换成原始类型会导致存在潜在的危险,破坏了泛型机制的安全性,那么通配符会不会呢?看下面代码。

Pair<Student> ps = new Pair<>();
Pair<? extends Person> pp = ps;
pp.setFirst(new Person());

其中pp.setFirst(new Person());这行代码编译出错。原因很简单,setFirst方法现在是这样子的void setFirst(? extends Person),这样子编译器只知道需要某个Person类的子类型,但不知道具体是什么类型(要知道Person类的子类型千奇百怪呢)。因为没法确定,为了保证类型安全,编译器拒绝传递任何特定的类型。但是传递一个null倒是可以,pp.setFirst(null);这样子是能编译通过的。

既然更改器方法不行,那么访问器方法呢?比如说? extends Person getFirst(),很明显,我们知道getFirst方法会返回一个Person类型(Person的子类也可以说是Person类型)的值,所以这是安全的,没有任何问题。

4. 通配符的下界

上一节讲了通配符的上界,我们很容易想到它应该还有下界,还是以上一节的关系,那么“? super Student”中,Student就是通配符的下界,这样子就把通配符限定在Student类和其父类范围内。上界跟下界,一个用extends限定,一个用super限定。其实这两者很容易理解,我们就把”?”当做占位符就好了。比如我有个类型S(像是方程式里的变量X),那么

  • ? extends S代表的范围就是S类及其子类
  • ? super S代表的范围就是S类及其父类

上和下是反义词,通配符的上界和通配符下界的行为是相反的,上一节讲到的访问器方法和更改器方法的问题。在通配符下界中就是这样的

  • void setFirst(? super Student) // 可用
  • ? super Person getFirst() // 返回Object类型

也很容易理解,在setFirst方法中,编译器虽然不知道参数的具体类型,但是可以安全地使用Student类型,Student父类型可以转换成Student类型而不会有问题;

而在getFirst方法中呢,编译器也不知道是Student的父类是什么,所以只能取出Object的实例(毕竟Object是所有的类的超类),你可以在取出来后再决定把它强转成一个Student类还是其某一个具体的父类来使用。

5. 无限定通配符

实际上还有无限定的通配符,例如Pair<?>,看起来与原始类型Pair没什么区别,实际上有很大不同,看看Pair<?>的两个方法

  • void setFirst(?)
  • ? getFirst()

其中getFirst方法只能返回Object类型的值,而setFirst方法不能被调用(不过setFirst(null)还是可以的)。而原始类型Pair可以针对Object类型操作,上面两个方法在原始类型Pair中就是这样子

  • void setFirst(Object)
  • Object getFirst()

那这么脆弱的类型我们要来有何用呢?其实它对许多简单的操作非常有用,例如下面这个方法用来测试一个pair是否含有null引用,它不需要实际类型

public static boolean hasNull(Pair<?> p) {
    return p.getFirst() == null || p.getSecond() == null;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值