泛型趣谈

2017 年 12 月 30 日 ImportNew

(点击上方公众号,可快速关注)


来源:四火的唠叨 ,

www.raychase.net/1932


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的文章以及下面的讨论


http://icyfenix.iteye.com/blog/1021949


方法泛型


在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根本不用改,改编译器就行了。


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

登录查看更多
0

相关内容

Java 是一门编程语言,拥有跨平台、面向对象、泛型编程等特性。
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
58+阅读 · 2020年6月26日
专知会员服务
32+阅读 · 2020年5月20日
干净的数据:数据清洗入门与实践,204页pdf
专知会员服务
162+阅读 · 2020年5月14日
机器学习速查手册,135页pdf
专知会员服务
342+阅读 · 2020年3月15日
人工智能学习笔记,247页pdf
专知会员服务
185+阅读 · 2019年12月14日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
23+阅读 · 2019年11月7日
Kali Linux 渗透测试:密码攻击
计算机与网络安全
16+阅读 · 2019年5月13日
已删除
创业邦杂志
5+阅读 · 2019年3月27日
Python3.8新特性概览
Python程序员
4+阅读 · 2018年12月8日
快乐的迁移到 Python3
Python程序员
5+阅读 · 2018年3月25日
为什么你应该学 Python ?
计算机与网络安全
4+阅读 · 2018年3月24日
设计和实现一款轻量级的爬虫框架
架构文摘
13+阅读 · 2018年1月17日
Tensor Flow、Caffe、Torch共同之处:敞开的漏洞!
PyTorch 到底好用在哪里?
AI研习社
3+阅读 · 2017年10月27日
Attend More Times for Image Captioning
Arxiv
6+阅读 · 2018年12月8日
Large-Scale Study of Curiosity-Driven Learning
Arxiv
8+阅读 · 2018年8月13日
Arxiv
11+阅读 · 2018年7月8日
Arxiv
11+阅读 · 2018年5月13日
VIP会员
相关VIP内容
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
58+阅读 · 2020年6月26日
专知会员服务
32+阅读 · 2020年5月20日
干净的数据:数据清洗入门与实践,204页pdf
专知会员服务
162+阅读 · 2020年5月14日
机器学习速查手册,135页pdf
专知会员服务
342+阅读 · 2020年3月15日
人工智能学习笔记,247页pdf
专知会员服务
185+阅读 · 2019年12月14日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
23+阅读 · 2019年11月7日
相关资讯
Kali Linux 渗透测试:密码攻击
计算机与网络安全
16+阅读 · 2019年5月13日
已删除
创业邦杂志
5+阅读 · 2019年3月27日
Python3.8新特性概览
Python程序员
4+阅读 · 2018年12月8日
快乐的迁移到 Python3
Python程序员
5+阅读 · 2018年3月25日
为什么你应该学 Python ?
计算机与网络安全
4+阅读 · 2018年3月24日
设计和实现一款轻量级的爬虫框架
架构文摘
13+阅读 · 2018年1月17日
Tensor Flow、Caffe、Torch共同之处:敞开的漏洞!
PyTorch 到底好用在哪里?
AI研习社
3+阅读 · 2017年10月27日
Top
微信扫码咨询专知VIP会员