Java中的泛型不是语言内在的机制,而是后来添加的特性,这样就带来一个问题:非泛型代码和泛型代码的兼容性。泛型是JDK1.5才添加到Java中的,那么之前的代码全部都是非泛型的,它们如何运行在JDK1.5及以后的VM上?为了实现这种兼容性,Java泛型被局限在一个很狭窄的地方,同时也让它变得难以理解,甚至可以说是Java语言中最难理解的语法。
擦除
为了实现与非泛型代码的兼容,Java语言的泛型采用擦除(Erasure)来实现,也就是泛型基本上由编译器来实现,由编译器执行类型检查和类型推断,然后在生成字节码之前将其清除掉,虚拟机是不知道泛型存在的。这样的话,泛型和非泛型的代码就可以混合运行,当然了,也显得相当混乱。
在使用泛型时,会有一个对应的类型叫做原生类型(raw type),泛型类型会被擦除到原生类型,如Generic<T>会被查处到Generic,List<String>会被查处到List,由于擦除,在虚拟机中无法获得任何类型信息,虚拟机只知道原生类型。下面的代码将展示Java泛型的真相-擦除:
class Erasure<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Erasure<String> eras = new Erasure<String>();
eras.set("not real class type");
String value = eras.get();
}
}
使用javap反编译class文件,得到如下代码:class com.think.generics.Erasure<T> {
com.think.generics.Erasure();
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: return
public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield #23 // Field t:Ljava/lang/Object;
5: return
public T get();
Code:
0: aload_0
1: getfield #23 // Field t:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #1 // class com/think/generics/Erasure
3: dup
4: invokespecial #30 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #31 // String not real class type
11: invokevirtual #33 // Method set:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #35 // Method get:()Ljava/lang/Object;
18: checkcast #37 // class java/lang/String
21: astore_2
22: return
}
从反编译出来的字节码可以看到,泛型Erasure<T>被擦除到了Erasure,其内部的字段T被擦除到了Object,可以看到get和set方法中都是把t作为Object来使用的。最值得关注的是,反编译代码的倒数第三行,对应到Java代码就是String value = eras.get();编译器执行了类型转换。这就是Java泛型的本质:对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这样的泛型真的是泛型吗?
即便我们可以说,Java中的泛型确实不是真正的泛型,但是它带来的好处还是显而易见的,它使得Java的类型安全前进了一大步,原本需要程序员显式控制的类型转换,现在改由编译器来实现,只要你按照泛型的规范去编写代码,总会得到安全的保障。在这里,我们不得不思考一个问题,理解Java泛型,那么其核心目的是什么?我个人认为,Java泛型的核心目的在于安全性,尤其是在理解泛型通配符时,一切奇怪的规则,归根结底都是处于安全的目的。
类型信息的丢失
由于擦除的原因,在泛型代码内部,无法获得任何有关泛型参数类型的信息。在运行时,虚拟机无法获得确切的类型信息,一切以来确切类型信息的工作都无法完成,比如instanceof操作,和new表达式,
class Erasure<T> {
public void f() {
if(arg instanceof T) //Error
T ins = new T();//Error
T[] array = new T[10];//error
}
}
那么在需要具体的类型信息时,我们就要记住Class对象来实现了,凡是在运行时需要类型信息的地方,都使用Class对象来进行操作,比如:
class Erasure<T> {
private Class<T> clazz;
Erasure(Class<T> kind) {
clazz = kind;
}
public void f() {
if(clazz.isInstance(arg)) {}
T t = clazz.newInstance();//必须要有无参构造方法
}
}
泛型类中的数组
数组是Java语言中的内建特性,将泛型与数组结合就会有一些难以理解的问题。首先Java中的数组是协变的,Integer是Number的子类,所以Integer[]也是Number[]的子类,凡是使用Number[]的地方,都可以使用Integer[]来代替,而泛型是不协变的,比如List<String>不是List<Object>的子类,在通配符中,会详细讨论这些情况。
由于无法获得确切的类型信息,我们怎么样创建泛型数组呢?在Java中,所有类的父类都是Object,所以可以创造Object类型的数组来代替泛型数组:
public class Array<T> {
private int size = 0;
private Object[] array;
public Array(int size) {
this.size = size;
array = new Object[size];
}
//编译器会保证插入进来的是正确类型
public void put(int index,T item) {
array[index] = item;
}
//显式的类型转换
public T get(int index) {
return (T)array[index];
}
public T[] rep() {
return (T[])array;
}
private static class Father {}
private static class Son extends Father {}
public static void main(String[] args) {
Array<String> instance = new Array<String>(10);
String[] array = instance.rep();//异常
}
}
在上面的代码中,get()和put()都可以正确的运行,编译器会保证类型的正确性。但是当rep()返回时赋给String[]类型的数组,则会抛出ClassCastException异常,抛出这样的异常是在意料之中的。在Java中,数组其实是一个对象,每一个类型的数组都后一个对应的类,这个类是虚拟机生成,比如上面的代码中,我们定义了Object数组,在运行时会生成一个名为"[Ljava.lang.Object"的类,它代表Object的一维数组;同样的,定义String[]数组,其对应的类是"[Ljava.lang.String"。从类名就可以看出,这些代表数组的类都不是合法的Java类名,而是由虚拟机生成,虚拟机在生成类是根据的是实际构造的数组类型,你构造的是Object类型的数组,它生成的就是代表Object类型的数组的类,无论你把它转型成什么类型。换句话说,没有任何方式可以推翻底层数组的类型。前面说到,数组是协变的,也就是说[Ljava.lang.Object其实是[Ljava.lang.String的父类,比如下面的代码会得到true:
String[] array = new String[10];
System.out.println(array instanceof Object[]);
所以在将rep()返回值赋给String[]类型时,它确实是发生了类型转换,只不过这个类型转换
不是数组元素的转换,并不是把Object类型的元素转换成String,而是把[Ljava.lang.Object转换成了Ljava.lang.String,是父类对象转换成子类,必然要抛出异常。那么问题就出来了,我们使用泛型就是为了获得更加通用的类型,既然我声明的是Array<String>,往里存储的元素是String,得到的元素也是String,我理所应当的认为,我获得的数组应该也是String[],如果我这么做,你却给我抛异常,这是几个意思啊!
导致这个问题的罪魁还是擦除,由于擦除,没有办法这样这样定义数组:T[] array = new T[size];为了产生具体类型的数组,只能借助于Class对象,在Java类库提供的Array类提供了一个创造数组的方法,它需要数组元素类型的Class对象和数组的长度:
private Class<T> kind;
public ArrayMaker(Class<T> kind ) {
this.kind = kind;
}
@SuppressWarnings("unchecked")
T[] create(int size) {
T[] array = (T[])Array.newInstance(kind, size);
System.out.println(array.getClass().getName());
return array;
}
这样构造的就是具体类型的数组,比如传递进来的是String.class,那么调用create方法会打印:[Ljava.lang.String,在底层构造的确实是String类型的数组。使用这样的方式创建数组,应该是一种更优雅,更安全的方式。
以上内容介绍了Java泛型的实质,它的泛型更像是一颗语法糖,一颗由编译器包括的语法糖。由编译器实现的泛型又有诸多奇怪的限制,可泛型的功能又是如此强大,使用的又是如此频繁,所以对泛型的抱怨一直在持续,同时,泛型又是个绕不过去的弯。
转载请注明:喻红叶《泛型-擦除实现的Java泛型》
分享到:
相关推荐
总结来说,Java泛型的类型擦除虽然在运行时消除了类型信息,但通过编译时的类型检查、桥接方法、通配符等补偿机制,仍然实现了强大的类型安全和便利性。开发者应理解这些补偿机制,以便更好地利用Java泛型进行类型...
通过学习和掌握Java泛型,开发者能够编写出更健壮、类型安全的代码,减少类型转换错误,提升代码质量。这个视频教程"29-API-集合框架-泛型-使用"应该会深入浅出地讲解这些概念,对于想要提升Java编程技能的初学者或...
然而,Java 的泛型在运行时是被擦除的,这意味着在运行时刻,所有的泛型类型信息都会丢失,无法直接用来创建对象或进行类型检查。这导致我们无法使用 `new T()` 或 `instanceof` 这样的操作。为了解决这个问题,我们...
总的来说,Java泛型提供了一种强大的工具,让开发者能够编写更加安全、灵活且可复用的代码。它简化了类型转换,增强了类型检查,并通过类型擦除保持了与现有Java代码的兼容性。理解和熟练运用泛型是每个Java开发者的...
解析Java泛型的类型擦除 Java 泛型是 Java SE 1.5 的新特性,它们在语法和应用环境上与 C++ 中的模板相似,但是本质上它们之间有着区别,这种区别就在于 Java 泛型的类型擦除。 Java 泛型的类型擦除是 Java 语言...
"Java 泛型总结(一):基本用法与类型擦除" Java 泛型是 Java 语言中的一种强大功能,它可以使代码更加简洁、安全。下面是对 Java 泛型的基本用法和类型擦除机制的介绍。 泛型的基本用法 ------------- 泛型是...
4. 泛型擦除: - Java的泛型在编译后会被擦除,所有类型参数都会被替换为它们的边界或者Object。 - 这意味着在运行时,泛型类型信息不再存在,但是编译时的类型检查仍然有效。 5. 泛型和集合: - 集合框架如...
- 泛型擦除前的例子把这段Java代码编译成Class文件,然后再用字节码反编译后,將会发现泛型都不见了,又变回了Java泛型出现之前的写法,泛型类型都变回了原
Java泛型在编译期之后就会把类型给擦除,在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这就意味着,泛型只是对于编译期来说的,在执行期间是没有...
然而,Java泛型的实现机制——类型擦除,也带来了一系列的问题和限制。本文将深入探讨Java泛型擦除的工作原理、它对编程的影响,以及在实际开发中的应对策略。 Java泛型的类型擦除机制是Java泛型实现的核心,它使得...
1. 类型擦除:Java 8.0的泛型在编译后会进行类型擦除,这意味着在运行时无法直接检测泛型类型。在Apla到Java的转换中,需要处理好类型信息的保留,以确保编译后的代码能正确处理各种泛型类型。 2. 参数化类型:Apla...
泛型擦除是指在编译时,Java 编译器将泛型类型参数擦除,替换为 Object 或其他类。例如,在编译时,`List<String>` 将被擦除为 `List`,使用 `Object` 作为类型参数。 桥接方法是指在子类中重写父类的方法时,Java ...
Java泛型是Java编程语言中的一个强大特性,它允许在定义类、接口和方法时使用类型参数,从而实现参数化类型。这使得代码更加安全、可读性更强,并且能够减少类型转换的必要。在“java泛型的内部原理及更深应用”这个...
* 泛型擦除:Java编译器在编译时会擦除泛型信息,这意味着在运行时,泛型信息将被擦除。 * 类型参数的约束:泛型的类型参数需要遵守一定的约束,例如,类型参数不能是基本类型。 * 泛型的使用需要遵守Java语言的语法...
1. **类型擦除**: Java泛型在编译后会进行类型擦除,也就是说,所有的泛型类在运行时都会退化为未使用泛型的原始形式。这意味着在运行时无法检查泛型类型,但编译时的类型检查可以避免很多错误。 2. **边界通配符**...
Java泛型是Java编程语言中一个强大的特性,它允许在定义类、接口和方法时使用类型参数,从而实现参数化类型。泛型的主要目标是提高代码的类型安全性和重用性,减少类型转换的麻烦,并在编译时捕获可能的类型错误。...
综上所述,虽然Java泛型在编译后会进行类型擦除,但通过上述技巧,我们仍然能够在运行时获得关于泛型类实例化类型的一些信息。在实际开发中,这些方法可以帮助我们编写更加灵活和安全的代码。在示例文件`GenericRTTI...
Java 泛型详解 Java 泛型是 Java 5 中引入的一种编程技术,旨在提高代码的复用性和类型安全性。泛型允许开发者编写出“非特定类型”的代码,能够根据实际情况选择合适的类型,从而提高代码的灵活性和可维护性。 1....