Skip to content

四火的唠叨

一个纯正程序员的啰嗦

Menu
  • 所有文章
  • About Me
  • 关于四火
  • 旅行映像
  • 独立游戏
  • 资源链接
Menu

泛型趣谈

Posted on 01/08/201410/08/2024 by 四火

strange

Java 中的泛型带来了什么好处?规约。就像接口定义一样,可以帮助对于泛型类型和对象的使用上,保证类型的正确性。如果没有泛型的约束,程序员大概需要在代码里面使用大量的类型强制转换语句,而且需要非常清楚没有标注的对象实际类型,这是容易出错的、恼人的。但是话说回来,泛型可不只有规约,还有很多有趣的用法,容我一一道来。

 

泛型擦除

Java 的泛型在编译阶段实际上就已经被擦除了(这也是它和 C#泛型最本质的区别),也就是说,对于使用泛型的定义,对于编译执行的过程,并没有任何的帮助(有谁能告诉我为什么按着泛型擦除来设计?)。所以,单纯利用泛型的不同来设计接口,会遇到预期之外的问题,比如说:

public interface Builder<K,V> {
	public void add(List<K> keyList);
	public void add(List<V> valueList);
}

想这样设计接口?仅仅靠泛型类型的不同来设计重载接口?那是痴人说梦。但是如果代码变成这样呢?

public class GenericTypes {

    public static String method(List<String> list) {
        System.out.println("invoke method(List<String> list)");
        return "";
    }

    public static int method(List<Integer> list) {
        System.out.println("invoke method(List<Integer> list)");
        return 1;
    }

    public static void main(String[] args) {
        method(new ArrayList<String>());
        method(new ArrayList<Integer>());
    }
}

这个情况就有点特殊了,Sun 的 Javac 编译器居然可以通过编译,而其它不行,这个例子来自 IcyFenix 的文章,有兴趣不妨移步参阅 IcyFenix 的文章以及下面的讨论。

 

方法泛型

在 JDK 的 java.util.List 接口里面,定义了这样一个方法:

public interface List<E> extends Collection<E> {
    <T> T[] toArray(T[] a);
}

事实上,这个方法泛型 T 表示的是任意类型,它可是和此例中的接口/类泛型 E 毫不相干啊。

如果我去设计方法,我可能写成这样:

<T> T[] toArray();

其实这个 T[ ] a 参数的作用也容易理解:

  1. 确定了数组类型;
  2. 如果给定的数组 a 能够容纳得下结果,就会把结果放进 a 里面(JDK 的注释有说明“If the list fits in the specified array, it is returned therein.”),同时也把 a 返回。

如果没有这个 T[ ] a 参数的话,光光定义一个方法泛型<T> 是没有任何意义的,因为这个 T 是什么类型完全是无法预料的,例如:

public class Builder {
	public <E> E call(){
		return null;
	}
	
	public static void main(String[] args) {
		String s = new Builder().call(); // ①
		Integer i = new Builder().call(); // ②
		new Builder().<String>call(); // ③
	}
}

可以看到,call() 方法返回的是类型 E,这个 E 其实没有任何约束,它可以表示任何对象,但是代码上不需要强制转换就可以赋给 String 类型的对象 s(①),也可以赋给 Integer 的对象 i(②),甚至,你可以主动告知编译器返回的对象类型(③)。

 

链式调用

看看如下示例代码:

public class Builder<S> {
	public <E> Builder<E> change(S left, E right){
		// 省略实现
	}
	
	public static void main(String[] args) {
		new Builder<String>().change("3", 3).change(3, 3.0f).change(3.0f, 3.0d);
	}
}

同样一个 change 方法,接收的参数变来变去的,上例中方法参数从 String-int 变到 int-float,再变为 float-double,这样的泛型魔法在设计链式调用的方法的时候,特别是定义 DSL 语法的时候特别有用。

 

使用问号 

其实问号帮助表示的是“ 通配符类型”,通配符类型 List<?> 与原始类型 List 和具体类型 List<String> 都不相同,List<?> 表示这个 list 内的每个元素的类型都相同,但是这种类型具体是什么我们却不知道。注意,List<?> 和 List<Object> 可不相同,由于 Object 是最高层的超类,List<Object> 表示元素可以是任何类型的对象,但是 List<?> 可不是这个意思。

来看一段有趣的代码:

class Wrapper<E> {
	private E e;
	public void put(E e) {
		this.e = e;
	}
	
	public E get(){
		return e;
	}
}

public class Builder {
	public void check(Wrapper<?> wrapper){
		System.out.println(wrapper.get()); // ①
		wrapper.put(new Object()); // ② wrong!
		wrapper.put(wrapper.get()); // ③ wrong!
		wrapper.put(null); // ④ right!
	}
}

Wrapper 的类定义里面指定了它包装了一个类型为 E 的对象,但是在另一个使用它的类 Builder 里面,指定了 Wrapper 的泛型参数是?,这就意味着这个被包装的对象的类型是完全不可知的:

  • 现在我可以调用 Wrapper 的 get 方法把对象取出来看看(①),
  • 但是我不能放任意类型确定的对象进去,Object 也不行(②),
  • 即便是从 wrapper 里面 get 出来也不行(编译器太不聪明了是吧?③)
  • 可奇葩的是,放一个 null 是可以被允许的,因为 null 根本就不是任何一个类型的对象(④,注意,不能放 int 这类的原语类型,虽然它不是对象,但因为它会被自动装箱成 Integer,从而变成具体类型,所以是会失败的)。

现在思考一下,如果要表示这个未知对象是某个类的子类,上面代码的 Wrapper 定义不变,但是 check 方法写成:

public void check(Wrapper<? extends String> wrapper){
    wrapper.put(new String());
}

这样呢?

……

依然报错,因为 new String() 确实是 String 的子类(或它自己)的对象,一点都没错,但是它可不见得和同为 String 子类(或它自己)的“?” 属于同一个类型啊!

如果写成这样呢(注意 extends 变成了 super)?

public void check(Wrapper<? super String> wrapper){
    wrapper.put(new String());
}

这次对了,为什么呢?

……

因为 wrapper 要求 put 的参数“?” 必须是 String 的父类(或它自己),而不管这个类型如何变化,它一定是 new String() 的父类(或它自己)啊!

 

泛型递归

啥,泛型还能递归?当然能。而且这也是一种好玩的泛型使用:

class Wrapper<E extends Wrapper<E>> implements Comparable<Wrapper<E>> {

	@Override
	public int compareTo(Wrapper<E> wrapper) {
		return 0;
	}

}

好玩吧?泛型也能递归。这个例子指的是,一个对象 E 由包装器 Wrapper 所包装,但是,E 也必须是一个包装器,这正是包装器的递归;同时,包装器也实现了一个比较接口,使得两个包装器可以互相比较大小。

 

别晕!泛型只不过是一个普普通通的语言特性,但是也挺有趣的。

 

【2014-1-9】补充,来自 kidneyball 的回复:

为什么要按着类型擦除来设计。据我所知,Java1.5 引入泛型的最大压力来自于没有泛型的容器 API 相比起 C++的标准模板库来太难用,太多不必要的显式转型,完全违背了 DRY 原则也缺乏精细的类型检查。但 Java 与 C++不同,C++的对象没有公共父类,不使用泛型根本无法建立一个能存放所有类型的容器,所以必须在费大力气在编译后的运行代码中支持泛型,保留泛型信息自然是顺水推舟。而 Java 所有对象都有一个共同父类 Object,当时已有的容器实现已经在运行期表现良好。所以 Sun 的考虑是加入一层简单的编译期泛型语法糖进行自动转换和类型检查,而在编译后的字节码中则擦除掉泛型信息,仍然走 Object 容器的旧路。这种升级方案对 jdk 的改动是最小的,Runtime 根本不用改,改编译器就行了。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

×Scan to share with WeChat

你可能也喜欢看:

  1. 泛型传递
  2. 运行时动态增加枚举类型
  3. 动手实现随机验证码
  4. 从 DCL 的对象安全发布谈起
  5. 看 JDK 源码,解几个疑问

5 thoughts on “泛型趣谈”

  1. 句号 says:
    05/07/2016 at 9:25 AM

    这个情况就有点特殊了,Sun 的 Javac 编译器居然可以通过编译,而其它不行,这个例子来自 IcyFenix 的文章,有兴趣不妨移步参阅 IcyFenix 的文章以及下面的讨论。

    泛型的重载,在 sun 的编译器、eclipse 编译器下都不行呀。

    Reply
  2. Anonymous says:
    03/24/2016 at 2:47 PM

    java 泛型之所以采用擦除,是因为泛型是在 JDK 1.5 才出现的,为了与以前的类库兼容,Java 语言的设计者们采用了这种在编译期将泛型擦除的妥协方法。个人理解,请多指教。

    Reply
  3. 独酌逸醉 says:
    01/10/2014 at 4:20 PM

    四火哥,博客内容不错,但是阅读体验不好啊。
     
    何不尝试一下 jekyll/Octopress ,搭建静态博客呢?

    Reply
    1. 四火 says:
      01/10/2014 at 8:10 PM

      以前不知道还有这东西,我去看看,或许以后吧。:)

      Reply
  4. Anonymous says:
    01/09/2014 at 2:40 PM

    所以,JAVA 的泛型骑虎难下
    C++是多范式,不需要被强迫使用某种编程范式,这就是其最大的好处,当然,也是坏处之一

    Reply

Leave a Reply to Anonymous Cancel reply

Your email address will not be published. Required fields are marked *

订阅·联系

四火,啰嗦的程序员一枚,现居西雅图

Amazon Google Groovy Hadoop Haskell Java JavaScript LeetCode Oracle Python Spark 互联网 前端 华为 历史 同步 团队 图解笔记 基础设施 工作 工作流 工具 工程师 应用系统 异步 微博 思考 技术 数据库 曼联 测试 生活 程序员 管理 系统设计 缓存 编码 编程范型 英语 西雅图 设计 评审 问题 面试 项目

分类

  • Algorithm and Data Structure (30)
  • Concurrency and Asynchronization (6)
  • System Architecture and Design (43)
  • Distributed System (18)
  • Tools Frameworks and Libs (13)
  • Storage and Data Access (8)
  • Front-end Development (33)
  • Programming Languages and Paradigms (55)
  • Testing and Quality Assurance (4)
  • Network and Communication (6)
  • Authentication and Authorization (6)
  • Automation and Operation Excellence (13)
  • Big Data and Machine Learning (5)
  • Product Design (7)
  • Hiring and Interviews (14)
  • Project and Team Management (14)
  • Engineering Culture (17)
  • Critical Thinking (25)
  • Career Growth (57)
  • Life Experience and Thoughts (45)

推荐文章

  • 谈谈分布式锁
  • 常见分布式系统设计图解(汇总)
  • 系统设计中的快速估算技巧
  • 从链表存在环的问题说起
  • 技术面试中,什么样的问题才是好问题?
  • 从物理时钟到逻辑时钟
  • 近期面试观摩的一些思考
  • RSA 背后的算法
  • 谈谈 Ops(汇总 + 最终篇):工具和实践
  • 不要让业务牵着鼻子走
  • 倔强的程序员
  • 谈谈微信的信息流
  • 评审的艺术——谈谈现实中的代码评审
  • Blog 安全问题小记
  • 求第 K 个数的问题
  • 一些前端框架的比较(下)——Ember.js 和 React
  • 一些前端框架的比较(上)——GWT、AngularJS 和 Backbone.js
  • 工作流系统的设计
  • Spark 的性能调优
  • “残酷” 的事实
  • 七年工作,几个故事
  • 从 Java 和 JavaScript 来学习 Haskell 和 Groovy(汇总)
  • 一道随机数题目的求解
  • 层次
  • Dynamo 的实现技术和去中心化
  • 也谈谈全栈工程师
  • 多重继承的演变
  • 编程范型:工具的选择
  • GWT 初体验
  • java.util.concurrent 并发包诸类概览
  • 从 DCL 的对象安全发布谈起
  • 不同团队的困惑
  • 不适合 Hadoop 解决的问题
  • 留心那些潜在的系统设计问题
  • 再谈大楼扔鸡蛋的问题
  • 几种华丽无比的开发方式
  • 我眼中的工程师文化
  • 观点的碰撞
  • 谈谈盗版软件问题
  • 对几个软件开发传统观点的质疑和反驳
  • MVC 框架的映射和解耦
  • 编程的未来
  • DAO 的演进
  • 致那些自嘲码农的苦逼程序员
  • Java 多线程发展简史
  • 珍爱生命,远离微博
  • 网站性能优化的三重境界
  • OSCache 框架源码解析
  • “ 你不适合做程序员”
  • 画圆画方的故事

近期评论

  • + 1.943624 BTC.NEXT - https://graph.org/Ticket--58146-05-02?hs=9a9c6f8dfe3cdbe0074006e3e640b19b& on 所有文章
  • Anonymous on 闲聊投资:亲自体验和护城河
  • 四火 on 关于近期求职的近况和思考
  • YC on 关于近期求职的近况和思考
  • mafulong on 常见分布式基础设施系统设计图解(四):分布式工作流系统
  • 四火 on 常见分布式基础设施系统设计图解(八):分布式键值存储系统
  • Anonymous on 我裸辞了
  • https://umlcn.com on 资源链接
  • Anonymous on 我裸辞了
  • Dylan on 我裸辞了
© 2025 四火的唠叨 | Powered by Minimalist Blog WordPress Theme