Java泛型的学习和使用

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

Java泛型的学习和使用_第1张图片

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

1. 什么是泛型?

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

Java泛型的学习和使用_第2张图片
泛型

2. 为什么需要泛型?

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

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

Java泛型的学习和使用_第3张图片
强制转换
Java泛型的学习和使用_第4张图片
类型转换失败

3. 泛型的擦除

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

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

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

Java泛型的学习和使用_第5张图片
反射

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

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

Java泛型的学习和使用_第6张图片

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


Java泛型的学习和使用_第7张图片
源码
Java泛型的学习和使用_第8张图片
javap -c 命令

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

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

Java泛型的学习和使用_第9张图片
ArrayList--get方法
Java泛型的学习和使用_第10张图片
ArrayList--get方法

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

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

Java泛型的学习和使用_第11张图片

运行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);

Java泛型的学习和使用_第12张图片

4. 泛型擦除带来的问题

4.1 类型信息的丢失

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

Java泛型的学习和使用_第13张图片
编译报错

4.2 类型擦除与多态

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

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

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

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

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

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

Java泛型的学习和使用_第14张图片
父类Test


Java泛型的学习和使用_第15张图片
子类TestChild

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

Java泛型的学习和使用_第16张图片
编译后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属于重载,那么就用代码实现下即可:

Java泛型的学习和使用_第17张图片
测试重载

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

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

Java泛型的学习和使用_第18张图片
意淫下的父类

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

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

Java泛型的学习和使用_第19张图片
子类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个这样的方法,是无法通过编译器的检查的!

Java泛型的学习和使用_第20张图片
同名方法

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

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

4.3 异常和泛型擦除

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

编译报错

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

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

Java泛型的学习和使用_第21张图片
捕获异常

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

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

Java泛型的学习和使用_第22张图片
捕获异常

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


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

5.1 泛型接口

Java泛型的学习和使用_第23张图片
泛型接口


Java泛型的学习和使用_第24张图片
泛型接口

5.2 泛型类

Java泛型的学习和使用_第25张图片
泛型类

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

Java泛型的学习和使用_第26张图片
静态修饰成员变量

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

5.3 泛型方法

Java泛型的学习和使用_第27张图片
泛型方法

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

6. 通配符与上下界

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

无限定通配符,形式:

上边界通配符,形式:

下边界通配符,形式:

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

Java泛型的学习和使用_第28张图片
通配符的运用

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

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

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

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

你可能感兴趣的:(Java泛型的学习和使用)