泛型之解析通配符

本贴最后更新于 1869 天前,其中的信息可能已经水流花落

通配符有着令人费解和混淆的语法,但通配符大量应用于 Java 容器类中。

  1. 更简洁的参数类型限定

    在上一篇中,为了将 Integer 对象添加到 Number 容器中,我们的类型参数使用了其他类型参数作为上界,我们提到,这种写法有点繁琐,它可以替换为更为简洁的通配符形式:

    public void addAll(DynamicArray<? extends E> c) {
    	for (int i = 0; i< c.size; i++) {
    		add(c.get(i));
    	}
    }
    

    这个方法没有定义类型参数,c 的类型是 DynamicArray<? extends E>,?表示通配符,<? extends E> 表示有限定通配符,匹配 E 或 E 的某个子类型,具体什么子类型是未知的。

    DynamicArray<Number> numbers = new DynamicArray<>();
    DynamicArray<Integer> ints = new DynamicArray<>();
    ints.add(100);
    ints.add(34);
    numbers.addAll(ints);
    

    这里,E 是 Number 类型,DynamicArray<? extends E> 可以匹配 DynamicArray。

    那么问题来了,同样是 extends 关键字,同样应用于泛型,和 <? extends E> 到底有什么关系?它们用的地方不一样:

    1. 用于定义类型参数,它声明了一个类型参数 T,可放在泛型类定义中类名后面、泛型方法返回值前面。
    2. 用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。

    虽然他们不一样,但两种写法经常可以达成相同目标。

    public void addAll(DynamicArray<? extends E> c)
    public <T extends E> void addAll(DynamicArray<T> c)
    

    那么到底应该用哪种形式?

  2. 理解通配符

    除了有限定通配符,还有一种通配符,形如 DynamicArray<?>,称为无限定通配符。

    public static int indexOf(DynamicArray<?> arr, Object elm) {
    	for (int i = 0; i< arr.size(); i++) {
    		if (arr.get(i).equals(elm)) {
    			return i;
    		}
    	}
    	return -1;
    }
    

    其实这种无限通配符形式也可以改为使用类型参数。

    public static int indexOf(DynamicArray<?> arr, Object elm)
    

    可以改为:

    public static <T> int indexOf(DynamicArray<T> arr, Object elm)
    

    不过,通配符形式更为简洁。虽然通配符形式更为简洁,但上面两种通配符都有一个重要的限制:只能读,不能写。

    DynamicArray<Integer> ints = new DynamicArray<>();
    DynamicArray<? extends Number> numbers = ints;
    Integer a = 200;
    numbers.add(a):	//错误
    numbers.add((Number) a); //错误
    numbers.add((Object) a); //错误
    

    三种 add 方法都是非法的,无论是 Integer,还是 Number 或 Object,编译器都会报错。因为问号就是表示类型安全无知,?extends Number 表示 Number 的某个子类型,但不知道具体子类型,如果允许写入,Java 就无法确保类型安全性,所以干脆禁止。

    DynamicArray<Integer> ints = new DynamicArray<>();
    DynamicArray<? extends Number> numbers = ints;
    Number n = new Double(23.0)
    Object o = new String("hello world");
    numbers.add(n);
    numbers.add(o);
    

    如果允许写入 Object 或 Number 类型,则最后两行编译就是正确的,也就是说,Java 将允许把 Double 或 String 对象放入 Integer 容器,这显然违背了 Java 关于类型安全的承诺。

    大部分情况下,这种限制是好的,但这使得一些理应正确的基本操作无法完成,比如交换两个元素的位置。

    public static void swap(DynamicArray<?> arr, int i, int j) {
    	Object tmp = arr.get(i);
    	arr.set(i, arr.get(j));
    	arr.set(j, tmp);
    } 
    

    这个代码看上去应该是正确的,但 Java 会提示编译错误,两行 set 语句都是非法的。不过,借助待类型参数的方法,这个问题可以如下解决。

    private static <T> void swapInternal(DynamicArray<T> arr, int i, int j) {
    	T tmp = arr.get(i);
    	arr.set(i, arr.get(j));
    	arr.set(j, tmp);
    }
    public static void swap(DynamicArray<?> arr, int i, int j) {
    	swapInternal(arr, i, j);
    }
    

    swap 可以调用 swapInternal,而带类型参数的 swapInternal 可以写入。Java 容器类中就有类似这样的用法,公共的 API 是通配的形式,形式更简单,但内部调用带类型参数的方法。

    除了这种需要写的场合,如果参数类型之间有依赖关系,也只能用类型参数,比如,将 src 容器中的内容复制到 dest 中:

    public static <D, S extends D> void copy(DynamicArray<D> dest, DynamicArray<S> src) {
    	for (int i = 0; i< src.size(); i++) {
    		dest.add(src.get(i));
    	}
    }
    

    S 和 D 有依赖关系,要么相同,要么 S 是 D 的子类,否则类型不兼容,有编译错误。不过上面的声明可以使用通配符简化,两个参数可以简化为一个,如下:

    public static <D> void copy(DynamicArray<D> dest, DynamicArray<? extends D> src) {
    	for (int i = 0; i< src.size(); i++) {
    		dest.add(src.get(i));
    	}
    }
    

    如果返回值依赖于类型参数,也不能用通配符,比如,计算动态数组中的最大值。

    public static <T extends Comparable<T>> T max(DynamicArray arr) {
    	T max = arr.get(0);
    	for(int i = 1; i< arr.size(); i++) {
    		if(arr.get(i).compareTo(max) > 0) {
    			max = arr.get(i);
    		}
    	}
    	return max;
    }
    

    上面的代码就难以用通配符代替。

    那么泛型方法到底应该用通配符的形式还是加类型参数?总结如下:

    1. 通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。
    2. 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以,能用通配符的就用通配符。
    3. 如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。
    4. 通配符形式和类型参数往往配合使用。比如,上面 copy 方法,定义必要的类型参数,使用通配符表达依赖,并解说更广泛的数据类型。
  3. 超类型通配符

    还有一种通配符,与形式 <? extends E> 正好相反,它的形式为 <? super E>,称为超类型通配符,表示 E 的某个父类型。有了它,我们就可以更灵活地写入了。

    如果没有这种语法,写入会有一些限制。

    public void copyTo(DynamicArray<E> dest) {
    	for(int i = 0; i< size; i++) {
    		dest.add(get(i));
    	}
    }
    

    这个方法也很简单,将当前容器中的元素添加到传入的目标容器中。我们可能希望这个使用:

    DynamicArray<Integer> ints = new DynamicArray<Integer>();
    ints.add(100);
    ints.add(34);
    DynamicArray<Number> numbers = new DynamicArray<>();
    ints.copyTo(numbers);
    

    Integer 是 Number 的子类,将 Integer 对象拷贝进 Number 容器,这种用法应该是合情合理的,但 Java 会提示编译错误,理由我们之前也说过,期望的参数类型是 DynamicArray,DynamicArray并不适用。

    如之前所说,一般而言,不能将 DynamicArray看作 DynamicArray,但我们这里的用法没有问题,Java 解决这个问题的方法就是超类型通配符,可以将 copyTo 代码该为:

    public void copyTo(DynamicArray<? super E> dest) {
    	for(int i = 0; i< size; i++) {
    		dest.add(get(i));
    	}
    }
    

    这样就没问题。

    超类型通配符另一个常用的场合是 Comparable/Comparator 接口。同样,如果不适用会有什么限制。

    public static <T extends Comparable<T>> T max(DynamicArray<T> arr)
    

    这个声明有什么限制呢?举个简单的例子,有两个类 Base 和 Child

    class Base implements Comparable<Base> {
    	private int sortOrder;
    	public Base(int sortOrder) {
    		this.sortOrder = sortOrder;
    	}
    
    	@Override
    	public int compareTo(Base o) {
    		if(sortOrder < o.sortOrder) {
    			return -1;
    		} else if(sortOrder > o.sortOrder) {
    			return 1;
    		} else {
    			return 0;
    		}
    	}
    }
    
    class Child extends Base {
        public Child(int sortOrder) {
            super(sortOrder);
        }
    }
    

    这里,Child 非常简单,只是继承了 Base。注意:Child 没有重新实现 Comparable 接口,因为 Child 的比较规则和 Base 是一样的。我们可能希望使用前面的 max 方法操作 Child 容器。

    DynamicArray<Child> childs = new DynamicArray<Child>();
    childs.add(new Child(20));
    childs.add(new Child(80));
    Child maxChild = mac(childs);
    

    遗憾的是,Java 会提示错误,类型不匹配。我们可能会认为,Java 会将 max 方法的类型参数 T 推断为 Child 类型,但类型 T 的要求是 extends Comparable,而 Child 并没有实现 Comparable,它实现的是 Comparable。

    但我们的需求是合理的,Base 类的代码已经有了关于比较所需要的全部数据,它应该可以用于比较 Child 对象。解决这个问题的方法就是修改 max 方法声明,使用超类型通配符。

    public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr)
    

    这么修改一下就可以了,这种写法比较抽象,将 T 替换为 Child,就是:

    Child extends Comparable<? super Child>
    

    类型参数限定只有 extends 形式,没有 super 形式,比如前面的 copyTo 方法的通配符形式的声明:

    public void copyTo(DynamicArray<? super E> dest)
    

    如果类型参数限定支持 super 形式,则应该是:

    public <T super E> void copyTo(DynamicArray<T> dest)
    

    事实是,Java 并不支持这种语法。

    对于有限定的通配符形式 <? extends E>,可以用类型参数限定替代,但是对于类似上面的超类型通配符,则无法用类型参数替代。

    泛型的三种通配符形式 <?>、<? super E> 和 <? extends E>,并分析了与类型参数形式的区别和联系,他们都容易混淆:

    1. 他们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型。
    2. 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。
    3. 和用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象,他们可以用类型参数的形式替代,但通配符形式更为简洁。

    在使用泛型类、方法和接口时,有一些值得注意的地方:

    • 基本类型不能用于实例化类型参数
    • 运行时类型信息不适用于泛型
    • 类型擦除可能会引发一些冲突

    在定义泛型类、方法和接口时,也有一些需要注意的地方:

    • 不能通过类型参数创建对象
    • 泛型类类型参数不能用于静态变量和方法
    • 了解多个类型限定的语法

    泛型与数组的关系:

    • Java 不支持创建泛型数组
    • 如果要存放泛型对象,可以使用原始类型的数组,或者使用泛型容器
    • 泛型容器内部使用 Object 数组,如果要转换泛型容器为对应类型的数组,需要使用反射。

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
AutisticV5
简单很快乐,快乐很简单。 佛山

推荐标签 标签

  • Sym

    Sym 是一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)系统平台。

    下一代的社区系统,为未来而构建

    524 引用 • 4601 回帖 • 700 关注
  • FlowUs

    FlowUs.息流 个人及团队的新一代生产力工具。

    让复杂的信息管理更轻松、自由、充满创意。

    1 引用 • 1 关注
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    15 引用 • 7 回帖 • 1 关注
  • 快应用

    快应用 是基于手机硬件平台的新型应用形态;标准是由主流手机厂商组成的快应用联盟联合制定;快应用标准的诞生将在研发接口、能力接入、开发者服务等层面建设标准平台;以平台化的生态模式对个人开发者和企业开发者全品类开放。

    15 引用 • 127 回帖 • 2 关注
  • Kafka

    Kafka 是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是现代系统中许多功能的基础。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。

    36 引用 • 35 回帖 • 3 关注
  • 安全

    安全永远都不是一个小问题。

    200 引用 • 816 回帖 • 1 关注
  • 宕机

    宕机,多指一些网站、游戏、网络应用等服务器一种区别于正常运行的状态,也叫“Down 机”、“当机”或“死机”。宕机状态不仅仅是指服务器“挂掉了”、“死机了”状态,也包括服务器假死、停用、关闭等一些原因而导致出现的不能够正常运行的状态。

    13 引用 • 82 回帖 • 59 关注
  • Sandbox

    如果帖子标签含有 Sandbox ,则该帖子会被视为“测试帖”,主要用于测试社区功能,排查 bug 等,该标签下内容不定期进行清理。

    410 引用 • 1246 回帖 • 587 关注
  • 单点登录

    单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    9 引用 • 25 回帖
  • Q&A

    提问之前请先看《提问的智慧》,好的问题比好的答案更有价值。

    8496 引用 • 38707 回帖 • 153 关注
  • 旅游

    希望你我能在旅途中找到人生的下一站。

    93 引用 • 899 回帖 • 1 关注
  • jsDelivr

    jsDelivr 是一个开源的 CDN 服务,可为 npm 包、GitHub 仓库提供免费、快速并且可靠的全球 CDN 加速服务。

    5 引用 • 31 回帖 • 72 关注
  • Logseq

    Logseq 是一个隐私优先、开源的知识库工具。

    Logseq is a joyful, open-source outliner that works on top of local plain-text Markdown and Org-mode files. Use it to write, organize and share your thoughts, keep your to-do list, and build your own digital garden.

    6 引用 • 63 回帖 • 4 关注
  • 运维

    互联网运维工作,以服务为中心,以稳定、安全、高效为三个基本点,确保公司的互联网业务能够 7×24 小时为用户提供高质量的服务。

    149 引用 • 257 回帖 • 1 关注
  • Markdown

    Markdown 是一种轻量级标记语言,用户可使用纯文本编辑器来排版文档,最终通过 Markdown 引擎将文档转换为所需格式(比如 HTML、PDF 等)。

    167 引用 • 1520 回帖
  • Chrome

    Chrome 又称 Google 浏览器,是一个由谷歌公司开发的网页浏览器。该浏览器是基于其他开源软件所编写,包括 WebKit,目标是提升稳定性、速度和安全性,并创造出简单且有效率的使用者界面。

    62 引用 • 289 回帖
  • Hexo

    Hexo 是一款快速、简洁且高效的博客框架,使用 Node.js 编写。

    21 引用 • 140 回帖 • 3 关注
  • 百度

    百度(Nasdaq:BIDU)是全球最大的中文搜索引擎、最大的中文网站。2000 年 1 月由李彦宏创立于北京中关村,致力于向人们提供“简单,可依赖”的信息获取方式。“百度”二字源于中国宋朝词人辛弃疾的《青玉案·元夕》词句“众里寻他千百度”,象征着百度对中文信息检索技术的执著追求。

    63 引用 • 785 回帖 • 162 关注
  • SpaceVim

    SpaceVim 是一个社区驱动的模块化 vim/neovim 配置集合,以模块的方式组织管理插件以
    及相关配置,为不同的语言开发量身定制了相关的开发模块,该模块提供代码自动补全,
    语法检查、格式化、调试、REPL 等特性。用户仅需载入相关语言的模块即可得到一个开箱
    即用的 Vim-IDE。

    3 引用 • 31 回帖 • 104 关注
  • WebSocket

    WebSocket 是 HTML5 中定义的一种新协议,它实现了浏览器与服务器之间的全双工通信(full-duplex)。

    48 引用 • 206 回帖 • 321 关注
  • NetBeans

    NetBeans 是一个始于 1997 年的 Xelfi 计划,本身是捷克布拉格查理大学的数学及物理学院的学生计划。此计划延伸而成立了一家公司进而发展这个商用版本的 NetBeans IDE,直到 1999 年 Sun 买下此公司。Sun 于次年(2000 年)六月将 NetBeans IDE 开源,直到现在 NetBeans 的社群依然持续增长。

    78 引用 • 102 回帖 • 685 关注
  • ActiveMQ

    ActiveMQ 是 Apache 旗下的一款开源消息总线系统,它完整实现了 JMS 规范,是一个企业级的消息中间件。

    19 引用 • 13 回帖 • 670 关注
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    91 引用 • 585 回帖
  • 周末

    星期六到星期天晚,实行五天工作制后,指每周的最后两天。再过几年可能就是三天了。

    14 引用 • 297 回帖
  • QQ

    1999 年 2 月腾讯正式推出“腾讯 QQ”,在线用户由 1999 年的 2 人(马化腾和张志东)到现在已经发展到上亿用户了,在线人数超过一亿,是目前使用最广泛的聊天软件之一。

    45 引用 • 557 回帖 • 42 关注
  • Firefox

    Mozilla Firefox 中文俗称“火狐”(正式缩写为 Fx 或 fx,非正式缩写为 FF),是一个开源的网页浏览器,使用 Gecko 排版引擎,支持多种操作系统,如 Windows、OSX 及 Linux 等。

    8 引用 • 30 回帖 • 410 关注
  • 工具

    子曰:“工欲善其事,必先利其器。”

    288 引用 • 734 回帖