Effective Java笔记(31)利用有限制通配符来提升 API 的灵活性

        参数化类型是不变的( invariant ) 。 换句话说,对于任何两个截然不同的类型 Typel 和 Type2 而言, List<Type1 >既不是 List<Type 2 > 的子类型,也不是它的超类型 。虽然 L ist<String>不是 List<Object>的子类型,这与直觉相悖,但是实际上很有意义 。你可以将任何对象放进一个List<Object>中,却只能将字符串放进 List<String>中 。由于 List<String>不能像 List<O句ect> 能做任何事情,它不是一个子类型 。

        有时候,我们需要的灵活性要比不变类型所能提供的更多 。比如第 29 条中的堆楼 。 提醒一下,下面就是它的公共 API:

public class Stack<E> {public Stack();public void push(E e);public E pop();public boolean isEmpty();
}

        假设我们想要增加一个方法,让它按顺序将一系列的元素全部放到堆枝中 。 第一次尝试如下:

public void pushAll(Iterable<E> src) {for(E e : src)push(e);
}

        这个方法编译时正确无误,但是并非尽如人意 。 如果 Iterable 的 src 元素类型与堆栈的完全匹配,就没有问题 。 但是假如有一个 Stack<Number>,并且调用了 push (intVal),这里的工ntVal 就是 Integer 类型 。 这是可以的,因为 Integer 是 Number 的一个子类型 。 因此从逻辑上来说,下面这个方法应该可行 :

Stack <Number> numberStack = new Stack<>() ;
Iterable<Integer> integers = ...;
numberStack. pushAll(integers);

但是,如果尝试这么做,就会得到下面的错误消息,因为参数化类型是不可变的:

        幸运的是,有一种解决办法 。Java 提供了一种特殊的参数化类型,称作有限制的通配符类型(bounded wildcard type ),它可以处理类似的情况 。pushAll 的输入参数类型不应该为“ E 的 Iterable 接口”,而应该为“ E 的某个子类型的 Iterable 接口”通配符类型Iterable<?extends E >正是这个意思 。 (使用关键字 ex ten也有些误导 :回忆一下第29 条中的说法,确定了子类型( subtype )后,每个类型便都是自身的子类型,即使它没有将自身扩展 。)我们修改一下 pushAll 来使用这个类型:

public void pushAll(Iterable<? extends E> src) {for(E e : src)push(e);
}

        修改之后,不仅 Stack 可以正确无误地编译,没有通过初始的 pushAll 声明进行编译的客户端代码也一样可以 。 因为 Stack 及其客户端正确无误地进行了编译,你就知道一切都是类型安全的了 。

        现在假设想要编写一个 pushAll 方法,使之与 popAll 方法相呼应 。popAll 方法从堆校中弹出每个元素,并将这些元素添加到指定的集合中 。 初次尝试编写的 popAll 方法可能像下面这样 :

public void popAll(Col1ection<E> dst) {while (!isEmpty())dst.add(pop());
}

        此外,如果目标集合的元素类型与堆栈的完全匹配,这段代码编译时还是会正确无误,并且运行良好 。 但是,也并不意味着尽如人意 。 假设你有一个 Stack<Number >和 Object 类型的变量 。 如果从堆校中弹出 一个元素,并将它保存在该变量中,它的编译和运行都不会出错,那你为何不能也这么做呢?

Stack<Number> numberStack = new Stack<Number>() ;
Collection<Object> objects = ...;
numberStack.popAll(objects) ;

        如果试着用上述 的 popAll 版本编译这段客户端代码,就会得到一个非常类似于第一次用 pushAll 时所得到的错误:Collection<Object >不是 Collection<Number>的子类型 。 这一次通配符类型同样提供了一种解决办法 。popAll 的输入参数类型不应该为“ E 的集合”,而应该为“ E 的某种超类的集合”(这里的超类是确定的,因此 E 是它自身的一个超类型)。 仍有一个通配符类型正符合此意:Collection<? super E > 。 让我们修改 popAll 来使用它:

public void popAll(Collection<? super E> dst) {while (!isEmpty())dst.add(pop();
}

        做了 这个变动之后,Stack 和客户端代码就都可以正确无误地编译了 。

        结论很明显:为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型 。 如果某个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型匹配,这是不用任何通配符而得到的 。

        下面的助记符便于让你记住要使用哪种通配符类型 :

        PECS 表示 producer-extends,consumer-super

        换句话说,如果参数化类型表示一个生产者 T ,就使用<? extends T >;如果它表示一个消 费者 T ,就使用 <? super T > 。 在我们的 Stack 示例中,pushAll 的 src 参数产生 E 实 例供 Stack 使用 ,因 此 src 相 应的类型为 Iterable<? extends E> ; popAll的 dst 参数通过 Stack 消费 E 实例,因此 dst 相应的类型为 Collection<? s uper E > 。PECS 这个助记符突 出了使用通配符类型的基本原则 。Naftalin 和 Wadler 称之为 Get αnd Put Principle。

        如果使用得当,通配符类型对于类的用户来说几乎是无形的 。 它们使方法能够接受它们应该接受的参数,并拒绝那些应该拒绝的参数 。 如果类的 用 户必须考虑通配符类型,类的API 或许就会出错

        一般来说, 如果类型参数只在方法声明中出现一次,就可以用通配符取代它 。 如果是无限制的类型参数,就用无限制的通配符取代它;如果是有限制的类型参数,就用有限制的通配符取代它。

        总而言之,在 API 中使用通配符类型虽然比较需要技巧,但是会使 API 变得灵活得多 。 如果编写 的是将被广泛使用的类库, 则一定要适当地利用通配符类型 。 记住基本的原则:producer-extends,consumer-super(PECS ) 。 还要记住所有的 comparable 和comparator 都是消费者 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/89980.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Rust 编程小技巧摘选(8)

目录 Rust 编程小技巧(8) 1. 取整函数 floor() 2. 取整函数ceil() 3. 取整函数 round() 4. 保留小数位数 5. 字符串转整数 unwrap() unwrap_or() Rust 编程小技巧(8) 1. 取整函数 floor() floor函数对浮点数进行向下取整 示例代码&#xff1a; fn main() {let x: …

【爬虫】爬取旅行评论和评分

以马蜂窝“普达措国家公园”为例&#xff0c;其评论高达3000多条&#xff0c;但这3000多条并非是完全向用户展示的&#xff0c;向用户展示的只有5页&#xff0c;数了一下每页15条评论&#xff0c;也就是75条评论&#xff0c;有点太少了吧&#xff01; 因此想了个办法尽可能多爬…

3.解构赋值

解构赋值是一种快速为变量赋值的简洁语法&#xff0c;本质上仍然是为变量赋值。 3.1数组解构 数组解构是 将数组的单元值快速批量赋值给一系列变量 的简洁语法 1.基本语法: &#xff08;1&#xff09;赋值运算符左侧的[ ]用于批量声明变量&#xff0c;右侧数组的单元值将被赋…

面试热题(最大子数组和)

给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组 是数组中的一个连续部分。 输入&#xff1a;nums [-2,1,-3,4,-1,2,1,-5,4] 输出&#xff1a;6 解释&#xff1a;连续…

SolidUI 一句话生成任何图形,v0.2.0功能介绍

文章目录 背景聊天窗口提示词 聊天窗口生成输入数据格式柱形图曲面图散点图螺旋线饼图兔子建模地图 设计页面页面布局预览 SolidUI社区的未来规划如何成为贡献者加群 背景 随着文本生成图像的语言模型兴起&#xff0c;SolidUI想帮人们快速构建可视化工具&#xff0c;可视化内容…

【数据结构】哈希表

总结自代码随想录 哈希表的原理&#xff1a; 对象通过HashCode()函数会返回一个int值&#xff1b;将int值与HashTable的长度取余&#xff0c;该余数就是该对象在哈希表中的下标。

GCviewer分析java垃圾回收的情况

一&#xff0c;下载并打包 1.在github上下载gcviewer,并解压。 2. 运行maven命令打包。mvn clean package -DskipTests 二&#xff0c;启动GCViewer 进入target目录&#xff0c;运行 java -jar gcviewer-1.37-SNAPSHOT.jar 运行后&#xff0c;会出来界面 三&#xff0c;加参…

docker菜谱大全

记录docker常用软件安装&#xff0c;感谢小马哥和杨师傅的投稿。&#x1f60e;&#x1f60e;&#x1f60e; 相关文档&#xff1a; DockerHub&#xff1a;https://hub.docker.com/Linux手册&#xff1a;https://linuxcool.com/Docker文档&#xff1a;https://docs.docker.com/Do…

【设计模式】原型模式

原型模式&#xff08;Prototype Pattern&#xff09;是用于创建重复的对象&#xff0c;同时又能保证性能。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式之一。 这种模式是实现了一个原型接口&#xff0c;该接口用于创建当前对象的克隆。当直接…

MySQL 45讲笔记(1-10讲)

1. SQL语句如何开始执行&#xff1f; MySQL分为Server和存储引擎两部分&#xff1a; Server层包含连接器、存储缓存、分析器、执行器等&#xff0c;以及所有的内置函数&#xff08;事件、日期&#xff09;等等&#xff0c;还有视图、触发器。 存储引擎是负责数据的存储和提取&a…

Java多款线程池,总有一款适合你。

线程池的选择 一&#xff1a;故事背景二&#xff1a;线程池原理2.1 ThreadPoolExecutor的构造方法的七个参数2.1.1 必须参数2.1.2 可选参数 2.2 ThreadPoolExecutor的策略2.3 线程池主要任务处理流程2.4 ThreadPoolExecutor 如何做到线程复用 三&#xff1a;四种常见线程池3.1 …

POI处理excel,根据XLOOKUP发现部分公式格式不支持问题

poi4不支持XLOOKUP函数&#xff0c;但poi最新的5.2.3却已经对此函数做了支持 poi下载地址&#xff1a;Index of /dist/poi/release/bin 公式源码位置&#xff1a;org/apache/poi/ss/formula/atp/XLookupFunction.java 但是在使用此函数过程中&#xff0c;发现有些XLOOKUP函数会…

网络设备(防火墙、路由器、交换机)日志分析监控

外围网络设备&#xff08;如防火墙、路由器、交换机等&#xff09;是关键组件&#xff0c;因为它们控制进出公司网络的流量。因此&#xff0c;监视这些设备的活动有助于 IT 管理员解决操作问题&#xff0c;并保护网络免受攻击者的攻击。通过收集和分析这些设备的日志来监控这些…

Oracle database Linux自建环境备份至远端服务器自定义保留天数

环境准备 linux下安装oracle 请看 oracle12c单节点部署 系统版本: CentOS 7 软件版本&#xff1a; Oracle12c 备份策略与实现方法 此次备份依赖Oracle自带命令exp与linux下crontab命令&#xff08;定时任务&#xff09; exp Oracle中exp命令是一个用于导出数据库数据和对象的…

虚拟机内搭建CTFd平台搭建及CTF题库部署,局域网内机器可以访问

一、虚拟机环境搭建 1、安装docker、git、docker-compose ubuntu&#xff1a; sudo apt-get update #更新系统 sudo apt-get -y install docker.io #安装docker sudo apt-get -y install git #安装git sudo apt-get -y install python3-pip #安装pip3 sudo pip install dock…

JavaEE初阶:多线程 - 编程

1.认识线程 我们在之前认识了什么是多进程&#xff0c;今天我们来了解线程。 一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行 着多份代码. 引入进程这个概念&#xff0c;主要是为了解决并发编程这样的…

时序预测 | MATLAB实现CNN-BiGRU-Attention时间序列预测

时序预测 | MATLAB实现CNN-BiGRU-Attention时间序列预测 目录 时序预测 | MATLAB实现CNN-BiGRU-Attention时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现CNN-BiGRU-Attention时间序列预测&#xff0c;CNN-BiGRU-Attention结合注意力机制时…

机器学习笔记之优化算法(十二)梯度下降法:凸函数VS强凸函数

机器学习笔记之优化算法——梯度下降法&#xff1a;凸函数VS强凸函数 引言凸函数&#xff1a;凸函数的定义与判定条件凸函数的一阶条件凸函数的梯度单调性凸函数的二阶条件 强凸函数强凸函数的定义强凸函数的判定条件强凸函数的一阶条件强凸函数的梯度单调性强突函数的二阶条件…

【从零学习python 】21.Python中的元组与字典

文章目录 元组一、访问元组二、修改元组三、count, index四、定义只有一个数据的元组五、交换两个变量的值 字典介绍一、列表的缺点二、字典的使用进阶案例 元组 Python的元组与列表类似&#xff0c;不同之处在于元组的元素不能修改。元组使用小括号&#xff0c;列表使用方括号…

C++初阶之一篇文章教会你queue和priority_queue(理解使用和模拟实现)

queue和priority_queue&#xff08;理解使用和模拟实现&#xff09; 什么是queuequeue的使用1.queue构造函数2.empty()3.size()4.front()5.back();6.push7.emplace8.pop()9.swap queue模拟实现什么是priority_queuepriority_queue的使用1.priority_queue构造函数1.1 模板参数 C…