相信通过上一篇泛型相关的文章,大家对泛型有了一个大致的了解,现在我们来简单的看一个小例子:
public class GenericEraseTest {
public static void main(String[] args){
ArrayList stringList = new ArrayList();
ArrayList intList = new ArrayList();
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和List就是两个不同的类,但是在java中并不是这样的。关于在java中为什么利用擦除来实现泛型我了解的有大概两种说法:
1.对兼容性方面的考虑。在Thinking in java 一书中作者说了如下一段话:
“为了减少潜在的关于擦除的混淆,你必须清楚的认识到这不是一个语言特性,它是java的泛型实现中的一种折中。如果泛型在Java1.0中就已经是其一部分了,那么这个特性将不会用擦除来实现——它将使用具体化”
在java1.5以后的版本中,即使引入了泛型的概念,我们也必须使其能兼容之前在没有泛型时所编写的类库。而之前所写的代码也要能在泛型加入类库中去时继续保持可用。
2.由于在C++或C#中泛型是真实存在的,List和List将生成两个不同的类,这样很容易导致类膨胀的问题,使得代码编译的速度降低。
上面两种说法都有自己的道理,也无法去深究其对错,而我们要做的是理解它本质的含义,以便在使用时可以得心应手。
泛型擦除所带来的影响
在泛型代码的内部,我们无法获得任何有关泛型参数类型的信息,虽然能得到类型的参数标识,但是并不能用来创建实例。这句话看起来比较抽象,什么是参数类型信息,什么是参数标识还是一头雾水,没关系,我们看下面几个例子:
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 {
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 holder = new Holder("123");
String string = holder.get();
System.out.println(string);
}
}
以下是反编译出来的执行过程:
Compiled from "Holder.java"
public class com.fsc.generic.Holder {
public com.fsc.generic.Holder(T);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":
()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 "":(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 {
private Class cls;
public Holder(Class 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 holder = new Holder(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 {
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 genericArray = new GenericArray(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 {
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 genericArray = new GenericArray(2);
String[] array2 = genericArray.getArray();
}
}
将转型的位置换了地方,在泛型数组中用Object数组来存放数据,但是很不幸,仍然报了和刚才一样的错误。虽然这种写法仍然报错,但是如果仔细查看java的源码就会发现ArrayList使用的就是这种方式,尽管它没有向我们提供接口来返回内部的数组。
下面再来看一种写法:
public class GenericArray {
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 genericArray = new GenericArray(2,String.class);
String[] array2 = genericArray.getArray();
}
}
运行结果一切正常,使用这种方式来创建泛型数组是可取的。再者,在我们想使用泛型数组的时候可以直接使用容器,容器也是支持泛型的,所以和使用数组的感觉没什么太大的不同。
在下一篇文章中将会详细的介绍泛型的边界,通配符,以及一些总结。