美文网首页
泛型通配符详解——?

泛型通配符详解——?

作者: fanderboy | 来源:发表于2020-01-11 17:50 被阅读0次

为什么要用通配符呢?

在java中,数组是可以协变的,比如dog extends Animal,那么Animal[] 与dog[]是兼容的。而集合是不能协变的,也就是说List<Animal>不是List<dog>的父类,这时候就可以用到通配符了。
一、基本概念:

在学习Java泛型的过程中, 通配符是较难理解的一部分. 主要有以下三类:

  1. 无边界的通配符(Unbounded Wildcards), 就是<?>, 比如List<?>.
      无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.
  2. 固定上边界的通配符(Upper Bounded Wildcards):
      使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据. 要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界. 注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类.
  3. 固定下边界的通配符(Lower Bounded Wildcards):
      使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据. 要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界. 注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界.

二、基本使用方法:

  1. 无边界的通配符的使用, 我们以在集合List中使用<?>为例. 如:
public static void printList(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

public static void main(String[] args) {
    List<String> l1 = new ArrayList<>();
    l1.add("aa");
    l1.add("bb");
    l1.add("cc");
    printList(l1);
    List<Integer> l2 = new ArrayList<>();
    l2.add(11);
    l2.add(22);
    l2.add(33);
    printList(l2);
    
}

这种使用List<?>的方式就是父类引用指向子类对象.
注意, 这里的printList方法不能写成public static void printList(List<Object> list)的形式, 原因集合是不能协变的, 虽然Object类是所有类的父类, 但是List<Object>跟其他泛型的List如List<String>, List<Integer>不存在继承关系, 因此会报错.
有一点我们必须明确, 我们不能对List<?>使用add方法, 仅有一个例外, 就是add(null). 为什么呢? 因为我们不确定该List的类型, 不知道add什么类型的数据才对, 只有null是所有引用数据类型都具有的元素. 请看下面代码:

public static void addTest(List<?> list) {
    Object o = new Object();
    // list.add(o); // 编译报错
    // list.add(1); // 编译报错
    // list.add("ABC"); // 编译报错
    list.add(null);
}

由于我们根本不知道list会接受到具有什么样的泛型List, 所以除了null之外什么也不能add.
还有, List<?>也不能使用get方法, 只有Object类型是个例外. 原因也很简单, 因为我们不知道传入的List是什么泛型的, 所以无法接受得到的get, 但是Object是所有数据类型的父类, 所以只有接受他可以, 请看下面代码:

public static void getTest(List<?> list) {
    // String s = list.get(0); // 编译报错
    // Integer i = list.get(1); // 编译报错
    Object o = list.get(2);
}
  1. 固定上边界的通配符的使用, 我仍旧以List为例来说明:
public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list) {
        // 注意这里得到的n是其上边界类型的, 也就是Number, 需要将其转换为double.
        s += n.doubleValue();
    }
    return s;
}

public static void main(String[] args) {
    List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
    System.out.println(sumOfList(list1));
    List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4);
    System.out.println(sumOfList(list2));
}

有一点我们需要记住的是, List<? extends E>不能使用add方法, 请看如下代码:

public static void addTest2(List<? extends Number> l) {
    // l.add(1); // 编译报错
    // l.add(1.1); //编译报错
    l.add(null);
}

原因很简单, 泛型<? extends E>指的是E及其子类, 这里传入的可能是Integer, 也可能是Double, 我们在写这个方法时不能确定传入的什么类型的数据, 如果我们调用:

List<Integer> list = new ArrayList<>();
addTest(list);

那么我们之前写的add(1.1)就会出错, 反之亦然, 所以除了null之外什么也不能add. 但是get的时候是可以得到一个Number, 也就是上边界类型的数据的, 因为不管存入什么数据类型都是Number的子类型, 得到这些就是一个父类引用指向子类对象.

  1. 固定下边界通配符的使用,仍以List为例:
public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

public static void main(String[] args) {
    List<Object> list1 = new ArrayList<>();
    addNumbers(list1);
    System.out.println(list1);
    List<Number> list2 = new ArrayList<>();
    addNumbers(list2);
    System.out.println(list2);
    List<Double> list3 = new ArrayList<>();
    // addNumbers(list3); // 编译报错
}

我们看到, List<? super E>是能够调用add方法的, 因为我们在addNumbers所add的元素就是Integer类型的, 而传入的list不管是什么, 都一定是Integer或其父类泛型的List, 这时add一个Integer元素是没有任何疑问的. 但是, 我们不能使用get方法, 请看如下代码:

public static void getTest2(List<? super Integer> list) {
    // Integer i = list.get(0); //编译报错
    Object o = list.get(1);
}

这个原因也是很简单的, 因为我们所传入的类都是Integer的类或其父类, 所传入的数据类型可能是Integer到Object之间的任何类型, 这是无法预料的, 也就无法接收. 唯一能确定的就是Object, 因为所有类型都是其子类型.
三、总结:

我们要记住这么几个使用原则, 有人将其称为PECS(即"Producer Extends, Consumer Super", 网上翻译为"生产者使用extends, 消费者使用super", 我觉得还是不翻译的好). 也有的地方写作"in out"原则, 总的来说就是:

1.in或者producer就是你要读取出数据以供随后使用(想象一下List的get), 这时使用extends关键字, 固定上边界的通配符. 你可以将该对象当做一个只读对象;
2.out或者consumer就是你要将已有的数据写入对象(想象一下List的add), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
3.当你希望in或producer的数据能够使用Object类中的方法访问时, 使用无边界通配符;
4.当你需要一个既能读又能写的对象时, 就不要使用通配符了.

相关文章

  • 详解Java泛型之4——一个例子理解泛型带来的好处

    前面我介绍了关于泛型、通配符以及泛型擦除的相关知识点,大家可以参考以下文章: 详解Java泛型之1——入门泛型必懂...

  • 详解Java泛型之3——十分钟理解泛型擦除

    前面我们介绍了泛型以及通配符的基础概念,可以参考文章: 详解Java泛型之1——入门泛型必懂的知识点[https:...

  • 泛型通配符详解——?

    为什么要用通配符呢? 在java中,数组是可以协变的,比如dog extends Animal,那么Animal[...

  • Android 学习(一):Java 泛型

    Java泛型学习 1.0 泛型常用案例 2.0 泛型方法 3.0 泛型类 4.0 通配符 ? 通配符,占位符,标识...

  • Kotlin泛型方法

    泛型约束 泛型通配符

  • Java-API-集合框架(三)-泛型

    泛型的由来和基本使用 泛型的擦除 泛型类的使用 泛型方法的使用 泛型接口 泛型通配符(?) 通配符? 在api中的...

  • java 泛型通配符和边界

    1. 通配符 泛型中常用的通配符: 我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T...

  • Java泛型

    1. 泛型通配符与PECS 为什么要使用泛型通配符和边界 List泛型转换需要用到通配符 ? “装A的List” ...

  • 注解

    作用: 在类中定义泛型: 在方法中定义泛型: 给泛型设置通配符上限: 给泛型设置通配符下限: 泛型擦除: 兼容性:...

  • Java中通配符的范围上限与范围下限

    泛型中通配符 我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V 等等,这...

网友评论

      本文标题:泛型通配符详解——?

      本文链接:https://www.haomeiwen.com/subject/wxlbactx.html