这篇博文主要记录学习Java编程思想的一些心得和体会。在这篇文中可能会引用一些优秀博文的内容,我会在文章末尾注明引用博文的地址。
通过Java编程思想一书中的例子来对Java的泛型擦除做一个存在性的演示:
public class ErasedTypeEquivalence{
public static void main(String[] args){
Class c1 = new ArrayList().getClass();
Class c2 = new ArrayList().getClass();
System.out.println(c1==c2);
}
}
//output:
//true
通过上面的代码我们可以很容易的认为ArrayList和ArrayList是两种不同的类型。但是该程序的输出结构却令我们很意外,输出的结构却是true。
其实出现该问题的原因就是Java泛型的擦除在捣鬼。
接下来,我们再来看下面的程序:
class HasF {
public void f() {
System.out.println("HasF f()");
}
public static void main(String[] args) {
Mainpulator hf = new Mainpulator<>(new HasF());
hf.mainpulate();
}
}
class Mainpulator{
private T obj;
public Mainpulator(T x) {
this.obj = x;
}
public void mainpulate() {
obj.f();//error
}
}
不用考虑,到你开始这样写程序的时候,你会发现写到obj.f()的时候,程序就已经报错了。报错提示:The method f() is undefined for the type T,也就是对于T这种类型f方法没有定义。那么我们来看看,T这种类型到底有写什么方法呢?
我们会发现T类型的obj展示出所有的方法分别为:
已经说明上面程序报错不能运行的原因了,那么该如何进行修改上面的程序呢让其能争取的执行呢?
方式1:既然编译器把类型参数擦除到第一边界类型,我们可以对上面的程序进行向下转型。
class HasF {
public void f() {
System.out.println("HasF f()");
}
public static void main(String[] args) {
Mainpulator hf = new Mainpulator<>(new HasF());
hf.mainpulate();
}
}
class Mainpulator{
private T obj;
public Mainpulator(T x) {
this.obj = x;
}
public void mainpulate() {
((HasF)obj).f();//向下转型
}
}
//output:
//HasF f()
方式2:对泛型类型参数声明边界
class HasF {
public void f() {
System.out.println("HasF f()");
}
public static void main(String[] args) {
Mainpulator hf = new Mainpulator<>(new HasF());
hf.mainpulate();
}
}
class Mainpulator<T extends HasF>{//通过extends声明T的边界为HasF
private T obj;
public Mainpulator(T x) {
this.obj = x;
}
public void mainpulate() {
obj.f();
}
}
//output:
//HasF f()
总结:泛型在编译时期会被编译器进行擦除操作,并且每次擦除到泛型的第一边界处,所以java中的泛型都是伴随在边界周围发生的。
由于泛型在编译时期会被擦除,也就是说任何在运行时期需要确切类型信息的操作都将无法工作。
例如下面的代码:
public class Erased {
public static final int SIZE = 100;
@SuppressWarnings("unchecked")
public void f(Object obj) {
if(obj instanceof T) {}
/**error:
* Cannot perform instanceof check against type
* parameter T. Use its erasure Object instead since further generic type
* information will be erased at runtime
*/
T t = new T(); //error: Cannot instantiate the type T
T[] array = new T[SIZE];//error: Cannot create a generic array of T
T[] obj1 = (T[]) new Object[SIZE];
}
}
1.对于第一个报错解决的方法可以通过引用显示的Class对象来擦除进行弥补:
public class ClassTypeComputer {
private Class kind;
public ClassTypeComputer(Class kind) {
this.kind = kind;
}
public boolean f(Object org) {
return kind.isInstance(org);
}
public static void main(String[] args) {
ClassTypeComputer house = new ClassTypeComputer<>(House.class);
System.out.println("House intanceof House:"+house.f(new House()));
System.out.println("Mirr intanceof House:"+house.f(new Mirr()));
ClassTypeComputer mirr = new ClassTypeComputer<>(Mirr.class);
System.out.println("House intanceof Mirr:"+mirr.f(new House()));
System.out.println("Mirr intanceof Mirr:"+mirr.f(new Mirr()));
}
}
//output:
//House intanceof House:true
//Mirr intanceof House:true
//House intanceof Mirr:false
//Mirr intanceof Mirr:true
该程序通过显示的Class对象引用来逃避直接对确切类型信息的操作。由于泛型参数在编译的时候会被编译器擦除。因此,在运行时期需要确切类型的信息的操作将失效。
2.对于第二个报错原因除了擦除外,还有可能有一个原因,就是编译器不能确定T类型是否有默认类型的构造器。解决此方式可以显示的引用工厂类来创建类型实例:
public class ClassFactory {
private Class kind;
public ClassFactory(Class kind) {
this.kind = kind;
}
public T create() {
try {
return kind.newInstance();
}catch(Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
ClassFactory s = new ClassFactory<>(Student.class);
System.out.println(s.create());
}
}
class Student{}
//output:
//generics.Student@2401f4c3
前面两个说了泛型擦除对判断类型和创建new实例带来的问题。现在我们来了解一下泛型擦除对数组带来的一些问题:
3.泛型擦除在数组中的问题:
class Generic{}
public class ArrayOfGerneric {
static final int SIZE = 100;
static Generic[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
//下面这段程序在运行时期会报ClassCastException异常
gia = (Generic[]) new Object[SIZE];
gia = new Generic[SIZE];
System.out.println(gia);
gia[0] = new Generic();
//下面两句代码都是绘制编译时期报错
//gia[1] = new Object();
//gia[2] = new Generic();
}
}
当然,大家看到这个程序的时候会发现,这个数组只有后面两行的代码才跟泛型有一定的关系。对于main方法中的第一行代码,为什么会在运行时期报错呢?
因为在编译时期把数Object[]换为Generic[]类型的数组了,但是数组有一个特性:数组的实际类型时在创建数组的时候才会确定。也就是说在编译时期的转换信息只存在编译时期,而在运行时期他仍然是Object[]数组。当然也可以说数组是不能进行转型的。
接下来我们再分析下面的代码:
public class ArrayGeneric {
private T[] array;
@SuppressWarnings("unchecked")
public ArrayGeneric(int size) {
array = (T[]) new Object[size];
}
public void put(int index,T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
ArrayGeneric gai = new ArrayGeneric(100);
System.out.println(gai.array);
//Integer[] i = gai.rep();//error:ClassCastException
}
}
//output:
//[Ljava.lang.Object;@4361bd48
//2
通过上面的代码,我们可以看出来,当运行此程序的时候,能够顺利的创建数组,但是通过打印信息,我们可以看出来,创建的数组却是Object类型的数组,而并不是Integer类型的数组。并且如果我们如果执行rep方法,会报ClassCastException异常。出现这些原因究竟是什么原因呢?
其实,我们已经说了,Java中的泛型会在编译时期进行擦除操作。因此类型参数,会被Object所取代,所以就能正确的创建出Object类型的数组,但是当我们执行rep方法的时候,会把Object类型的数组返回给Integer类型的数组,所以就会报ClassCastException异常。如果对上面的程序进行修改,让泛型参数的边界不再是Object类型,在创建数组的时候就会报ClassCastException异常。请看代码:
public class ArrayGeneric<T extends Integer> {
private T[] array;
@SuppressWarnings("unchecked")
public ArrayGeneric(int size) {
array = (T[]) new Object[size];
}
public void put(int index,T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
ArrayGeneric gai = new ArrayGeneric(100);
}
}//output:
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
at generics.ArrayGeneric.(ArrayGeneric.java:8)
at generics.ArrayGeneric.main(ArrayGeneric.java:24)
大家可以看出来这段代码和上面的代码就只是在声明泛型参数的时候有所不同,这段代码通过限定泛型参数T的第一边界是Integer类型。如果这样的话,我们在创建数组的时候,就相当于我们在创建数组的时候,对数组进行了(Integer[])的转换。因此在创建数组的时候会报ClassCastException异常导致创建失败。
为了更好的理解这两点的不同,我分别给出上面两段代码构造器的反编译代码进行对比:
public class generics.ArrayGeneric {
public generics.ArrayGeneric(int);
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."":()V
4: aload_0
5: iload_1
6: anewarray #3 // class java/lang/Object
9: putfield #15 // Field array:[Ljava/lang/Object;
12: return
public class generics.ArrayGeneric<T extends java.lang.Integer> {
public generics.ArrayGeneric(int);
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."":()V
4: aload_0
5: iload_1
6: anewarray #3 // class java/lang/Object
9: checkcast #15 // class "[Ljava/lang/Integer;"
12: putfield #16 // Field array:[Ljava/lang/Integer;
15: return
注意看这代码,在第二段代码中,程序对创建的Object数组进行了checkcast转型为Integer类型的操作,因此这就是程序出错的原因。
通过这些代码,我们可以发现,泛型并不是很适用在数组中。那么我们非要适用泛型数组该怎么办呢?
public class ArrayGeneric2 {
private T[] array;
@SuppressWarnings("unchecked")
public ArrayGeneric2(Class cls,Integer size) {
array = (T[]) Array.newInstance(cls, size);
}
public void put(T item,Integer index) {
array[index] = item;
}
public T get(Integer index) {
return array[index];
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
ArrayGeneric2 a = new ArrayGeneric2<>(Integer.class,10);
System.out.println(a.array);//[Ljava.lang.Integer;@53bd815b
Integer[] rep = a.rep();
ArrayGeneric2 a2 = new ArrayGeneric2<>(String.class,10);
System.out.println(a2);//[Ljava.lang.String;@53bd815b
String[] rep2 = a2.rep();
}
}
同样给出这种方式构造的反编译代码:
public class generics.ArrayGeneric2 {
public generics.ArrayGeneric2(java.lang.Class, java.lang.Integer);
Code:
0: aload_0
1: invokespecial #13 // Method java/lang/Object."":()V
4: aload_0
5: aload_1
6: aload_2
7: invokevirtual #16 // Method java/lang/Integer.intValue:()I自动拆箱
10: invokestatic #22 // Method java/lang/reflect/Array.newInstance:(Ljava/lang/Class;I)Ljava/lang/Object;
13: checkcast #28 // class "[Ljava/lang/Object;"转型
16: putfield #29 // Field array:[Ljava/lang/Object;
19: return
如果我们此时同时也给泛型参数限定边界:
public class ArrayGeneric2<T extends Integer> {
private T[] array;
@SuppressWarnings("unchecked")
public ArrayGeneric2(Class<T> cls,Integer size) {
array = (T[]) Array.newInstance(cls, size);
}
public void put(T item,Integer index) {
array[index] = item;
}
public T get(Integer index) {
return array[index];
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
ArrayGeneric2 a = new ArrayGeneric2<>(Integer.class,10);
System.out.println(a.array);//[Ljava.lang.Integer;@53bd815b
Integer[] rep = a.rep();
}
}
运行代码任然不会报错。这里也给出构造器的反编译代码:
public class generics.ArrayGeneric2<T extends java.lang.Integer> {
public generics.ArrayGeneric2(java.lang.Class, java.lang.Integer);
Code:
0: aload_0
1: invokespecial #13 // Method java/lang/Object."":()V
4: aload_0
5: aload_1
6: aload_2
7: invokevirtual #16 // Method java/lang/Integer.intValue:()I自动拆箱
10: invokestatic #22 // Method java/lang/reflect/Array.newInstance:(Ljava/lang/Class;I)Ljava/lang/Object;
13: checkcast #28 // class "[Ljava/lang/Integer;"将newInstance返回的Object对象转换为Integer数组
16: putfield #29 // Field array:[Ljava/lang/Integer;
19: return
关于Java泛型出现的原因,我这里就简单的说一下:由于,Java从1.0的时候并不支持泛型。而是在Java5的时候才支持的。所以,考虑到版本的兼容性问题和一种折中的选择,才使Java泛型在编译时期被擦除,而在运行时期,Java中是没有泛型的。
如果我理解的有误,欢迎大家给我指出来,我下来再去改正,让我们一起进步吧!
目前这篇博客主要是来自对Java编程思想的一些学校领悟吧!