热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

如何理解Java中的泛型通配符

泛型通配符的作用为了让大家更好地理解泛型通配符的作用及使用场景,我们先来看一个例子:我们假定有一个抽象类Shape,它有一个抽象方法dr

泛型通配符的作用

为了让大家更好地理解泛型通配符的作用及使用场景,我们先来看一个例子:
我们假定有一个抽象类Shape,它有一个抽象方法draw,我们规定draw方法将会打印一串描述性的字符串。TriangleRectangleCircleShape的子类,均重写了draw方法()。

// 超类
abstract class Shape{/**** draw方法将会打印一串描述性的字符串**/abstract protected void draw();
}class Triangle extends Shape{@Overrideprotected void draw() {System.out.println("draw a Triangle");}
}
class Rectangle extends Shape{@Overrideprotected void draw() {System.out.println("draw a Rectangle");}
}
class Circle extends Shape{@Overrideprotected void draw() {System.out.println("draw a Circle");}
}

我们现在有这样一个需求:我们需要提供一个方法,它能够接受任意一个Shape,然后在方法内部调用它的draw方法。
我们可以利用方法可以重载这一特性来实现:

public static void shapeToDraw(Triangle triangle){triangle.draw();}public static void shapeToDraw(Rectangle rectangle){rectangle.draw();}public static void shapeToDraw(Circle circle){circle.draw();} // 调用shapeToDraw(new Triangle()); // 打印draw a TriangleshapeToDraw(new Circle()); // 打印draw a Circle

我们可以为每一种具体的子类实现一个特定的方法,所有的方法除了入参以外完全一样。编译器会根据实际传入的参数类型帮我们判断哪一个方法是我们真正需要的。 虽然方法的重载非常有用,但是用在此处却不太合适。想象一下,如果Shape有100个子类,为了兼顾所有的情况,我们需要提供100个对应的方法!

稍微有一点经验的开发者很容易想到下面的实现:

public static void shapeToDraw(Shape shape){shape.draw();}// 调用shapeToDraw(new Triangle()); // 打印draw a TriangleshapeToDraw(new Circle()); // 打印draw a Circle

shapeToDraw(Shap shap)方法接收一个抽象的Shape,而不是具体的子类。得益于Java的多态性,该程序可以正常地运行,且具备一定的扩展性。例如,如果后续我们为Shape再添加一个子类Square,该方法不需要做任何修改。

现在,我们的需求发生了变化: 我们需要提供一个方法,它能够接受任意一个Shape集合,然后在方法内部依次调用集合元素的draw方法。(为了简化问题,我们假定集合特指List)
有了上面需求的经验,我们可以很容易地得到如下实现:

public static void shapesToDraw(List<Shape> shapes){for (Shape shape : shapes){shape.draw();}}

shapesToDraw方法接受一个List&#xff0c;在方法内部迭代调用draw方法。由于List里面可以装载Shape的所有子类&#xff0c;因此这个实现看起来似乎是完美的。但是如果使用者传入的是一个List呢&#xff1f;该方法还能正常运行吗&#xff1f;答案是不能。该方法不能接收List作为入参&#xff0c;原因是&#xff1a;一个List并不是一个List&#xff0c;这确实让人感到非常费解&#xff0c;因为CircleShape的子类&#xff0c;那么一个Circle就是一个Shape&#xff0c;为什么一个List不能是一个List呢&#xff1f;
要想理解这个悖论&#xff0c;我们可以看一下下面这个例子&#xff1a;

// 先初始化一个 List 和一个 List
List<Shape> shapeList;
List<Circle> circleList &#61; new ArrayList<>();
// 我们假定一个 List 是一个 List&#xff0c;那么这样赋值是完全没有问题的
shapeList &#61; circleList; //实际上编译不能通过// 往List中添加一个Triangle&#xff0c;这当然没有任何问题
shapeList.add(new Triangle());
// 因为 shapeList 与 circleList 指向同一个List&#xff0c;因此往 shapeList 中添加的元素通过 circleList 当然可以访问到
// 但是 circleList 是一个 List,因此调用get方法应该返回一个Circle &#xff0c;然而实际上&#xff0c;该位置存放的却是一个Triangle&#xff01;
Circle circle &#61; circleList.get(0); // 转型错误&#xff01;

由于将一个List 当成一个 List 会出现上述问题&#xff0c;因此&#xff0c;Java 语言规定, 即使集合之间的元素存在继承关系&#xff0c;也不能认为泛型的集合之间存在继承关系&#xff0c;

显然这不是我们希望的&#xff0c;因为使用者当然希望程序将一个 List 看做是一个 List。那么该如何实现呢&#xff1f;答案是&#xff1a;泛型通配符。
我们只需要将上述程序稍作修改&#xff0c;就能满足现在的需求&#xff1a;

public static void shapesToDraw(List<? extends Shape> shapes){for (Shape shape : shapes){shape.draw();}}

List描述了这样一类List&#xff1a;List的泛型类型是Shape的子类(当然也可以是Shape本身)。 这一点是非常重要的&#xff0c;在使用泛型通配符之前&#xff0c;我们只能描述List内部元素的类型&#xff0c;如List描述了一个内部元素是Shape子类(因为Shape是一个抽象类)的List。而有了泛型通配符以后&#xff0c;我们拥有了描述泛型类型的能力&#xff01;


泛型通配符上界

基本语法形式&#xff1a;

List<? extends Shape>

它规定了泛型类型的上界。也就是说&#xff0c;如果使用上述表达式作为某个方法的入参&#xff0c;那么在实际调用方法时&#xff0c;可以传入的泛型类型必须为 Shape的子类。另外还有一点需要特别注意&#xff1a;


因为我们只知道泛型类型是Shape的子类而不能确定其具体的泛型类型&#xff0c;因此往该List中添加任何元素都是非法的(会引起编译错误)。


我们可以这样理解这句话&#xff1a;
我们只知道 List 接受的 List 泛型是 Shape 的子类&#xff0c;因此它既可以是List 也可以是List&#xff0c;还可以是List。当我们想要往这个List中添加数据的时候会发生什么&#xff1f;我们根本不知道应该向它传入什么样的数据&#xff01;&#xff01;&#xff01; 我们应该向它传入一个Circle吗&#xff1f;如果调用方法的时候实际传入的参数是一个List怎么办&#xff1f;List并不能装载一个Circle&#xff01;那我们向它传入一个Triangle&#xff1f;一个Rectangle&#xff1f;都不行&#xff01;不论我们向它传入什么类型的对象&#xff0c;我们都会遇到前面的问题。最根本的原因是&#xff1a;我们无法确定泛型的类型&#xff0c;因此我们无法确定应该向该List添加什么类型的元素

当我需要从List中取出元素的时候&#xff0c;会发生什么呢&#xff1f;
因为我们知道List实际的泛型类型一定是Shape的子类&#xff0c;那么List中的元素类型也一定是Shape的子类&#xff0c;因此我们使用Shape或者Shape的父类(本例中Shape没有父类)去接收取出的元素一定是安全的。


泛型通配符下界

基本语法形式&#xff1a;

List<? super Triangle>

它规定了泛型类型的下界。也就是说&#xff0c;如果使用上述表达式作为某个方法的入参&#xff0c;那么在实际调用方法时&#xff0c;可以传入的泛型类型必须为 Triangle的父类(当然也包括Triangle本身)。在本例中&#xff0c;传入的参数类型可以是ListList&#xff0c;当然也可以是List。另外还有一点需要特别注意&#xff1a;


因为我们只知道泛型类型是Triangle的父类而不能确定其具体的泛型类型&#xff0c;因此从该List取出元素的时候&#xff0c;我们不能知道该元素的确切类型。但是我们知道&#xff0c;任何一个类都是Object的子类&#xff0c;因此可以使用Object来接收该元素&#xff0c;除此之外的其他任何类型都不行。(但是得到一个Object类型的元素对我们来说似乎没有太大的作用&#xff0c;因为我们使用泛型就是希望能够得到更加确切的数据类型&#xff0c;而不是一个笼统的Object)


当我们需要往List中添加元素的时候&#xff0c;会发生什么呢&#xff1f;
因为我们知道List实际的泛型类型一定是Triangle的父类&#xff0c;那么List中的元素类型也一定是Triangle的父类&#xff0c;因此我们往List中添加Triangle或者Triangle的子类元素一定是安全的。

基于上述原因&#xff0c;阿里巴巴Java开发手册中规定&#xff1a;
在这里插入图片描述


List

通过上面的分析我们已经知道了List表示List的泛型类型(一定要注意是泛型类型&#xff0c;而不是元素类型)可以为任意类型。也就是说如果一个方法签名的参数是List&#xff0c;那么实际传入的类型可以是ListListList或者任意一个带泛型的List(甚至可以是不带泛型的List)。
当我们试图往这个List中插入元素的时候&#xff0c;我们会得到一个编译错误&#xff0c;原因是我们无法得知泛型的确切类型(与上文上界通配符中的解释一致)。
当我们试图从这个List中取出元素的时候&#xff0c;我们只能得到一个Object(与上文下界通配符中的解释一致)。


结论


  1. 泛型通配符定义的是泛型的类型&#xff0c;而不是集合元素的类型&#xff1b;
  2. 上界通配符&#xff0c;形如适合频繁往外读取数据的场景&#xff1b;
  3. 下界通配符&#xff0c;形如适合频繁插入数据的场景&#xff1b;
  4. 无法往List插入任何数据&#xff0c;只能从List中得到Object类型的元素。

推荐阅读
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • Final关键字的含义及用法详解
    本文详细介绍了Java中final关键字的含义和用法。final关键字可以修饰非抽象类、非抽象类成员方法和变量。final类不能被继承,final类中的方法默认是final的。final方法不能被子类的方法覆盖,但可以被继承。final成员变量表示常量,只能被赋值一次,赋值后值不再改变。文章还讨论了final类和final方法的应用场景,以及使用final方法的两个原因:锁定方法防止修改和提高执行效率。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 如何搭建Java开发环境并开发WinCE项目
    本文介绍了如何搭建Java开发环境并开发WinCE项目,包括搭建开发环境的步骤和获取SDK的几种方式。同时还解答了一些关于WinCE开发的常见问题。通过阅读本文,您将了解如何使用Java进行嵌入式开发,并能够顺利开发WinCE应用程序。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
author-avatar
武艺最新单曲问月09
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有