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

java泛型(二)--泛型的擦除

2015-11-12 19:07 656 查看
相信通过上一篇泛型相关的文章,大家对泛型有了一个大致的了解,现在我们来简单的看一个小例子:

public class GenericEraseTest {
public static void main(String[] args){
ArrayList<String> stringList = new ArrayList<String>();
ArrayList<Integer> intList = new ArrayList<Integer>();

System.out.println(stringList.getClass() == intList.getClass());
}
}



上面的代码打印出的应该是什么?根据我们上一次泛型基础所了解到的,int类型的元素是无法添加到stringList中的,按正常的思维,打印出的值应该是false,因为很明显两个类的行为不同(接受的参数类型不同)。但是结局又是让人崩溃的,打印出了true。我们将上面的java代码编译成class文件,然后再反编译出来结果如下所示:

public class GenericEraseTest {
public static void main(String[] args){
ArrayList stringList = new ArrayList();
ArrayList intList = new ArrayList();

System.out.println(stringList.getClass() == intList.getClass());
}
}
现在看起来应该熟悉多了,打印出的true也应该是在意料之中了。但是为什么这样?这就引出了今天的第一个概念:

泛型的擦除

在java语言中,泛型只存在于源代码中,而在字节码中泛型类都被替换为原生类,在运行期所操作的类型也都是原生类,这种特性我们称之为擦除。相信有了上面的实例,不难理解这句话的意思。我们来想一下java的泛型为什么通过擦除来实现?

在C++或者C#中,泛型无论是在源码,还是在编译的中间代码,亦或者是在运行期中,泛型都是真实存在的,我们都可以正常的使用它,List<String>和List<Integer>就是两个不同的类,但是在java中并不是这样的。关于在java中为什么利用擦除来实现泛型我了解的有大概两种说法:

1.对兼容性方面的考虑。在Thinking in java 一书中作者说了如下一段话:

“为了减少潜在的关于擦除的混淆,你必须清楚的认识到这不是一个语言特性,它是java的泛型实现中的一种折中。如果泛型在Java1.0中就已经是其一部分了,那么这个特性将不会用擦除来实现——它将使用具体化”

在java1.5以后的版本中,即使引入了泛型的概念,我们也必须使其能兼容之前在没有泛型时所编写的类库。而之前所写的代码也要能在泛型加入类库中去时继续保持可用。

2.由于在C++或C#中泛型是真实存在的,List<String>和List<Integer>将生成两个不同的类,这样很容易导致类膨胀的问题,使得代码编译的速度降低。

上面两种说法都有自己的道理,也无法去深究其对错,而我们要做的是理解它本质的含义,以便在使用时可以得心应手。

泛型擦除所带来的影响

在泛型代码的内部,我们无法获得任何有关泛型参数类型的信息,虽然能得到类型的参数标识,但是并不能用来创建实例。这句话看起来比较抽象,什么是参数类型信息,什么是参数标识还是一头雾水,没关系,我们看下面几个例子:

public T get() {
T t = new T();
return a;
}

如果我们在代码中写了类似上面的语句,那么编译器报错,并且提示如下的语句“Cannot instantiate the type T”。

if(T instanceof String){
//xxxxx
}

如果我们在代码中这样写,编译器同样也会报错“T cannot be resolved”。

现在大家应该可以明白上面所说的不能获得任何参数类型的信息,也不能用来创建实例hi什么意思了吧。但是将其作为类型来转型还是可以的,比如说这样:

public T get() {
Object obj = new Object();
return (T)obj;
}
编译器只是报了一个转型的警告但是并没有阻止,这段代码也解释了上面提到的可以获得类型的参数标识。

为了分析内部的原因,我们引入一个简单的Holder类代码:

public class Holder<T> {
private T a;

public Holder(T a) {
this.a = a;
}

public void set(T a) {
this.a = a;
}

public T get() {
return a;
}

public static void main(String[] args) {
Holder<String> holder = new Holder<String>("123");
String string = holder.get();
System.out.println(string);

}
}
以下是反编译出来的执行过程:

Compiled from "Holder.java"
public class com.fsc.generic.Holder<T> {
public com.fsc.generic.Holder(T);
Code:
0: aload_0
1: invokespecial #1                  // Method java/lang/Object."<init>":
()V
4: aload_0
5: aload_1
6: putfield      #2                  // Field a:Ljava/lang/Object;
9: return

public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield      #2                  // Field a:Ljava/lang/Object;
5: return

public T get();
Code:
0: aload_0
1: getfield      #2                  // Field a:Ljava/lang/Object;
4: areturn

public static void main(java.lang.String[]);
Code:
0: new           #3                  // class com/fsc/generic/Holder
3: dup
4: ldc           #4                  // String 123
6: invokespecial #5                  // Method "<init>":(Ljava/lang/Objec
t;)V
9: astore_1
10: aload_1
11: invokevirtual #6                  // Method get:()Ljava/lang/Object;
14: checkcast     #7                  // class java/lang/String
17: astore_2
18: getstatic     #8                  // Field java/lang/System.out:Ljava/
io/PrintStream;
21: aload_2
22: invokevirtual #9                  // Method java/io/PrintStream.printl
n:(Ljava/lang/String;)V
25: return
}

我们可以从中看到两点比较重要的内容:1.在调用set方法传递参数的时候是以Object对象来接收的。2.在调用get方法进行返回时返回的是Object,仍然需要转型,只不过转型不是手动的而已,是编译器自动帮我们插入的。通过上面的代码我们也应该明白了为什么不能使用new关键字来创建T类型的对象了,因为T根本就不存在,在类中针对T的方法操作其实都是针对Object来的。

在对泛型的使用中,我们失去了它的参数类型信息,不能用来创建对象以及类型的比较,接下来提供一种思路来处理这种问题:

public class Holder<T> {
private Class<T> cls;

public Holder(Class<T> cls) {
this.cls = cls;
}

public T getInstance(){
T newInstance = null;
try {
newInstance = cls.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return newInstance;
}

public boolean isInstance(Object obj){
return cls.isInstance(obj);
}

public static void main(String[] args) {
Holder<String> holder = new Holder<String>(String.class);
boolean isInstance = holder.isInstance("123");
System.out.println(isInstance);
String instance = holder.getInstance();
System.out.println(instance);
}
}
我们重新定义了一个Holder类,只不过它存储的东西编程了具体类型的Class对象,这样我们就能通过这个Class对象来在一定程度上来弥补泛型类所带来的缺陷。当然这也仅仅只是一段示例,若要真正使用,还需要处理很多问题。

泛型数组的创建

通过前面的学习我们知道,在泛型内无法得到泛型参数类型的信息,那么我们如何创建出泛型参数类型的数组呢?

由于泛型的类型信息在运行期被擦除掉了,在有泛型类型参与的地方全部变为Object(当然也有可能是其他的类,在下一篇文章中会介绍),那么我们是不是可以考虑创建出一个Object类型的数组,然后将其转型储存起来,就像下面这样:

public class GenericArray<T> {
private T[] array;

@SuppressWarnings("unchecked")
public GenericArray(int size){
array = (T[]) new Object[size];
}

public T[] getArray(){
return array;
}
public static void main(String[] args) {
GenericArray<String> genericArray = new GenericArray<String>(2);
String[] array2 = genericArray.getArray();
}
}

在创建数组的时候因为涉及到了转型信息,所以使用注解抑制了警告。运行上面的程序将会发现报错了,错误如下:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
错误已经说的很清楚了,不能将Object类型的数组转换成String类型的数组

我们再来看另外一种写法:

public class GenericArray<T> {
private Object[] array;

public GenericArray(int size){
array = new Object[size];
}

@SuppressWarnings("unchecked")
public T[] getArray(){
return (T[]) array;
}
public static void main(String[] args) {
GenericArray<String> genericArray = new GenericArray<String>(2);
String[] array2 = genericArray.getArray();
}
}
将转型的位置换了地方,在泛型数组中用Object数组来存放数据,但是很不幸,仍然报了和刚才一样的错误。虽然这种写法仍然报错,但是如果仔细查看java的源码就会发现ArrayList使用的就是这种方式,尽管它没有向我们提供接口来返回内部的数组。

下面再来看一种写法:

public class GenericArray<T> {
private T[] array;
private Class cls;

@SuppressWarnings("unchecked")
public GenericArray(int size, Class cls){
this.cls = cls;
array = (T[]) Array.newInstance(cls, size);
}

public T[] getArray(){
return array;
}
public static void main(String[] args) {
GenericArray<String> genericArray = new GenericArray<String>(2,String.class);
String[] array2 = genericArray.getArray();
}
}

运行结果一切正常,使用这种方式来创建泛型数组是可取的。再者,在我们想使用泛型数组的时候可以直接使用容器,容器也是支持泛型的,所以和使用数组的感觉没什么太大的不同。

在下一篇文章中将会详细的介绍泛型的边界,通配符,以及一些总结。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息