Java 泛型擦除

泛型擦除概念

Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
例如:List<String>List<Integer> 在编译后都变成 List

证明泛型擦除

         ArrayList<String> arrayList1=new ArrayList<String>();
		arrayList1.add("abc");
		ArrayList<Integer> arrayList2=new ArrayList<Integer>();
		arrayList2.add(123);
		System.out.println(arrayList1.getClass()==arrayList2.getClass());

我们定义了两个ArrayList数组,不过一个是ArrayList泛型类型,只能存储字符串。一个是ArrayList泛型类型,只能存储整形。最后,我们通过arrayList1对象和arrayList2对象的getClass方法获取它们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下了 原始类型。

ArrayList<Integer> arrayList3=new ArrayList<Integer>();
		arrayList3.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer
		arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
		for (int i=0;i<arrayList3.size();i++) {
			System.out.println(arrayList3.get(i));
		}

在程序中定义了一个ArrayList泛型类型实例化为Integer的对象,如果直接调用add方法,那么只能存储整形的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了 原始类型。

引用检查与编译

说类型变量会在编译的时候擦除掉,那为什么我们往ArrayList arrayList=new ArrayList();所创建的数组列表arrayList中,不能使用add方法添加整形呢?

java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的。

         ArrayList<String> arrayList1=new ArrayList();
		arrayList1.add("1");//编译通过
		arrayList1.add(1);//编译错误
		String str1=arrayList1.get(0);//返回类型就是String
		
		ArrayList arrayList2=new ArrayList<String>();
		arrayList2.add("1");//编译通过
		arrayList2.add(1);//编译通过
		Object object=arrayList2.get(0);//返回类型就是Object
		
		new ArrayList<String>().add("11");//编译通过
		new ArrayList<String>().add(22);//编译错误
		String string=new ArrayList<String>().get(0);//返回类型就是String

类型检查就是针对引用的,会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

泛型擦除与多态的冲突和解决方法

泛型重载变重写?

public class Pair<T> {
	private T value;
	public T getValue() {
		return value;
	}
	public void setValue(T value) {
		this.value = value;
	}
}

public class DateInter extends Pair<Date> {
	@Override
	public void setValue(Date value) {
		super.setValue(value);
	}
	@Override
	public Date getValue() {
		return super.getValue();
	}
}

在这个子类中,我们设定父类的泛型类型为Pair,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:

将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型

    public Date getValue() {
		return value;
	}
	public void setValue(Date value) {
		this.value = value;
	}

实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载

本意是将父类中的泛型转换为Date类型,子类重写参数类型为Date的两个方法

实际上,虚拟机并不能将泛型类型变为Date,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM知道你的本意吗?知道!!!可是它能直接实现吗,不能!!!

如果真的不能的话,那我们怎么去重写我们想要的Date类型参数的方法

JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法

JVM桥方法

使用bytecode viewer插件反编译DateInter

public class com/qhong/basic/genericity/DateInter extends com/qhong/basic/genericity/Pair {

  // compiled from: DateInter.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 13 L0
    ALOAD 0
    INVOKESPECIAL com/qhong/basic/genericity/Pair.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public setValue(Ljava/util/Date;)V
   L0
    LINENUMBER 16 L0
    ALOAD 0
    ALOAD 1
    INVOKESPECIAL com/qhong/basic/genericity/Pair.setValue (Ljava/lang/Object;)V
   L1
    LINENUMBER 17 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L2 0
    LOCALVARIABLE value Ljava/util/Date; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  public getValue()Ljava/util/Date;
   L0
    LINENUMBER 20 L0
    ALOAD 0
    INVOKESPECIAL com/qhong/basic/genericity/Pair.getValue ()Ljava/lang/Object;
    CHECKCAST java/util/Date
    ARETURN
   L1
    LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1041
  public synthetic bridge setValue(Ljava/lang/Object;)V
   L0
    LINENUMBER 13 L0
    ALOAD 0
    ALOAD 1
    CHECKCAST java/util/Date
    INVOKEVIRTUAL com/qhong/basic/genericity/DateInter.setValue (Ljava/util/Date;)V
    RETURN
   L1
    LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1041
  public synthetic bridge getValue()Ljava/lang/Object;
   L0
    LINENUMBER 13 L0
    ALOAD 0
    INVOKEVIRTUAL com/qhong/basic/genericity/DateInter.getValue ()Ljava/util/Date;
    ARETURN
   L1
    LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。

虚拟机兼容方法签名相同

子类中的桥方法 Object getValue()和Date getValue()是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别

<T> VS <?>

不同点:

  • <T> 用于 泛型的定义,例如 class MyGeneric<T> {...}
  • <?> 用于 泛型的声明,即泛型的使用,例如 MyGeneric<?> g = new MyGeneric<>();

相同点:

都可以指定上界和下界,例如:
class MyGeneric<T extends Collection> {...}
class MyGeneric<T super List> {...}

MyGeneric<? extends Collection> g = new MyGeneric<>();`
 `MyGeneric<? super List> g = new MyGeneric<>();

静态方法,静态类中的泛型

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

public class Test2<T> {  
        public static T one;   //编译错误  
        public static  T show(T one){ //编译错误  
            return null;  
        }  
} 

因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

静态方法自己定义的泛型

public class Test2<T> {    
        public static <T >T show(T one){//这是正确的  
            return null;  
        }  
} 

因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。

参考:

Java 泛型,你了解类型擦除吗?

java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题

posted @ 2020-11-17 11:18  hongdada  阅读(4979)  评论(0编辑  收藏  举报