前面,由于对泛型擦除的思考,引出了对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的父类,这就是通配符的下边界;
网友评论
Class类的getMethod()方法注释如下:
返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。name 参数是一个 String,用于指定所需方法的简称。parameterTypes 参数是按声明顺序标识该方法形参类型的 Class 对象的一个数组。如果 parameterTypes 为 null,则按空数组处理。
如果 name 是 "<init>;" 或 "<clinit>",则将引发 NoSuchMethodException。否则,要反映的方法由下面的算法确定(设 C 为此对象所表示的类):
1.在 C 中搜索任一匹配的方法。如果找不到匹配的方法,则将在 C 的超类上递归调用第 1 步算法。
2. 如果在第 1 步中没有找到任何方法,则在 C 的超接口中搜索匹配的方法。如果找到了这样的方法,则反映该方法。
在 C 类中查找匹配的方法:如果 C 正好声明了一个具有指定名称的公共方法并且恰恰有相同的形参类型,则它就是反映的方法。如果在 C 中找到了多个这样的方法,并且其中有一个方法的返回类型比其他方法的返回类型都特殊,则反映该方法;否则将从中任选一个方法。
注意,类中可以有多个匹配方法,因为尽管 Java 语言禁止类声明带有相同签名但不同返回类型的多个方法,但 Java 虚拟机并不禁止。这增加了虚拟机的灵活性,可以用来实现各种语言特性。例如,可以使用桥方法 (brige method)实现协变返回;桥方法以及将被重写的方法将具有相同的签名,不同的返回类型。
总结:
虚拟机与java 语法对于 重载的 认定有细微差别,虚拟机不禁止 具有相同签名和参数列表但是不同返回值的 方法存在,应用场景就是 桥方法。
其他:真佩服楼主的认真,细节,和思考能力,作为同是做这行的,很惭愧,写不出楼主的这么高质量的东西,再次感谢