龙空技术网

互联网大厂面试-什么是Java语言中的类型擦除?

架构师面试宝典 24195

前言:

如今大家对“java是类型语言”大体比较注重,大家都想要分析一些“java是类型语言”的相关知识。那么小编同时在网上收集了一些关于“java是类型语言””的相关资讯,希望同学们能喜欢,咱们快快来了解一下吧!

什么是泛型?

首先来讲泛型操作是从JDK5开始引入Java语言的一个新特性,其允许在定义类或者是接口的时候使用一个类型参数,并且这个类型参数可以在使用的时候被替换成一个具体的参数。使用泛型最大的好处就是可以极大的提升代码的复用性。

以List为例,我们可以在List集合中放入任何的数据类型例如String、Integer等等,当然也可以放入对象类型。如果不使用泛型那么就会出现想要使用String类型的List集合的时候需要定义一个String类型的List集合,需要使用Integer类型的List的集合的时候就需要定义相应类型的集合,这样做就有点复杂了,为了解决这样的问题,就出现了我们的泛型操作。

各种语言对泛型操作的处理机制?

通常情况下,编译器需要通过如下的几种方式来对泛型数据类型进行处理。

在实例化一个泛型类或者是一个泛型类型的方法的时候,对于每一种数据类型都产生一份新的代码来进行具体的支撑,例如针对List的泛型操作,如果使用了String类型,就生成一份String类型的List代码,使用Integer就生成一份Integer的List代码。对于每个使用泛型操作的类或者方法来讲生成的代码都是唯一的一份,并且将所有的类型通过映射都映射到泛型上,在需要具体使用的时候对类型进行判断进行转换,使得泛型能够正常的操作。

在C++语言中的泛型就是通过第一种方式来实现的,也就是说C++语言的编译器会为每个泛型都生成一份对应的执行代码,在代码中String类型与Integer类型对应的泛型类是不一样的。而这样带来的后果就是在编译后的代码中会存在大量的冗余代码。

在C#语言中泛型操作无论是在源码中还是在编译后的中间代码中或者是在运行时期的代码中都是存在不同类型的数据代码,唯一不同的是这些代码是在运行期间生成的,并且有自己的虚地址表示,这种方式被称为是类型膨胀。而基于这种方式所实现的泛型操作被称为是真实泛型。

在Java语言中的泛型与前面两者是不一样的,它只在源代码中存在,在编译之后的字节码中就会被替换成原生类,并且在相应的代码中加入了强制类型转换。所以对于Java语言而言,ArrayList<String>和ArrayList<Integer>其实就是同一个类型,对于Java来讲泛型操作可以称之为Java语言的优势,而在Java语言中使用到的对泛型的操作则是被称为是类型擦除,而基于这种方法实现的泛型操作被称为是伪泛型。

C++和C#是使用Code specialization的处理机制,前面提到,他有一个缺点,那就是会导致代码膨胀。另外一个弊端是在引用类型系统中,浪费空间,因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用Code sharing方式处理泛型的主要原因。

Java编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类型实例映射到唯一的字节码表示是通过类型擦除(type erasure)实现的。

什么是Java语言的类型擦除?

什么是类型擦除?类型擦除是指通过类型参数组合的方式,将泛型类型的实例对象关联到同一份字节码文件上。编译器最终只会为泛型类型提供一份统一的字节码,并且将相关的实例对象关联到这一份字节码文件上。

类型擦除的关键就是在于从泛型类型中清除类型参数的信息,并且在使用的时候需要对传入的真实参数进行类型检查和类型转换。可以简单的理解为将泛型类型的代码转换成了普通类型的代码,只不过对于Java编译器来讲,是将对应的字节码文件进行了转换。

类型擦除器过程主要分为如下两个步骤

将所有的泛型有关的参数用最顶级的父类进行替换移除所有的具体类型相关的参数Java语言泛型处理过程

首先来看一段简单的代码

public static void main(String[] args) {      Map<String, String> map = new HashMap<String, String>();      map.put("name", "nihui");      map.put("age", "22");      System.out.println(map.get("name"));      System.out.println(map.get("age"));  }  

经过反编译之后的结果如下

public static void main(String[] args) {      Map map = new HashMap();      map.put("name", "nihui");      map.put("age", "22");     System.out.println((String) map.get("name"));      System.out.println((String) map.get("age"));  }  

从反编译的结果来看,我们对于KV键值对的处理都已经不存在了,只留下了我们想要实现的部分的代码,就是直接进行类型的存储。

interface Comparable<A> {    public int compareTo(A that);}public final class NumericValue implements Comparable<NumericValue> {    private byte value;    public NumericValue(byte value) {        this.value = value;    }    public byte getValue() {        return value;    }    public int compareTo(NumericValue that) {        return this.value - that.value;    }}

反编译之后的结果

 interface Comparable {  public int compareTo( Object that);} public final class NumericValue    implements Comparable{    public NumericValue(byte value)    {        this.value = value;    }    public byte getValue()    {        return value;    }    public int compareTo(NumericValue that)    {        return value - that.value;    }    public volatile int compareTo(Object obj)    {        return compareTo((NumericValue)obj);    }    private byte value;}

编译之前的代码

public class Collections {    public static <A extends Comparable<A>> A max(Collection<A> xs) {        Iterator<A> xi = xs.iterator();        A w = xi.next();        while (xi.hasNext()) {            A x = xi.next();            if (w.compareTo(x) < 0)                w = x;        }        return w;    }}

反编译之后的代码

public class Collections{    public Collections()    {    }    public static Comparable max(Collection xs)    {        Iterator xi = xs.iterator();        Comparable w = (Comparable)xi.next();        while(xi.hasNext())        {            Comparable x = (Comparable)xi.next();            if(w.compareTo(x) < 0)                w = x;        }        return w;    }}

第2个泛型类Comparable <A>擦除后 A被替换为最左边界Object。Comparable<NumericValue>的类型参数NumericValue被擦除掉,但是这直 接导致NumericValue没有实现接口Comparable的compareTo(Object that)方法,

于是编译器在其中添加了一个桥接方法。 从第3个示例中限定了类型参数的边界<A extends Comparable<A>>A,A必须为Comparable<A>的子类,按照类型擦除的过程,先讲所有的类型参数 ti换为最左边界Comparable<A>,然后去掉参数类型A,得到最终的擦除后结果。

泛型带来的问题有哪些?

一、泛型方法的重载

public class GenericTypes {      public static void method(List<String> list) {          System.out.println("invoke method(List<String> list)");      }      public static void method(List<Integer> list) {          System.out.println("invoke method(List<Integer> list)");      }  }  

从上面这段代码中我们可以看到两个方法由于参数类型不同形成了方法的重载,但是这段代码在实际编译的过程中是没法通过的,因为在之前的内容中我们提到过List<String>和List<Integer>在Java语言来看由于类型擦除会将其认定为同一个类型,这样就会变成两个参数一样并且方法名一样的方法,这个在Java操作中是不被允许的。

二、遇到Catch

我们都知道在Java语言中会经常遇到异常捕获,如果我们在开发过程中自定义了一个通用的异常处理,那么就会有各种各样的异常被通用的异常处理捕获,这个时候如果想要实现不同的异常进行不同的处理那就非常难了

三、当泛型类型包含静态变量

public class StaticTest{    public static void main(String[] args){        GT<Integer> gti = new GT<Integer>();        gti.var=1;        GT<String> gts = new GT<String>();        gts.var=2;        System.out.println(gti.var);    }}class GT<T>{    public static int var=0;    public void nothing(T x){}}

如上面代码所示,如果在泛型中遇到的静态变量,会出现什么样的结果呢?我们都知道静态变量是与类的存在是有关的,也就是说对于一个泛型如果与到了一个静态变量,那么这个静态变量就是对于所有的泛型类所共享的。而上面的代码执行结果也就显而易见了。输出的结果应该是2。因为对于第一个操作来讲,将泛型中的静态变量改成了1,但是由于静态变量是共享的,在第二个操作中就会将对应的值改成2,所以最终的输出结果应该是2。

总结

综上所述,在虚拟机中没有泛型的概念,只有普通类与普通方法的概念,所有泛型类型在编译之后都会被进行类型擦除操作,也就是说泛型并没有自己独立存在的对象进行存储,也就不会有反射等一些高级操作。在使用泛型操作的时候一定要提前的去对类型做处理让编译器提前就知道需要处理的类型的是什么。这样可以在提升编译器处理效率的同时也可以避免带来各种各样的问题。

标签: #java是类型语言