Saul's blog Saul's blog
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)

Saul.J.Wu

立身之本,不在高低。
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)
  • Java入门基础

  • Java核心基础

    • 多线程

    • Java常用类

    • 枚举类与注解

    • Java集合

    • 数据结构与算法

    • 泛型

      • 什么是泛型
      • 使用泛型
      • 编写泛型
      • 擦拭法
      • 通配符
        • extends通配符
          • 作用
          • 使用extends限定T类型
          • 小结
        • super通配符
        • 对比extends和super通配符
        • PECS原则
        • 无限定通配符
        • 总结
    • IO流

    • 网络编程

    • 反射

    • 函数式编程

  • 设计模式

  • Web开发

  • SpringBoot

  • 微服务

  • Elasticsearch

  • 运维

  • 后端
  • Java核心基础
  • 泛型
SaulJWu
2020-12-27

通配符

# extends通配符

我们前面已经讲到了泛型的继承关系:Pair<Integer>不是Pair<Number>的子类。

假设我们定义了Pair<T>:

class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

然后,我们又针对Pair<Number>类型写了一个静态方法,它接收的参数类型是Pair<Number>:

public class PairHelper {
    static int add(Pair<Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }
}
1
2
3
4
5
6
7

上述代码是可以正常编译的。使用的时候,我们传入:

int sum = PairHelper.add(new Pair<Number>(1, 2));
1

注意:传入的类型是Pair<Number>,实际参数类型是(Integer, Integer)。

既然实际参数是Integer类型,试试传入Pair<Integer>:

 Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
1
2
3

直接运行,会得到一个编译错误:

incompatible types: Pair<Integer> cannot be converted to Pair<Number>
1

原因很明显,因为Pair<Integer>不是Pair<Number>的子类,因此,add(Pair<Number>)不接受参数类型Pair<Integer>。

但是从add()方法的代码可知,传入Pair<Integer>是完全符合内部代码的类型规范,因为语句:

Number first = p.getFirst();
Number last = p.getLast();
1
2

实际类型是Integer,引用类型是Number,没有问题。问题在于方法参数类型定死了只能传入Pair<Number>。

有没有办法使得方法参数接受Pair<Integer>?

办法是有的,这就是使用Pair<? extends Number>使得方法接收所有泛型类型为Number或Number子类的Pair类型。

我们把代码改写如下:

//只需要修改add方法
static int add(Pair<? extends Number> p) {
    Number first = p.getFirst();
    Number last = p.getLast();
    return first.intValue() + last.intValue();
}
1
2
3
4
5
6

运行结果:

579
1

这样一来,给方法传入Pair<Integer>类型时,它符合参数Pair<? extends Number>类型。

这种使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。

除了可以传入Pair<Integer>类型,我们还可以传入Pair<Double>类型,Pair<BigDecimal>类型等等,因为Double和BigDecimal都是Number的子类。

泛型的向上转型: 如果我们考察对Pair<? extends Number>类型调用getFirst()方法,实际的方法签名变成了:

<? extends Number> getFirst();
1

即返回值是Number或Number的子类,因此,可以安全赋值给Number类型的变量:

Number x = p.getFirst();
1

然后,我们不可预测实际类型就是Integer,例如,下面的代码是无法通过编译的:

Integer x = p.getFirst();
1

这是因为实际的返回类型可能是Integer,也可能是Double或者其他类型,编译器只能确定类型一定是Number的子类(包括Number类型本身),但具体类型无法确定。

我们再来考察一下Pair<T>的set方法:

public class Main {
    public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        int n = add(p);
        System.out.println(n);
    }

    static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        p.setFirst(new Integer(first.intValue() + 100));
        p.setLast(new Integer(last.intValue() + 100));
        return p.getFirst().intValue() + p.getFirst().intValue();
    }
}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

不出意外,我们会得到一个编译错误:

incompatible types: Integer cannot be converted to CAP#1
where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
1
2
3

编译错误发生在p.setFirst()传入的参数是Integer类型。

有些童鞋会问了,既然p的定义是Pair<? extends Number>,那么setFirst(? extends Number)为什么不能传入Integer?

原因还在于擦拭法。

如果我们传入的p是Pair<Double>,显然它满足参数定义Pair<? extends Number>,

然而,Pair<Double>的setFirst()显然无法接受Integer类型。

这就是<? extends Number>通配符的一个重要限制:方法参数签名setFirst(? extends Number)无法传递任何Number的子类型给setFirst(? extends Number)。

这里唯一的例外是可以给方法参数传入null:

p.setFirst(null); // ok, 但是后面会抛出NullPointerException
p.getFirst().intValue(); // NullPointerException
1
2

# 作用

extends通配符的作用:

如果我们考察Java标准库的java.util.List<T>接口,它实现的是一个类似“可变数组”的列表,主要功能包括:

public interface List<T> {
    int size(); // 获取个数
    T get(int index); // 根据索引获取指定元素
    void add(T t); // 添加一个新元素
    void remove(T t); // 删除一个已有元素
}
1
2
3
4
5
6

现在,让我们定义一个方法来处理列表的每个元素:

int sumOfList(List<? extends Integer> list) {
    int sum = 0;
    for (int i=0; i<list.size(); i++) {
        Integer n = list.get(i);
        sum = sum + n;
    }
    return sum;
}
1
2
3
4
5
6
7
8

为什么我们定义的方法参数类型是List<? extends Integer>而不是List<Integer>?从方法内部代码看,传入List<? extends Integer>或者List<Integer>是完全一样的,但是,注意到List<? extends Integer>的限制:

  • 允许调用get()方法获取Integer的引用;
  • 不允许调用set(? extends Integer)方法并传入任何Integer的引用(null除外)。

因此,方法参数类型List<? extends Integer>表明了该方法内部只会读取List的元素,不会修改List的元素(因为无法调用add(? extends Integer)、remove(? extends Integer)这些方法。换句话说,这是一个对参数List<? extends Integer>进行只读的方法(恶意调用set(null)除外)。

# 使用extends限定T类型

在定义泛型类型Pair<T>的时候,也可以使用extends通配符来限定T的类型:

public class Pair<T extends Number> { ... }
1

现在,我们只能定义:

Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Double> p3 = null;
1
2
3

因为Number、Integer和Double都符合<T extends Number>。

非Number类型将无法通过编译:

Pair<String> p1 = null; // compile error!
Pair<Object> p2 = null; // compile error!
1
2

因为String、Object都不符合<T extends Number>,因为它们不是Number类型或Number的子类。

# 小结

使用类似<? extends Number>通配符作为方法参数时表示:

  • 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();;
  • 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);。

即一句话总结:使用extends通配符表示可以读,不能写。

使用类似<T extends Number>定义泛型类时表示:

  • 泛型类型限定为Number以及Number的子类。

# super通配符

我们前面已经讲到了泛型的继承关系:Pair<Integer>不是Pair<Number>的子类。

考察下面的set方法:

void set(Pair<Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}
1
2
3
4

传入Pair<Integer>是允许的,但是传入Pair<Number>是不允许的。

和extends通配符相反,这次,我们希望接受Pair<Integer>类型,以及Pair<Number>、Pair<Object>,因为Number和Object是Integer的父类,setFirst(Number)和setFirst(Object)实际上允许接受Integer类型。

我们使用super通配符来改写这个方法:

void set(Pair<? super Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}
1
2
3
4

注意到Pair<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。

下面的代码可以被正常编译:

public class Main {
	Pair<Number> p1 = new Pair<>(12.3, 4.56);
	Pair<Integer> p2 = new Pair<>(123, 456);
	setSame(p1, 100);
    setSame(p2, 200);
    System.out.println(p1.getFirst() + ", " + p1.getLast());
    System.out.println(p2.getFirst() + ", " + p2.getLast());   
}


static void setSame(Pair<? super Integer> p, Integer n) {
    p.setFirst(n);
    p.setLast(n);
}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

运行结果:

100, 100
200, 200
1
2

考察Pair<? super Integer>的setFirst()方法,它的方法签名实际上是:

void setFirst(? super Integer);
1

因此,可以安全地传入Integer类型。

再考察Pair<? super Integer>的getFirst()方法,它的方法签名实际上是:

? super Integer getFirst();
1

这里注意到我们无法使用Integer类型来接收getFirst()的返回值,即下面的语句将无法通过编译:

Integer x = p.getFirst();
1

因为如果传入的实际类型是Pair<Number>,编译器无法将Number类型转型为Integer。

注意:虽然Number是一个抽象类,我们无法直接实例化它。但是,即便Number不是抽象类,这里仍然无法通过编译。此外,传入Pair<Object>类型时,编译器也无法将Object类型转型为Integer。

唯一可以接收getFirst()方法返回值的是Object类型:

Object obj = p.getFirst();
1

因此,使用<? super Integer>通配符表示:

  • 允许调用set(? super Integer)方法传入Integer的引用;
  • 不允许调用get()方法获得Integer的引用。

唯一例外是可以获取Object的引用:Object o = p.getFirst()。

换句话说,使用<? super Integer>通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。

# 对比extends和super通配符

我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:

  • <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
  • <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

一个是允许读不允许写,另一个是允许写不允许读。

先记住上面的结论,我们来看Java标准库的Collections类定义的copy()方法:

public class Collections {
    // 把src的每个元素复制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}
1
2
3
4
5
6
7
8
9

它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。

这个copy()方法的定义就完美地展示了extends和super的意图:

  • copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
  • copy()方法内部也不会修改src,因为不能调用src.add(T)。

这是由编译器检查来实现的。如果在方法代码中意外修改了src,或者意外读取了dest,就会导致一个编译错误:

public class Collections {
    // 把src的每个元素复制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        ...
        T t = dest.get(0); // compile error!
        src.add(t); // compile error!
    }
}
1
2
3
4
5
6
7
8

这个copy()方法的另一个好处是可以安全地把一个List<Integer>添加到List<Number>,但是无法反过来添加:

// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);

// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);
1
2
3
4
5
6
7

而这些都是通过super和extends通配符,并由编译器强制检查来实现的。

# PECS原则

何时使用extends,何时使用super?

为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。

即:

  • 如果需要返回T,它是生产者(Producer),要使用extends通配符;

  • 如果需要写入T,它是消费者(Consumer),要使用super通配符。

还是以Collections的copy()方法为例:

public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i); // src是producer
            dest.add(t); // dest是consumer
        }
    }
}
1
2
3
4
5
6
7
8

需要返回T的src是生产者,因此声明为List<? extends T>,需要写入T的dest是消费者,因此声明为List<? super T>。

# 无限定通配符

我们已经讨论了<? extends T>和<? super T>作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?:

void sample(Pair<?> p) {
}
1
2

因为<?>通配符既没有extends,也没有super,因此:

  • 不允许调用set(T)方法并传入引用(null除外);
  • 不允许调用T get()方法并获取T引用(只能获取Object引用)。

换句话说,既不能读,也不能写,那只能做一些null判断:

static boolean isNull(Pair<?> p) {
    return p.getFirst() == null || p.getLast() == null;
}
1
2
3

大多数情况下,可以引入泛型参数<T>消除<?>通配符:

static <T> boolean isNull(Pair<T> p) {
    return p.getFirst() == null || p.getLast() == null;
}
1
2
3

<?>通配符有一个独特的特点,就是:Pair<?>是所有Pair<T>的超类:

public class Main {
	public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        Pair<?> p2 = p; // 安全地向上转型
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }    
}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

运行结果:

123, 456
1

上述代码是可以正常编译运行的,因为Pair<Integer>是Pair<?>的子类,可以安全地向上转型。

# 总结

  • <?>允许所有泛型的引用调用
  • 通配符指定上限
    • extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
  • 通配符指定下限
    • super:使用时指定的类型不能小于操作的类,即>=
  • 举例:
    • <?extends Number> (无穷小, Number] - 只允许泛型为Number及Number子类的引用调用
    • <? super Number> [Number , 无穷大) - 只允许泛型为Number及Number父类的引用调用
    • <? extends Comparable> - 只允许泛型为实现Comparable接口的实现类的引用调用

使用类似<? super Integer>通配符作为方法参数时表示:

  • 方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);;
  • 方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();。

即使用super通配符表示只能写不能读。

使用extends和super通配符要遵循PECS原则。

无限定通配符<?>很少使用,可以用<T>替换,同时它是所有<T>类型的超类。

帮我改善此页面 (opens new window)
#extends#super#T
上次更新: 2021/01/07, 09:35:27
擦拭法
IO流概述

← 擦拭法 IO流概述→

最近更新
01
zabbix学习笔记二
02-28
02
zabbix学习笔记一
02-10
03
Linux访问不了github
12-08
更多文章>
Theme by Vdoing | Copyright © 2020-2022 Saul.J.Wu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式