Java的类型擦除与泛型的关系

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

在讨论类型擦除之前我们必须先来了解一下java的泛型。所谓的泛型就是参数化的类型。这就意思着我们可以具体的类型作为一个参数传递给方法、类、接口。

为什么我们需要泛型呢首先我们都知道在java里Object就是对象的父类。Object可以引用任何类型的对象。但是这一点会带来类型安全的问题。而泛型的出现就给java带来了类型安全这一项功能

  • 泛型方法
class Test {
	static <T> void helloworld(T t){}
}
  • 泛型类
 
class Test<T> {
	T obj
	Test(T obj){
		this.obj = obj;
	}
}
  • 泛型接口
interface Test<T> {
	T getData();
}

使用泛型可以提供代码的复用使用一份代码应用到不同的类型上。其次泛型还保证了类型的安全在编译期就可以检查出来。比如说

ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world");

// 编译器会阻止下面的操作从而保证了我们的类型安全
list.add(100); // error

泛型还有一个好处不需要单独的类型转换如

ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world");


String s0 = (String)list.get(0); // 类型转换是不需要做的
// 下面展示了我们不需要进行单独的类型转换
String s1 = list.get(0);
String s2 = list.get(1);

前面已经做好铺垫了我们是时候聊类型擦除的问题了。

什么是类型擦除所谓的类型擦除是一个只在编译期强制类型约束和在运行期丢弃类型信息的过程。如我们现在有这么一个泛型方法

public static <T> boolean myequal(T t1, T t2){
	return t1.equals(t2);
}

编译器会用Object替换掉类型T如下

public static <Object> boolean myequal(Object t1, Object t2){
	return t1.equals(t2);
}

泛型类的类型擦除

在类级别的类型擦除遵循这样的规则首先编译器丢弃类上的类型参数并用它的第一个绑定类型替换它如果类型参数没有绑定就用Object来替换。

  • 参数类型没有绑定
public class MyClass<T> {
	private T[] elements;
	public void doSomething(T element){}
	public T getSomething(){}
}

MyClass的类型参数T没有绑定到任何类型所以将会用Object来替换掉T替换结果

public class MyClass {
	private Object[] elements;
	public void doSomething(Object element){}
	public Object getSomething(){}
}
  • 参数类型有绑定
interface MyT {}
public class MyClass<T extends MyT> {
	private E[] elements;
	public void doSomething(T element){}
	public T getSomething(){}
}

MyTClass是MyClass的类型参数T第一个绑定到的类型因此T将会被替换成MyTClass

public class MyClass {
	private MyT[] elements;
	public void doSomething(MyT element){}
	public MyT getSomething(){}
}

为什么取第一个绑定就OK了呢比如说如果MyT还有父类父类还有父类那么我们的类型参数就有了很多间接的绑定而第一个绑定就覆盖了所有的父类因此用第一个绑定就可以了。

泛型方法的类型擦除

对于泛型方法它的类型参数不会被存放起来它遵循这样的规则首先编译器丢弃方法上的类型参数并用它的第一个绑定类型替换它如果类型参数没有绑定就用Object来替换。

  • 参数类型没有绑定
public static <T> void printSomething(T[] arr){
	for(T item: arr) {
		System.out.printf("%s", item);
	}
}

上面的方法进行类型擦除的结果后

public static void printSomething(Object[] arr){
	for(Object item: arr) {
		System.out.printf("%s", item);
	}
}
  • 参数类型有绑定
public static <T extends MyT> void printSomething(T[] arr){
	for(T item: arr) {
		System.out.printf("%s", item);
	}
}

上面的方法进行类型擦除的结果后

public static void printSomething(MyT[] arr){
	for(MyT item: arr) {
		System.out.printf("%s", item);
	}
}

类型擦除中产生的桥接方法

除了上述的规则外对于那些相似的方法编译器会创建一些合成方法来区分它们这个合成方法是扩展相同的第一个绑定类的方法签名这句话通过下面的例子就会有一个比较直观的认识。
首先我们有这么一个类

class MyQueue<T> {
    private T[] elements;

    public MyQueue(int size){
        this.elements = (T[])new Object[size];
    }

    public void add(T data){}
    public T dequeue(){
        if(elements.length > 0){
            return elements[0];
        } else {
            return null;
        }
    }
}

上面这个类在类型擦除后T都会被Object替换具体规则请参考前面部分。我们现在写一个类来继承MyQueue

class IntegerQueue extends MyQueue<Integer> {

    public IntegerQueue(int size){
        super(size);
    }

    @Override
    public void add(Integer data) {
        super.add(data);
    }
}

接着我们写一个测试方法来引述出它的合成方法的原理
请添加图片描述

我们可以看到MyQueue的add方法的参数类型已变成Object所以queue.add("Helllo") 是说得过去。IDE也提示了这个类型可能有问题请添加图片描述
其实这也并不是我们想要的因为IntegerQueue只想接收Int类型的。那么当我们运行这个测试用例时我们就看到了下面的错误请添加图片描述
这个例子再次说明泛型是为了类型安全而引入的功能。编译器是怎么做到的呢它是怎么起作用的呢

实际上是编译器额外生成了一个方法前面提到的合成方法来做桥接。我们都知道MyQueue进行类型擦除后会变成下面这样

class MyQueue {
    private Object[] elements;

    public MyQueue(int size){
        this.elements = (Object[])new Object[size];
    }
    public void add(Object data){}
    public Object dequeue(){
        if(elements.length > 0){
            return elements[0];
        } else {
            return null;
        }
    }
}

IntegerQueue 的方法public void add(Integer data){}MyQueue public void add(Object data){}是相似的编译器就会为相似的方法创建一个中间的方法来做它们之间的桥。为了保证泛型的多态性在类型擦除这个方法是生成在IntegerQueue 而且编译器能够保证这种相似的方法不会匹配错也就是编译都会这种相似的方法创建一个合成方法在它们之间做桥接。如何上面提到的编译器创建的桥接方法如下

 static class IntegerQueue extends MyQueue<Integer> {


        public void add(Object data){
            add((Integer) data);
        }
        
        public void add(Integer data) {
            super.add(data);
        }
    }

本例子中的这个合成方法就是

public void add(Object data){
       add((Integer) data);
}

它是扩展了相同的第一个绑定类的方法签名换句话说就是它们的方法名是一样的而这个合成方法的参数的类型是类型擦除后的第一个绑定类具体可以参考前面部分的内容。它的作用就是桥接了IntegerQueue类中的add方法与其父类中的add方法以此来解决泛型在继承中的类型安全问题。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java