`
喻红叶
  • 浏览: 41064 次
  • 性别: Icon_minigender_1
  • 来自: 哈尔滨
社区版块
存档分类
最新评论

泛型-擦除实现的Java泛型

 
阅读更多

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泛型,开发者能够编写出更健壮、类型安全的代码,减少类型转换错误,提升代码质量。这个视频教程"29-API-集合框架-泛型-使用"应该会深入浅出地讲解这些概念,对于想要提升Java编程技能的初学者或...

    Java 泛型擦除后的三种补救方法

    然而,Java 的泛型在运行时是被擦除的,这意味着在运行时刻,所有的泛型类型信息都会丢失,无法直接用来创建对象或进行类型检查。这导致我们无法使用 `new T()` 或 `instanceof` 这样的操作。为了解决这个问题,我们...

    java中的泛型-详细

    总的来说,Java泛型提供了一种强大的工具,让开发者能够编写更加安全、灵活且可复用的代码。它简化了类型转换,增强了类型检查,并通过类型擦除保持了与现有Java代码的兼容性。理解和熟练运用泛型是每个Java开发者的...

    解析Java泛型的类型擦除.pdf

    解析Java泛型的类型擦除 Java 泛型是 Java SE 1.5 的新特性,它们在语法和应用环境上与 C++ 中的模板相似,但是本质上它们之间有着区别,这种区别就在于 Java 泛型的类型擦除。 Java 泛型的类型擦除是 Java 语言...

    Java 泛型总结(一):基本用法与类型擦除

    "Java 泛型总结(一):基本用法与类型擦除" Java 泛型是 Java 语言中的一种强大功能,它可以使代码更加简洁、安全。下面是对 Java 泛型的基本用法和类型擦除机制的介绍。 泛型的基本用法 ------------- 泛型是...

    关于java基础的泛型的练习

    4. 泛型擦除: - Java的泛型在编译后会被擦除,所有类型参数都会被替换为它们的边界或者Object。 - 这意味着在运行时,泛型类型信息不再存在,但是编译时的类型检查仍然有效。 5. 泛型和集合: - 集合框架如...

    Java-Edge#Java-Interview-Tutorial#Java语法糖之泛型与类型擦除1

    - 泛型擦除前的例子把这段Java代码编译成Class文件,然后再用字节码反编译后,將会发现泛型都不见了,又变回了Java泛型出现之前的写法,泛型类型都变回了原

    很好的Java泛型的总结

    Java泛型在编译期之后就会把类型给擦除,在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这就意味着,泛型只是对于编译期来说的,在执行期间是没有...

    Java泛型擦除深度解析:原理、影响与编程实践

    然而,Java泛型的实现机制——类型擦除,也带来了一系列的问题和限制。本文将深入探讨Java泛型擦除的工作原理、它对编程的影响,以及在实际开发中的应对策略。 Java泛型的类型擦除机制是Java泛型实现的核心,它使得...

    后端研究-Apla-Java算法程序生成系统中新型泛型机制的实现.pdf

    1. 类型擦除:Java 8.0的泛型在编译后会进行类型擦除,这意味着在运行时无法直接检测泛型类型。在Apla到Java的转换中,需要处理好类型信息的保留,以确保编译后的代码能正确处理各种泛型类型。 2. 参数化类型:Apla...

    泛型,泛型擦除,桥接方法

    泛型擦除是指在编译时,Java 编译器将泛型类型参数擦除,替换为 Object 或其他类。例如,在编译时,`List&lt;String&gt;` 将被擦除为 `List`,使用 `Object` 作为类型参数。 桥接方法是指在子类中重写父类的方法时,Java ...

    java泛型的内部原理及更深应用

    Java泛型是Java编程语言中的一个强大特性,它允许在定义类、接口和方法时使用类型参数,从而实现参数化类型。这使得代码更加安全、可读性更强,并且能够减少类型转换的必要。在“java泛型的内部原理及更深应用”这个...

    Java泛型研究.pdf

    * 泛型擦除:Java编译器在编译时会擦除泛型信息,这意味着在运行时,泛型信息将被擦除。 * 类型参数的约束:泛型的类型参数需要遵守一定的约束,例如,类型参数不能是基本类型。 * 泛型的使用需要遵守Java语言的语法...

    Java集合框架及泛型

    1. **类型擦除**: Java泛型在编译后会进行类型擦除,也就是说,所有的泛型类在运行时都会退化为未使用泛型的原始形式。这意味着在运行时无法检查泛型类型,但编译时的类型检查可以避免很多错误。 2. **边界通配符**...

    Java泛型_Java中的泛型结构_

    Java泛型是Java编程语言中一个强大的特性,它允许在定义类、接口和方法时使用类型参数,从而实现参数化类型。泛型的主要目标是提高代码的类型安全性和重用性,减少类型转换的麻烦,并在编译时捕获可能的类型错误。...

    java 泛型类的类型识别示例

    综上所述,虽然Java泛型在编译后会进行类型擦除,但通过上述技巧,我们仍然能够在运行时获得关于泛型类实例化类型的一些信息。在实际开发中,这些方法可以帮助我们编写更加灵活和安全的代码。在示例文件`GenericRTTI...

    Java 泛型最全指南(定义和使用+继承泛型类/实现泛型接口+泛型的边界+通配符+类型擦除)

    Java 泛型详解 Java 泛型是 Java 5 中引入的一种编程技术,旨在提高代码的复用性和类型安全性。泛型允许开发者编写出“非特定类型”的代码,能够根据实际情况选择合适的类型,从而提高代码的灵活性和可维护性。 1....

Global site tag (gtag.js) - Google Analytics