热门标签 | 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类型的元素。

推荐阅读
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
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社区 版权所有