Java泛型的学习和使用

前面,由于对泛型擦除的思考,引出了对Java-Type体系的学习。本篇,就让我们继续对“泛型”进行研究:

JDK1.5中引入了对Java语言的多种扩展,泛型(generics)即其中之一。

1. 什么是泛型?

泛型,即“参数化类型”,就跟在方法或构造函数中普通的参数一样,当一个方法被调用时,实参替换形参,方法体被执行。当一个泛型声明被调用,实际类型参数取代形式类型参数。

泛型

2. 为什么需要泛型?

对于Java开发者来说,集合是泛型运用最多的地方,例如:List<String>、Map<String,Integer>;试想一下,如若没有泛型泛型,当我们对集合进行遍历、进行元素获取的时候,一坨坨强制类型转换的代码就足以让人发疯,而且极易出现类型转换失败的风险;

但是,泛型的出现解决了这个问题,它不但简化了代码,还提高了程序的安全性;类型转换的错误提前到编译期解决掉;

强制转换
类型转换失败

3. 泛型的擦除

JDK1.5版本推出了泛型机制,在此之前,Java语言中并没有泛型的概念;当新特性来到的时候,必然会引起新老代码兼容性的问题,泛型也不例外。Java为解决兼容性问题,采用了擦除机制;

当我们声明并使用泛型的时候,编译器会帮助我们进行类型的检查和推断,然而在代码完成编译后的Class文件中,泛型信息却不复存在了,JVM在运行期间对泛型无感知,这样新老代码的兼容性迎刃而解,这也就是Java泛型的擦除;

在方法中,我们定义了List<String>、Map<String,Integer>等对象,在编译结束之后,都会变成List、Map等原始类型;对于JVM来说,泛型的信息是不可见的;下面,我们通过反射,来观察下!

反射

在程序运行期间,泛型的约束并不存在,通过反射,可以向集合中添加任意类型对象;

此外,当我们通过反编译工具查看GenericTest.class文件的时候,发现ArrayList对象中的泛型没有了,这也间接证明了泛型的擦除;

接下来,我们在通过javap命令查看生成的Class文件:


源码
javap -c 命令

结果显示,当我们执行集合的add方法的时候,泛型类型String已经被擦除,取而代之的是Object类型;当我们执行get方法的时候,泛型同样不存在,也是被当做Object来返回;

可是,我有个疑问,在编译期由于泛型的存在,我们不需要显式的进行类型转换,但是在运行期间是如何解决的呢,难道不会报错吗?

ArrayList--get方法
ArrayList--get方法

查看源码发现,ArrayList在get方法中,已经显式进行了类型转换;

自定义一个泛型类,在get方法中不进行类型转换的声明,看看结果如何?

运行main方法后,程序没有报错,正常结束;

通过上面的2个例子,我们不仅产生疑问,ArrayList中声明了类型转换,Test中没有声明,但是两者在运行期间都没有报错?那么ArrayList的声明意义何在呢 ?

当再次查看ArrayList源码时发现,elementData对象实际上是一个Object类型数组,当我们获取元素并返回的时候,编译器会根据方法的返回值进行类型安全检查,所以 return (E) elementData[index]才会有强制类型转换的情况;

通过了解checkcast指令后,结合上面的2个例子,我认为JVM虚拟机在真正执行get方法的时候,实际上隐式的为我们的代码进行了类型转换操作,就好比在代码中直接声明String ss = (String)test.getT()、String sss = (String)list.get(0)一样;

实际上,在了解到checkcast虚拟机指令后,再次证明了上面的观点;

checkcast:“检验类型转换,检验未通过将抛出ClassCastException”;

官方解释:checkcast checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type. For example, if you write in Java:return ((String)obj);

4. 泛型擦除带来的问题

4.1 类型信息的丢失

由于泛型擦除机制的存在,在运行期间无法获取关于泛型参数类型的任何信息,自然也就无法对类型信息进行操作;例如:instanceof 、创建对象等;

编译报错

4.2 类型擦除与多态

首先,我们先复习下多态的概念,多态出现的场景;

简明直译,多态多态,多种形态;接口下众多的实现类,便是多态最显著实现场景之一;

其次,还有方法的重写Overriding和重载Overloading;

重写Overriding是父类与子类之间多态性的一种表现,如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写(Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了。

重载Overloading是一个类中多态性的一种表现,如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。Overloaded的方法是可以改变返回值的类型但同时参数列表也得不同。

接下来,让我们看一个例子,来具体的分析;

父类Test


子类TestChild

由于泛型擦除的存在,在程序运行期间,Test类在JVM虚拟机中实际的形态如下:

编译后Test类

泛型被擦除,泛型变量替换为Object对象;接下来,我们在看看子类TestChild代码----setT:

@Override

public void setT(String s) {}

首先,来看看set方法,实际运行期间父类Test的set方法参数为Object,子类的为String;回顾下Override
的定义,“如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写(Overriding)”;显然,在运行期间我们子类和父类的set方法只有相同的名称,并没有相同的参数,所以并不满足“重写”的定义;

在看下,重载的定义,“如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)”。既然不是重写,并且Test 和 TestChild又是子父类关系,那么set方法从定义上来看只有可能是重载的关系;子类继承父类方法,在TestChild中形成重载:setT(Object t)、setT(String t);

既然我们推断是setT属于重载,那么就用代码实现下即可:

测试重载

很不幸,编译报错,在子类中并没有一个叫做setT(Object t)的方法,重载不成立,子类的方法依旧和父类属于重写关系;下面,让我来进一步去分析:

子类TestChild继承了父类Test,并传入泛型变量String,如果忽略泛型擦除的存在,父类Test代码应该变成这样:

意淫下的父类

但实际上,Java在编译期已经将泛型变量擦除,运行期间泛型变量变成了Object,没有任何关于泛型String的信息;我们本意是实现方法的重写,但实际上变成了重载(意淫下的重载);这下可如何是好?

于是,JVM虚拟机采用了一个特殊的方式来解决擦除和多态之间的矛盾,桥方法由此诞生;我们继续使用javap -c 命令查看class文件;

子类TestChild

截图中,子类TestChild实际上生成了4个方法,最下面的2个方法,就是JVM所生成的桥方法,而真正实现方法重写的便是这个桥方法------------setT(Object t),而我们自己定义的@Oveerride注解只不过为了满足编译期的要求所存在的假象而已;

这样一来,虚拟机便解决了泛型擦书和多态之间的矛盾;那么,get()是否存在上面重写的问题呢?

答案是NONONO!由于重写(Overriding)只针对于方法名和方法参数,并不没有强调返回值的异同。所以子类---public String getT()父类---public Object getT() 是可以形成重写的关系!

但是,在编译之后的class文件中,由于桥方法的存在,子类中有了2个getT()方法,分别为public String getT()、public Object getT(),如果在我们实际定义方法的时候,在一个类中出现2个这样的方法,是无法通过编译器的检查的!

同名方法

因为以上2个方法,违背了重载的定义,重名方法必须要有不同的形参,否则编译器会报错!

但实际上由于桥方法是在编译后的class文件中生成,所以我们认为虚拟机是允许这样的情况出现,JVM虚拟机认定方法唯一的方式,不单通过方法名称和参数,还包括了方法的返回值;

4.3 异常和泛型擦除

自定义异常类,还必须是带有泛型的异常类;

编译报错

自定义的泛型类并不能继承exception,为什么?

归根到底,还是由于泛型擦除的存在!如果上面编译通过,那么我们在代码中将会看到如下情形:

捕获异常

由于泛型擦除的存在,GenericException在编译之后将不存在泛型信息,2次catch的异常将会变成一样,这在Java中是不允许存在的;

此外,还有一种情况,看如下代码:

捕获异常

由于泛型擦除的存在,T泛型变量在编译之后将会变成Exception类型(由于extends的存在,此处不会变成Object);根据Java中关于捕捉异常的规则:子类异常必须在最前面,以此往后捕捉父类异常;所以说,以上代码违背了Java异常规范,禁止在catch中使用泛型!


5. 自定义泛型接口、泛型类和泛型方法

5.1 泛型接口

泛型接口


泛型接口

5.2 泛型类

泛型类

值得注意的是,在泛型类中,成员变量不能使用静态修饰,编译报错!

静态修饰成员变量

由于是静态变量,不需要创建对象即可调用,无法确定泛型是哪种类型,所以编译禁止通过!当然,需要区分5.3章节中的情况:

5.3 泛型方法

泛型方法

在泛型方法中,自己定义的泛型变量,与类无关;

6. 通配符与上下界

在我们实际工作中,常见的通配符有3类:

无限定通配符,形式:<?>

上边界通配符,形式:<? extends Number>

下边界通配符,形式:<? super Number>

泛型的通配符?与我们平常所定义的T 、K、V等泛型变量功能类似,但是通配符?只能使用在已声明过泛型的类中,不能直接定义在类上,方法上,属性上;

通配符的运用

List<?> list代表着,可以向List中存入任何类型的对象,此时的?可以理解为Object;

那么,上边界和下边界又是什么意思呢?

<? extends Number>代表着所传入的类型参数只能为Number的子类,这就是通配符的上边界;

<? super Number>代表着所传入的类型参数只能为Number、Number的父类,这就是通配符的下边界;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • object 变量可指向任何类的实例,这让你能够创建可对任何数据类型进程处理的类。然而,这种方法存在几个严重的问题...
    CarlDonitz阅读 910评论 0 5
  • 泛型是Java 1.5引入的新特性。泛型的本质是参数化类型,这种参数类型可以用在类、变量、接口和方法的创建中,分别...
    何时不晚阅读 3,028评论 0 2
  • 在之前的文章中分析过了多态,可以知道多态本身是一种泛化机制,它通过基类或者接口来设计,使程序拥有一定的灵活性,但是...
    _小二_阅读 679评论 0 0
  • 多年后,当我坐在出租屋的桌前,夜半歌声,悠扬而轻快,执笔逐字逐句回忆着过去,仿佛那些樱花与柳絮就飞舞在眼前,音乐的...
    5a23edd886c0阅读 494评论 1 2
  • 书名:《火花》 作者:又吉直树 这是一部小说又是一部“自传”。故事站在主人公——漫才组合Sparks的德永——的角...
    鞠茜阅读 311评论 0 0