泛型趣谈

原文出处: 四火的唠叨

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

泛型擦除

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


1

2

3

4

public interface Builder<K,V> {

    public void add(List<K> keyList);

    public void add(List<V> valueList);

}

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


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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接口里面,定义了这样一个方法:


1

2

3

public interface List<E> extends Collection<E> {

    <T> T[] toArray(T[] a);

}

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

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


1

<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是什么类型完全是无法预料的,例如:


1

2

3

4

5

6

7

8

9

10

11

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(②),甚至,你可以主动告知编译器返回的对象类型(③)。

链式调用

看看如下示例代码:


1

2

3

4

5

6

7

8

9

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<?>可不是这个意思。

来看一段有趣的代码:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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方法写成:


1

2

3

public void check(Wrapper<? extends String> wrapper){

    wrapper.put(new String());

}

这样呢?

……

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

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


1

2

3

public void check(Wrapper<? super String> wrapper){

    wrapper.put(new String());

}

这次对了,为什么呢?

……

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

泛型递归

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


1

2

3

4

5

6

7

8

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

    @Override

    public int compareTo(Wrapper<E> wrapper) {

        return 0;

    }

}

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

时间: 2024-11-08 19:19:16

泛型趣谈的相关文章

趣谈并发2:认识并发编程的利与弊

读完本文你将了解: 多线程的优点 1提高资源利用率 2响应更快 多线程的缺点 1增加资源消耗 2上下文切换的开销 3设计编码测试的复杂度增加 Java 内存模型与 CPU 内存简介 Java 中的堆 Java 中的栈 计算机中的内存寄存器缓存 多线程可能出现的问题 竞态条件与临界区 内存可见性 总结 Thanks 从上篇文章 趣谈并发(1):全面认识 Thread 我们了解了 Java 中线程的基本概念和关键方法. 在开始使用线程之前,我觉得我们有必要先了解下多线程给我们带来的好处与可能造成的损

趣谈 32 种设计模式

32种设计模式趣谈 好东西不得不转 在网上看见了这篇文章,作者以轻松的语言比喻了java的32种模式,有很好的启发作用. 创建型模式 1.FACTORY-追MM少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西,虽然口味有所不同,但不管你带MM去麦当劳或肯德基,只管向服务员说"来四个鸡翅"就行了.麦当劳和肯德基就是生产鸡翅的Factory 工厂模式:客户类和工厂类分开.消费者任何时候需要某种产品,只需向工厂请求即可.消费者无须修改就可以接纳新产品.缺点是当产品修改时,工厂类也

ASP.NET应用程序生命周期趣谈(三) HttpModule

在之前的文章中,我们提到过P_Module(HttpModule)这个能干的程序员哥们儿,它通过在项目经理HttpApplication那里得到的授权,插手整个应用程序级别的事件处理.所有的HttpModule都要实现IHttpModule接口,那么我们看IHttpModule的定义: namespace System.Web { public interface IHttpModule { void Dispose(); void Init(HttpApplication context);

傅里叶变换趣谈

转载自:傅里叶分析之掐死教程(完整版)   https://zhuanlan.zhihu.com/p/19763358 作者:Heinrich链接:https://zhuanlan.zhihu.com/p/19763358来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 谨以此文献给大连海事大学的吴楠老师,柳晓鸣老师,王新年老师以及张晶泊老师. 转载的同学请保留上面这句话,谢谢.如果还能保留文章来源就更感激不尽了. ——更新于2014.6.6,想直接看更新的同学可以

趣谈云计算:孙悟空脚踏七彩云,靠的就是云计算

?? 互联网上有这么一个段子:这日,菩提老祖将悟空唤至身前:"你已学会长生不老术和七十二变,今日为师欲传授你新的法术." 悟空道:"是何法术?" 菩提老祖道:"看到这天上的云彩了吗?这边有七朵云彩,那边有五朵云彩,一共有几朵?" 悟空答:"十二朵." 菩提老祖道:"嗯,我要教你的就是云计算." 大部分的中国人都喜欢齐天大圣孙悟空,他的身上拥有了我们很多人想拥有的个性,机灵可爱,牛逼轰轰,可以打败许许多多的妖

牛刀小试 - 趣谈Java中的异常处理

概述 顾名思义,通俗来讲异常就是指,那些发生在我们原本考虑和设定的计划之外的意外情况. 生活中总是会存在各种突发情况,如果没有做好准备,就让人措手不及. 你和朋友约好了明天一起去登山,半道上忽然乌云蔽日,下起了磅礴大雨.这就是所谓的异常情况. 你一下子傻眼了,然后看见朋友淡定的从背包里掏出一件雨衣穿上,淫笑着看着你.这就是对异常的处理. 对于一个OO程序猿来讲,所做的工作就是:将需要处理的现实生活中的复杂问题,抽象出来编写成为程序. 既然现实生活中总是存在着各种突然的异常情况,那么对应其抽象出的

趣谈编程史第3期-人生苦短,不如Python

这是我制作的编程语言科普系列视频的第三期,博客根据视频文案整理而成,提供给有需要的朋友阅读或使用. 视频地址:https://www.bilibili.com/video/av86031488/  如果感兴趣可以观看视频,感谢博友 在编程语言这个竞争激烈 人才辈出的江湖 时常上演勾心斗角 尔虞我诈的狗血故事 各家编程语言的’言粉’ 也时常骂来骂去 尊己卑人 在这个鱼龙混杂的环境里 有这么一号人物 他出身寒门 名不见经传 养在深闺人未识 最初只是在不知名圈子里小有名气 但是因为天生丽质 惹人怜爱

uboot初体验-----趣谈nand设备发起的浅显理解

1 选择Uboot版本号 2 移植uboot至console正常work 3 制造uImage和使用uboot指南 4 写NFC驱动器 5 uboot从nand启动引导系统 1 选择Uboot版本号 正所谓"工欲善其事,必先利其器".假设在整个过程中可以有一套友好的软硬件开发环境整个过程就比較顺利了. 戳中痛点-- 对于选择Uboot的版本号.一般人都会选择最新版本号.可是新版本号必定会有些结构上的差异.因此在选择好版本号之后,一定要细致分析一下如今的uboot  tree.然后选择合

趣谈函数调用与返回值

今晚看老毕的视频时看到了函数的那块,视频中老毕写了一个算术方法,然后通过主函数调用它,方法得到参数后得到返回值再传给主函数.在提到标示返回值类型时,老毕说了一个很生动形象的例子 首先,来谈谈定义,当函数运算后,没有具体返回值时,这时返回值类型用一个特殊的关键字来标示,该关键字是void.当函数中的返回值类型是void时,函数中的return语句可以省略不写.好了,接下来谈谈老毕的那个形象的例子-----假设老毕是方法,同学是主函数,同学要买盒饭就要调用老毕,先给他钱,也就是“传参”,然后老毕返回