专栏文章导航

Java泛型入门篇: 泛型类、泛型接口以及泛型方法
Java泛型进阶篇: 无界通配符、上界通配符以及下界通配符
Java泛型原理篇: 类型擦除以及桥接方法


文章目录

  • 前言
  • 1. 无界通配符
    • 使用方法
    • List<?>与List
    • 应用场景
  • 2. 上界通配符
    • 使用方法
      • 基本写法
      • 指定泛型标识
      • 多重上界
    • 特点
    • 应用场景
  • 3. 下界通配符
    • 使用方法
    • 特点
    • 应用场景

前言

    在介绍通配符之前,首先我们来思考如下问题:

public class Animal {}
public class Cat extends Animal {}
public static void main(String[] args) {Cat cat = new Cat();  Animal animal = cat;  ❶List<Cat> cats = new ArrayList<>();List<Animal> animals = cats;  ❷
}

  以上代码中①处以及②处代码是否正确?

  ①处的代码没有任何问题,是多态的典型用法,即父类引用指向子类对象,而②处的代码与①处类似,但是却提示编译错误。原因是因为Java泛型中规定,即使泛型类型具有继承关系,但是并不意味着该泛型类型的容器也具有继承关系。在上一篇文章中曾介绍过,我们可以把泛型理解成一个标签,一个list中贴上了animal的标签,另一个list贴上了cat的标签,可以说cat是animal的子类,但是不能说贴了cat标签的list就是贴了animal标签list的子类,他们实际上都是list。

  知道泛型的这一特性后,就出现了一个比较麻烦的问题: 泛型容器似乎没办法实现多态。

  举个例子,在上例中我们为Animal添加一个方法getFood,同时Cat类需要重写该方法

public class Animal {public String getFood() {return "食肉动物吃肉,食草动物吃草";}
}
public class Cat implements Animal {@Overridepublic String getFood() {return "猫粮,小鱼干,猫薄荷";}
}

  如果想在控制台输出不同动物的食物,此时需要定一个print方法,利用多态特性,形参可定义为父类Animal,这样实参就可以传Animal以及其子类对象了。

public void print(Animal animal) {System.out.println(animal.getFood());
}public static void main(String[] args) {Test test = new Test();test.print(new Animal());test.print(new Cat());
}

  但是如果想打印一批动物的食物信息,就无法利用多态这一特性,因为List<Cat>并不是List<Animal>的子类,所以只能额外增加方法。

public void printAnimalFood(List<Animal> animals) {animals.forEach(a -> System.out.println(a.getFood());
}public void printCatFood(List<Cat> cats) {cats.forEach(c -> System.out.println(c.getFood());
}public static void main(String[] args) {Test test = new Test();List<Cat> cats = Arrays.asList(new Cat());test.printAnimalFood(cats);   // 编译错误,List<Cat>不是List<Animal>的子类test.printCatFood(cats);   // 正确执行
}

  如果还需要增加Dog,Rabbit等子类,那么就需要增加相应对应的方法printDogFood,printRabbitFood等,久而久之代码就会很臃肿,那么怎么解决这件事呢,那就是接下来要介绍的通配符的相关知识。

1. 无界通配符

使用方法

  Java泛型提供了通配符(Wildcards),用?表示,例如List<?>Set<?>等。我们知道Object是任意类的父类,而在Java泛型中,通配符就是任意同泛型类型容器的父类,如List<?>是所有泛型List的父类,为了与接下来的要介绍的另外两种形式的通配符予以区分,可以称之为无界通配符,意味没有界限限制。

  由于?不是泛型标识,所以也就无法在类、接口以及方法中传递,以下写法是不允许的

public class TestGeneric<?> {private ? a;
}public <?> ? test(? a) {// Do something
}

  这么做也是合情合理,因为?不符合Java变量的命名标准。所以?只能出现在引用中或方法入参中,如

// 出现在引用中
Class<?> cls = xxx;// 出现在方法入参中
public void print(List<?> list) {// Do something
}

  需要注意的是方法入参中的无界通配符只起到了告知的作用,并不能进行传递,也不需要传递(因为是任意类型)。

  总结:

  1. 无界通配符不能标注在泛型类、泛型接口以及泛型方法上
  2. 无界通配符只能使用在引用以及方法入参中,并且无法进行传递

  另外无界通配符也可以起到占位以及修饰作用,可以直观告诉使用者这里可以接受任意类型。比如Java中的反射,我们经常写成下面的写法:

Class<?> cls = xxx;

  这里其实通配符?是可以不写的,但是写了就可以显示的告诉使用者,这里可以是任意泛型类型。

List<?>与List

  知道了无界通配符的含义之后,我们可以把前言中的例子做一下优化

public void print(List<?> animals) {animals.forEach(a -> System.out.println(((Animal) a).getFood()));
}public static void main(String[] args) {Test test = new Test();List<Animal> animals = Arrays.asList(new Animal());List<Cat> cats = Arrays.asList(new Cat());test.print(animals);    // 正确执行test.print(cats);    // 正确执行
}

  使用无界通配符后,即使后续有新增的子类,此方法也可以正确执行。至此我们解决了参数传递问题,但是新问题出现了:使用无界通配符似乎与不使用泛型而直接使用List没有任何区别:list中的元素可以是任意类型,并且取元素时需要强制转换。无界通配符看起来并没有特殊的用途,那为什么要需要使用无界通配符呢?

  • List可以理解为持有任意Object类型的原始列表。
  • List<?>可以理解为想要使用泛型列表,但是不确定具体的类型,用?标识任意类型都可以。由于是泛型,所以列表中的元素的类型理应一致,但是以下写法是可以通过编译的
// 正确执行
List list = new ArrayList();
list.add(1);
list.add("2");
List<?> list2 = list;
public void print(List<?> list) {//..
}// 正确执行
public static void main(String[] args) {List list = new ArrayList();list.add(1);list.add("2");new Test().print(list);
}

  共同点:由于List<?>List在编译期间都无法确定元素的实际类型,所以获取的时候都为Object类型

List<?> list1 = ...;
Object obj1 = list1.get(0);List list2 = ...;
Object obj2 = list2.get(0);

  不同点:List<?>由于无法在编译期间确定泛型的实际类型,所以没法向List<?>中添加除了null外的任意类型元素。而List由于可以存放Object对象,所以List可以添加任意类型的元素。

List<?> list1 = new ArrayList<>();
list1.add(1);   // 编译错误
list1.add("2");   // 编译错误
list1.add(null);    // 正确执行List list2 = new ArrayList();
list2.add(1);   // 正确执行
list2.add("2");   // 正确执行

应用场景

  可以使用无界通配符来实现通用的方法,如本节中的优化方案,使用List<?>来实现print方法的通用,当然这里使用List的效果是一样的,下例可能更能说明问题

public void testMap(Map<String, ?> map) {// Do something
}
Map<String, String> map1 = ...;
Map<String, Integer> map2 = ...;
Map<String, Object> map3 = ...;Test test = new Test();
test.testMap(map1); // 正确执行
test.testMap(map2); // 正确执行
test.testMap(map3); // 正确执行

  testMap方法入参map的value想接受任意类型,只能使用无界通配符来进行占位。

2. 上界通配符

  无界通配符由于可以接受任意类型,所以某些情况下还是不太适用,往往需要强制转换类型,一是不方便,二是可能引发转换异常,这个时候就可能需要使用上界通配符来解决我们的问题了。

  上界通配符(Upper Bounde Wildcard),顾名思义,存在一个最上级的界限,即指定一个最高级别的父类,它表示对于该上界类型以及其子类都适用。

  我们也可以将无界通配符上界为Object上界通配符

使用方法

基本写法

  ? extends xx

  同无界通配符,由于包含?,所以这种写法只能出现在引用以及方法入参中,如:

List<? extends Number> numbers = xxx;
public void test(Class<? extends Number> cls) {}

指定泛型标识

  前面分析?并不能进行泛型类型传递,所以如果想在在泛型类、泛型接口、泛型方法中使用上界通配符,需要将?指定为标识类型,如

// 泛型类
public class TestGeneric<T extends Number> {private T t;public TestGeneric(T t) {this.t = t;}
}
// 泛型接口
public interface TestGerneric<T extends Number> {}
// 泛型
public <E extends Collection> void test(E e) {System.out.println(e.size());
}

多重上界

  当需要指定多个上界时,需要使用&来连接,并且只能在指定泛型标识的时候使用,例如:

public class TestGeneric<T extends LongFur & BlueEye> {}

  上例中表示该泛型类型为同时满足为两个指定上界类型或其子类的类型。

  需要注意的是,被指定的多个上界不能有有冲突的方法,否则会编译错误

特点

  我们在编译期只能知道上界通配符的上界是什么类型,所以在取元素时,只能获取到上界的类型。具体的实际类型只有在运行时才能确定,同时也构成了多态。

List<Integer> ints = Arrays.asList(1);
List<Double> doubles = Arrays.asList(2.2);
```java
public void test(List<? extends Number> numbers) {// 编译期间无法确定实际类型是Integer、Long、Double还是其他,只能使用Number接收// 运行时构成多态:Number number = 1; Number number = 2.2Number number = numbers.get(0);System.out.println(number);
}

  同时与无界通配符相似,由于编译期无法知晓具体的实际类型,所以不支持上界通配符的容器添加元素(或赋值)

List<? extends Number> list = new ArrayList<>();
list.add(1);  // 编译错误
list.add(2.2); // 编译错误
// 如果以上操作允许,那么list中的类型就不统一了

应用场景

  我们再使用上界通配符来优化前言的例子

public void print(List<? extends Animal> animals) {animals.forEach(a -> System.out.println(a.getFood()));
}public static void main(String[] args) {Test test = new Test();List<Animal> animals = Arrays.asList(new Animal());List<Cat> cats = Arrays.asList(new Cat());test.print(animals);    // 正确执行test.print(cats);    // 正确执行
}

  由于方法形参定义为List<? extends Animal>,所以不用担心转型的问题,获取的元素肯定为Animal类型(包括子类),也不用担心实参类型传递错误。

  通过优化我们可以感受到上界通配符的好处,既能够做到向无界通配符一样的通用型,又能够限制具体的类型范围,所以如果想达到此目的,就可以使用上界通配符,而实际编码中我们使用上界通配符的次数也是最多的。

  以JDK为例,Collection#addAll方法就使用了上界通配符

3. 下界通配符

  与上界通配符相反,下界通配符(Lower Bound Wildcard),顾名思义,存在一个最低级的界限,即指定一个最低级别的子类,它表示对于该下界类型以及其父类都适用。

使用方法

  下界通配符上界通配符类似,只需将extends改为super即可:

List<? super Integer> list = xxx;
public void test(Class<? super Integer> cls) {}

  值得注意的是下界通配符不支持制定泛型标识以及多重下界的写法。

特点

  与上界通配符相反,我们在编译期只能知道下界通配符的下界是什么类型,所以在添加元素时,只能向其中添加下界类型。

public void test(List<? super Integer> list) {// 编译期间无法确定实际类型是Integer还是Number或更高级别的父类,只能添加Integerlist.add(1);
}
List<Number> numbers = Arrays.asList(1, 2L, 3.3);
xxx.test(numbers);

  同时由于编译期无法知晓具体的实际类型,所以只能使用Object来接收获取的元素

List<? super Integer> list = new ArrayList<>();
// 编译失败,因为无法确定是否是Integer,有可能是Number
Integer i = list.get(0);
// 编译失败,因为无法确定是否是Number,有可能是Object
Number m = list.get(1);
// 编译正确,Object可以接受任意值
Object o = list.get(2);

应用场景

  下界通配符在实际使用中还是比较少的,可以通过JDK的实际例子来感受一下使用方式

  在java.util.Comparator接口中存在大量的下界通配符的使用,如

Java泛型进阶篇: 无界通配符、上界通配符以及下界通配符相关推荐

  1. Java泛型进阶 - 如何取出泛型类型参数

    在JDK5引入了泛型特性之后,她迅速地成为Java编程中不可或缺的元素.然而,就跟泛型乍一看似乎非常容易一样,许多开发者也非常容易就迷失在这项特性里. 多数Java开发者都会注意到Java编译器的类型 ...

  2. Java 基础进阶篇(四):抽象类与模板方法设计模式

    文章目录 一.抽象类.抽象方法概述 二.抽象类的特征 三.模板方法设计模式 3.1使用场景 3.2 实现步骤 3.3 写作文案例 补充:final 和 abstract 是什么关系? 一.抽象类.抽象 ...

  3. Java 基础进阶篇(十四):File 类常用方法

    File 类的对象代表操作系统的文件(文件.文件夹),File 类在 java.io.File 包下. File 类提供了诸如:创建文件对象代表文件,获取文件信息(大小.修改时间).删除文件.创建文件 ...

  4. JAVA SE 进阶篇 C3 解析XML文件,做一个jar工具包

    文章目录 P1 XML文件 1 XML文件概述 (1) 可扩展标记语言:XML (2) XML文件的书写规则和语法要求 2 创建一个XML文件 P2 解析给定的XML文件 1 XML解析器和W3C 2 ...

  5. java web进阶篇(八) Ajax(阿贾克斯)开发技术

    Ajax(Asynchronous JavaScript and XML)并不是一项新的技术,它产生的主要目的之一就是用于页面的局部刷新. 其本身是一门综合性的技术,应用了HTML.JavaScrip ...

  6. 面试测试开发工程师:Java测试进阶篇

    1. 按开发阶段划分 测试金字塔与业务测试分析 1.1 单元测试(Unit Testing) 手机功有很多,女孩子都喜欢用美颜功能,突然有一天美颜功能不可用了,怎么办?只针对这一功能的代码进行测试. ...

  7. 【Java数据结构】泛型详解+图文,通配符上界、下界

    0. 泛型的本质 0. 泛型的目的 1. 泛型的语法 1.1 泛型的使用 2. 包装类 2.1 装箱和拆箱 2.2.1练习题 3 .泛型如何编译 4.泛型的上界 5. 通配符 5.1通配符上界 5.2 ...

  8. java泛型程序设计——无限定通配符+通配符捕获

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 无限定通配符+通配符捕获 的相关知识: [1]无限定通配符相关 1. ...

  9. 【Java 泛型】泛型(泛型类型、原始类型、泛型方法)、通配符(上界、下界、无限制、继承)

    泛型(Generics) 泛型(Generics) 泛型类型(Generic Type) 多个类型参数 泛型类型的继承 原始类型(Raw Type) 泛型方法(Generic Method) 泛型方法 ...

最新文章

  1. IOS开发怎么UINavigationController设置title标题的颜色?
  2. ios开发学习-手势交互(Gesture)效果源码分享
  3. CentOS6.5更改ssh端口问题
  4. 第四天 用户管理和服务管理
  5. 理解“动心忍性”的含义
  6. 关于GC.Collect在不同机器上表现不一致问题
  7. 对用2遍dfs求有向图强连通分量的理解
  8. 20应用统计考研复试要点(part16)--应用多元分析
  9. SQL -- 多表查询
  10. 通孔的作用是什么linux,电路板空洞的作用是什么 如何区分PTH与NPTH两种通孔
  11. 【壹刊】Azure AD B2C(一)初识
  12. hdu3689(kmp+dp)
  13. forms角色验证,以普通用户身份登陆管理页面先弹出警告信息窗口
  14. thinkphp redis队列处理_教你用ThinkPHP中thinkphpqueue
  15. shell编程脚本练习题
  16. ORACLE 常用操作命令
  17. windows 10 英文版显示中文乱码
  18. 【虚拟光驱怎么安装Win7系统】
  19. Access2016学习1
  20. 电线 电流 和 断路器选择

热门文章

  1. 单反相机的传奇—佳能单反50年辉煌之路(连载十七)
  2. python将字典按行或按列写入csv文件
  3. 来来来,咱们聊一下 JWT。安全验证的知识 两篇文章就够了
  4. 19年拿了19个诺贝尔奖,日本科学为何“井喷”?
  5. android 发音乐通知到通知栏
  6. Linux中GCC编译工具集中个软件的用途、gcc的简单编译以及ELF文件格式
  7. 大胆预测,2019年最佳外置硬盘和便携式SSD非这四款莫属!
  8. Nickel 28就青山控股集团镍锍生产计划发表评论
  9. 【Python】过滤表情字符
  10. 显卡算力排行_RTX3090 时代最新GPU选购指南:哪款显卡配得上我的炼丹炉?