java泛型--泛型的擦除

Posted 加冰雪碧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java泛型--泛型的擦除相关的知识,希望对你有一定的参考价值。

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

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();
	

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



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



以上是关于java泛型--泛型的擦除的主要内容,如果未能解决你的问题,请参考以下文章

反射中泛型的擦除赋值

Java泛型用法

Java泛型用法

Java中泛型的介绍与简单使用

java遗珠之泛型类型擦除

java遗珠之泛型类型擦除