前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >泛型趣谈

泛型趣谈

作者头像
四火
发布2022-07-18 13:55:31
2370
发布2022-07-18 13:55:31
举报
文章被收录于专栏:四火的唠叨

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

泛型擦除

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

代码语言:javascript
复制
public interface Builder<K,V> {
	public void add(List<K> keyList);
	public void add(List<V> valueList);
}

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

代码语言:javascript
复制
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 接口里面,定义了这样一个方法:

代码语言:javascript
复制
public interface List<E> extends Collection<E> {
    <T> T[] toArray(T[] a);
}

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

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

代码语言:javascript
复制
<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 是什么类型完全是无法预料的,例如:

代码语言:javascript
复制
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(); // ③
代码语言:javascript
复制
	}
}

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

链式调用

看看如下示例代码:

代码语言:javascript
复制
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<?> 可不是这个意思。

来看一段有趣的代码:

代码语言:javascript
复制
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 方法写成:

代码语言:javascript
复制
public void check(Wrapper<? extends String> wrapper){
    wrapper.put(new String());
}

这样呢?

……

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

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

代码语言:javascript
复制
public void check(Wrapper<? super String> wrapper){
    wrapper.put(new String());
}

这次对了,为什么呢?

……

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

泛型递归

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

代码语言:javascript
复制
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 根本不用改,改编译器就行了。

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档