JAVA虚拟机没有泛型类型或方法的概念

编译器通过擦除类型变量将泛型类和方法转换为普通类和普通的方法。

JAVA泛型出来的比较晚,被设计的主要目标是允许泛型代码和遗留代码之间能够相互操作。也就是说java中的泛型是“伪泛型”

如何理解Java中的泛型是伪泛型?泛型中类型擦除

Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure)。

类型擦除:将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型)【E —> Object】,就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。

类型擦除原则

  • 类型参数声明 —> 被消除 例:public List<E>{...} —–> public List{...}

  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型

    • 类型参数是无限制通配符没有上下界限定则替换为Object

      例:类型声明为<E>或<?> :private E name ——> private Object name

    • 存在上下界限定则根据子类替换原则取**类型参数的最左边限定类型(即第一个)**(即父类)。

      例: 类型声明为<E extends Number> : private E age ——> private Number age

  • 为了保证类型安全,必要时插入强制类型转换代码。

  • 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”

类型擦除示例

  • 擦除类定义中的类型参数

    • 无限制类型擦除

      java-basic-generic-1

    • 有限制类型擦除

      java-basic-generic-2

    • 多个有限制类型擦除

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class Pair<T extends Comparable & Cloneable >{//替换为第一个限定类型
      private T first;
      public boolean show(T a,T b){
      return a.compareTo(b)==0;
      }
      }
      //=======================擦除之后的===============================
      class Pair{
      private Comparable first;//替换为限定类型
      public boolean show(Comparable a,Comparable b){
      return a.compareTo(b)==0;
      }
      }
    • 擦除返回类型时,编译器会插入类型强制转换

      1
      2
      3
      4
      5
      6
      //	调用时
      Pair<Integer> pair = new Pair<>();
      Integer first = pair.getFirst();
      //擦除之后
      Pair pair = new Pair();
      Integer first = (Integer) pair.getFirst();//插入一个强制类型转换
    • 参数类型不需要强制转换,因为它被擦除为Object。

      1
      2
      3
      4
      5
      Pair<String> pair1 = new Pair<>();
      pair1.setFirst("1234");
      //-------------------------------
      Pair pair1 = new Pair();
      pair1.setFirst("1234");
  • 擦除方法定义中的类型参数

    java-basic-generic-3

如何证明类型擦除

例一  原始类型相等

定义两个ArrayList

一个泛型是String,只能添加String 类型。

1
ArrayList<String> stringlist = new ArrayList<>();

一个泛型是Integer,只能添加Integer类型。

1
ArrayList<Integer> integerlist = new ArrayList<>();

比较他俩Class对象。我们会发现结果为true。

1
System.out.println(stringlist.getClass() == integerlist.getClass());//true

同一对象的Class对象是等同的,因此由此可以得出,即使泛型不一样,但他们的原始类型是相等的。

关于Class对象

​ 这个Class对象是在编译之后生成的,之所以相等,是因为泛型被擦除为了原始类型。

​ 每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。

例二  通过反射添加其它类型元素

定义一个ArrayList<Integer>,通过add()方法只能添加Integer类型的。

1
2
ArrayList<Integer> list = new ArrayList<Integer>();         
list.add(1);

但我们可以==通过反射绕过泛型检查==,进而添加其他类型的元素。

1
list.getClass().getMethod("add", Object.class).invoke(list, "asd");

这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。

为什么反射可以绕过泛型检查?

因为,泛型检查是在编译期间进行的,而反射获取的是已经编译之后。之所以能通过反射添加其他类型元素,就是因为编译之后,泛型被擦除掉了,回归成了原始类型,可以理解为等价于ArrayList<Object> list = new Arraylist<>();,可以添加任何类型的元素,因为泛型被擦除成了Object

什么是原始类型?如何理解?

原始类型 就是==擦除去了泛型信息,最后在字节码中的类型变量的真正类型==,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换

其实在泛型类中,不指定泛型的时候,也差不多,只不过这个时候的泛型为Object,就比如ArrayList中,如果不指定泛型,那么这个ArrayList可以存储任意的对象。

1
2
3
4
ArrayList arrayList = new ArrayList();
arrayList.add(1);
arrayList.add("123");
arrayList.add(12.2);

如何理解泛型的编译期检查?

当我们定义一个ArrangList,并指定泛型为String类型,然后就只能添加或者获取String类型的数据。当添加其他类型的数据时,就会报错,这是为什么呢?这是因为存在编译期检查。

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

类型擦除后,原始类型为Object,但不允许任意引用类型添加,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

类型检查针对谁?

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

例一:

1
2
3
ArrayList<String> list1 = new ArrayList(); //有警告
list1.add("123");
list1.add(123);//报错!

类型检查针对ArrayList<String>这个引用。当调用这个引用的方法add(),就根据<String>这个类型进行检查。

例二:

1
2
3
ArrayList list2 = new ArrayList<String>(); //有警告
list2.add("123");
list2.add(2);

以上没有报错,类型检查针对ArrayList,当调用方法add()时,由于没有指定泛型,所以使用Object进行检查。

泛型中参数化类型不考虑继承关系

情况一ArrayList<String> list1 = new ArrayList<Object>();

1
2
3
4
ArrayList<Object> list1 = new ArrayList<Object>();  
list1.add(new Object());
list1.add(new Object());
ArrayList<String> list2 = list1; //编译错误

我们假设以上编译成功····

list.get()获取的是String类型。但我们之前添加的是Object类型,我们知道,Java中对象直接向下转型的【只有向上转型过的对象才能向下转型】。

这样就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(也违背了设计的初衷)

情况二ArrayList<Object> list2 = new ArrayList<String>();

1
2
3
4
5
ArrayList<String> list1 = new ArrayList<String>();  
list1.add(new String());
list1.add(new String());

ArrayList<Object> list2 = list1; //编译错误

这样的情况比第一种情况好的多,最起码,在我们用list2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢,泛型出现的原因,就是为了解决类型转换的问题。

我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。再说,你如果又用list2往里面add()新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String类型的,还是Object类型的呢?

如何理解泛型的多态【桥方法】

为什么类型擦除会造成多态的冲突?

现在有这样一个泛型类:

1
2
3
4
5
6
7
8
9
10
11
12
class Pair<T> {  

private T value;

public T getValue() {
return value;
}

public void setValue(T value) {
this.value = value;
}
}

然后定义一个类继承这个泛型类

1
2
3
4
5
6
7
8
9
10
11
12
class DateInter extends Pair<Date> {  

@Override //重写覆盖
public void setValue(Date value) {
super.setValue(value);
}

@Override //重写覆盖
public Date getValue() {
return super.getValue();
}
}

现在,有这样的一个事实,类DateInter继承了一个泛型类Pair<T>

类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

image-20220811144206244

让我们回忆一下什么是重写,什么是重载

重写:指子类实现了一个与父类在方法声明上完全相同的一个方法。

重载:存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。

setValue方法,在父类类型擦除之后,还算是重写吗?

父类的参数类型是Object,子类的参数类型是Date

这根本不是重写,而是重载。

我们的本意是:进行重写,实现多态。

但是事实是:类型擦除后,只能变为了重载。

这样,类型擦除就和多态有了冲突。

JVM如何解决类型擦除和多态的冲突?

JVM为了解决类型擦除和多态的冲突,采用了特殊的方法:==桥方法==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Pair<T> {  

private T value;

public T getValue() {
return value;
}

public void setValue(T value) {
this.value = value;
}
}
class DateInter extends Pair<Date> {

@Override
public void setValue(Date value) {
super.setValue(value);
}

@Override
public Date getValue() {
return super.getValue();
}
}

以上,代码经过编译后,编译器会自动给我们生成桥方法Pair<T>经过泛型擦除后变成了原始类型,因为不做展示,让我们来看看子类DateInter,经过编译后是什么样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DateInter extends Pair<Date> {  


public void setValue(Date value) {
...
}


public Date getValue() {
...
}
//以下两个方法就是编译器给我们生成的“桥方法”。
public void setValue(Object value) {
setValue(value);
}


public Object getValue() {
return getValue();
}
}

我们可以看到,原本两个方法,编译之后成了四个方法,这多出来的方法就是桥方法,这个桥方法,不就相当于重写了父类的方法吗,实现了多态。

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

但是还是有一个问题:

image-20220811151617944

当我们自己写代码时,两个方法的方法签名是一样的。这是不允许的,编译器无法通过,但为了解决类型擦除和多态的冲突,编译器却生成了他不允许的方法。

但实际上,JVM 会用方法名、参数类型和返回类型来确定一个方法,所以针对方法签名相同的两个方法,返回值类型不相同的时候,JVM是能分辨的。

所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。

方法签名:方法名+方法参数列表【重载过程中,使用方法签名可以唯一确定一个方法】

还有一点需要说明的是

这里面的setValuegetValue这两个桥方法的意义又有不同。

  • setValue方法是为了解决类型擦除与多态之间的冲突。

  • getValue却有普遍的意义

    如果这是一个普通的继承关系,那么父类的getValue方法如下:

    1
    2
    3
    public Object getValue() {  
    return super.getValue();
    }

    而子类重写的方法是:

    1
    2
    3
    public Date getValue() {  
    return super.getValue();
    }

    其实这在普通的类继承中也是普遍存在的重写,这就是协变

参考:

__END__