您的位置:首页 > 编程语言 > Java开发

详解Java泛型(三)之类型擦除的问题

2016-12-10 22:37 330 查看

1. 概述

类型擦除后,会带来很多幺蛾子,有些限制不得不提一下,下面就来看看有哪些限制,为什么会有这些限制。

2. 不能用基本类型实例化类型参数

在传递类型变量的时候不能传一个基本类型,如
ArrayList<int> list = new ArrayList<int>();
这句代码是有错的,根据我们上面说的类型擦除可知ArrayList只能存储Object类型的数据,而Object与int类型并不能直接转换,下面的代码之所以能编译通过是因为自动装箱。
Object o = (int)a;
这句代码在反编译后就是
Object o = Integer.valueOf(a);


public class Main {

public static void main(String[] args) {
int a = 1;
Object o = (int)a;
}
}


所以可以使用包装类型来替代基本类型实例化类型参数,如
ArrayList<Integer> list = new ArrayList<Integer>();


3. 运行时类型查询只适用于原始类型

由于编译的时候就把类型擦除了,所以在运行时并不存在泛型一说,只有相应的原始类型,如
Pair<T>
在编译后就变成了
Pair
,所以在查询类型的时候只有原始类型。例如下面这个代码

public class Main {

public static void main(String[] args) {
Pair<Integer> iPair = new Pair<Integer>();
Pair<String> sPair = new Pair<String>();
if(iPair.getClass().equals(sPair.getClass()))
System.out.println(true);
else
System.out.println(false);

if(iPair.getClass().equals(Pair.class))
System.out.println(true);
else
System.out.println(false);
}
}


结果会输出两个true,说明Pair和Pair的类型都是同一个,而且都是
Pair.class
。但是在编译时这又是两种不同的类型,因为
iPair=sPair
这句代码会编译出错。

4. 不能创建参数化类型的数组

什么叫不能建参数化类型的数组呢,就是这个样子
Pair<String>[] pairs = new Pair<String>[10];
是不行的,编译的时候就会出错。

这又是为什么呢?最根本的原因在于数组在创建的时候就必须知道其内部元素的类型,并且会记住这个元素的类型,每次添加新的元素的时候都会检查一下,如果试图往里面添加其它类型的元素就会抛出一个
ArrayStoreException
异常,而类型擦除会使这种机制无效。

试想,如果
Pair<String>[] pairs = new Pair<String>[10];
这句代码是能行的,在类型擦除后pairs的类型就是Pair[],这样子的话就可以把它转换为Object[],如
Object[] objArr = pairs;
,Object是包容万物呀,那我可以这样子做,
objArr[0] = new Pair<Integer>();
这样子做的话就绕过了数组存储的检查。然后你能想到的灾难性的结果就有了,你以为Pair中存储的是String类型的数据,人家存的是Integer呢,把Integer类型数据当成String类用,不死才怪。

最后需要注意的是,不能创建参数化类型的数组说的是创建,但是声明一个带参数化类型的数组还是可以的,如
Pair<String>[] pairs;
不过就是不能用
new Pair<String>[10]
来初始化这个变量。

5. 可变参数的警告

上面我们说到不能创建参数化类型的数组,但是其实这是只准州官放火,不准百姓点灯,虚拟机就可以创建参数化类型的数组!请看下面这个方法

public static <T> void store(Collection<T> coll,T ...ts ){
for (T t : ts) {
coll.add(t);
}
}


大家应该清楚java把可变参数当做数组处理,所以ts应该是一个数组变量。然后再看看下面的调用代码

Collection<Pair<String>> coll = new ArrayList<Pair<String>>();
Pair<String> p1 = new Pair<String>();
Pair<String> p2 = new Pair<String>();
addAll(coll, p1,p2);


你看,为了调用addAll这个方法,虚拟机必须创建一个Pair数组,这就违反了前面的规则。不过虚拟机是老大嘛,这个时候你只会得到一个警告而非错误。而且可以采用两种方法来抑制这个警告

在调用addAll方法的方法上添加注解
@SuppressWarnings("unchecked")


在addAll方法上添加注解
@SafeVarargs


虽然是虚拟机创建的这个参数化类型的数组,但是还是会存在上一节说的转Object类型的问题。

6. 不能实例化类型变量

不能像这样子使用类型变量

new T(…);

new T[…];

T.class

很明显T类型是要在运行时才能知道具体的类型,像上面那样子直接用肯定是行不通的,但是可以用反射技术来完成上面的意图。

如实例化一个类型变量,不能用
T.class.newInstance();
这样子,但是要调用newInstance方法,总得有个Class对象吧,自己没有而且不能造,那只能靠进口了,让调用者传进来,像下面这样子

public static <T> T getInstance(Class<T> clazz){
T instance = null;
try{
instance = clazz.newInstance();
}catch (Exception e) {
}
return instance;
}


调用代码就是这样子
String instance = getInstance(String.class);


这里需要注意的是Class类本身就是一个泛型类,例如String.class就是Class的实例,所以在getInstance方法内能够推断出instance的具体类型。

同样的,也可以实例化一个类型变量数组

public static <T> T[] getArray(Class<T> clazz){
// 创建一个clazz类型的数组长度为2
Object obj = Array.newInstance(clazz, 2);
T[] arr = (T[]) obj;
return arr;
}


调用代码

public static void main(String[] args) {
String[] arr = getArray(String.class);
System.out.println(arr.length);
}


结果输出2

7. 泛型类中的类型变量在静态上下文中无效

什么意思呢?就是说不能再静态变量或者静态方法中引用类型变量,像下面这样子

public class Singleton<T> {
// 报错
private static T instance;
// 报错
public static T getInstance(){
if(instance != null)
return instance;
}
}


像上面的代码是不能编译通过的。很多人说因为类型变量在类实例化的时候才会确定具体类型,而静态方法和成员不用实例化的时候就可以用,所以有矛盾。其实并不是如此,像上一节的例子中我就定义静态的泛型方法,这是是可以的。

考虑一下如果上面的例子能编译通过。
Singleton<Person>
Singleton<Student>
在经历过类型擦除后,在运行中的时候是同一个类,而静态变量又称为类变量,一个类的所有对象共享静态变量。这样子应该就明白了吧,这个时候instance变量到底是Person类型的呢还是Student类型的呢?所以就矛盾了。

8. 不能抛出或捕获泛型类的实例

实际上,连泛型类继承Throwable都有罪,像
Person<T> extends Throwable
是不能通过编译的。

至于为什么,《Java核心技术》里面说的我不是很懂,希望清楚的朋友能指点一下。然后我找到了另一个解释,是这么说的

假设可以抛出有泛型的异常

如果说一个方法被声明为
throws SomeException<String>,SomeException<Number>


那么对于外界来说String的意义是什么

外界是否要捕获 分别
SomeException<String>和 SomeException<Number>
?

还是只要捕获
SomeException<?>


这无疑增加了异常处理的复杂度。

异常,最初设计就是为了把异常处理的代码和业务逻辑的代码分开,不会粘粘到一起。

这样可以在写业务逻辑的时候抛开泛型暂时不管,先把完整的业务逻辑写完,然后单独处理异常情况。

现在如果异常支持泛型,那么就会导致异常处理的复杂度增加。不能专心与业务逻辑。

9. 注意擦除后的冲突

冲突一:

请先看下面这段代码

public class Pair<T> {

public boolean equals(T value){
return true;
}
}


上面的代码是无法通过编译的,有这么一段报错信息

Name clash: The method equals(T) of type Pair<T> has
the same erasure as equals(Object) of type Object
but does not override it


大概的意思就是
Pair<T>
equals(T)
方法在类型擦除后和Object的
equals(Object)
相同但是又没有重写它(不知道翻译的准备哈~)。没错了。虽然定义
boolean equals(T value)
,但是在编译后,T就被Object替代了,这样子就跟Object的equals方法的方法签名相同了,但是又不是重写它,这就矛盾了。

冲突二

要想支持擦除后的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一个接口的不同参数化。说起来有点绕口,其实就是这个样子

class Person implements Comparable<String>
class Student extends Person implements Comparable<Integer>


这样子一来,Student会实现
Comparable<String>
Comparable<Integer>
,这就是同一个接口的不同参数化。

但是注意咯,在非泛型版本上面的关系是合法的,像下面这样子

class Person implements Comparable
class Student extends Person implements Comparable


为什么会出现这种冲突呢,可能就是跟桥接方法有关,关于桥接方法大家可以去这里看一下http://blog.csdn.net/timheath/article/details/53559470

如果同时实现了
Comparable<String>
Comparable<Integer>
,那么桥接方法就会很尴尬了,它里面桥接的时候是调用哪个具体方法好呢,是
compareTo(Integer o)
还是
compareTo(String o)
,这就是矛盾所在了。

10. 结言

在这里谈一下个人的感想哈,警醒一下自己也给正在努力学习的朋友一点建议。

学习得有些探究精神,就像是这篇文章的内容,我都是反复看《Java核心技术》里面的知识,再加以研磨才得以写出来的,写的很多都是自己的理解。有很多地方不懂,就去查,却发现很多博客直接把书中的内容直接复制粘贴上去的,没有一点解释或者自己的理解,更有甚者还删减了一些内容。关于泛型的知识一开始我以为我一天就能看完,然而我现在已经看了好几天了,并且还在仔细的研磨中。Java里面包含很多设计思想,很多东西可以怎么用,不可以怎么用,都是有它的道理的,这可以防止我们犯一下比较少见的错误,而且这些都是那些人才设计出来的,学习一下这些设计思想,可能以后自己会用到呢。

不积小流无以成江海 不积跬步无以至千里

如果上面的内容有错误的地方或者讲的不好的地方,还请大家指点一下,我好及时修改。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 泛型 泛型擦除