Java——擦除

直接代码分析一波:

import java.util.*;

public class Ex12 {
    public static void main(String[] args) {
        Class c1 = new Double(0).getClass();
        Class c2 = new Integer(0).getClass();
        Class c3 = new ArrayList<String>().getClass();
        Class c4 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
        System.out.println(c3 == c4);
    }
}

这里的c1 c2 c3 c4将分别得到Double、Integer、ArrayList<String>、ArrayList<Integer>的类,输出是:

false
true

c1 == c2很明显是不一样的,这里的ArrayList<String>和ArrayList<Integer>很容易认为是不同的类型,但是其实他两是相同类型的,

import java.util.*;

class test1<T>{}
class test2<T,Q>{}
public class Ex12 {
    public static void main(String[] args) {
        test1<Double> t1 = new test1<Double>();
        test2<Double, Double> t2 = new test2<Double, Double>();
        System.out.println(Arrays.toString(t1.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(t2.getClass().getTypeParameters()));
    }
}

Class.getTypeParameters()将返回一个泛型声明所声明的类型参数数组,但是只能看到T,Q,得不到Double,这是因为在java泛型是使用擦除来实现的,这就意味这当你在使用泛型的时候,任何具体点的类型信息都被擦除了,唯一知道的就是你在使用一个对象,因此List<String>和List<Integer>是相同的类型。

1、边界

class HasF{
    public void f() {System.out.println("f()");}
}
class Ma<T>{
    T obj;
    Ma(T t){ obj = t;}
    public void maf(){obj.f();}//这里是错的
}

假如你有一个HasF类,这个类中有一个f()方法,然后你想创建一个泛型类,并且想将obj的f()方法,这是java编译器无法实现的,因为擦除。为了调用f(),我们必须协助泛型类,给定泛型的边界,以此告知编译器只能接受遵循这个边界的类型。

用extends关键字,就能设置边界。

import java.util.*;

class HasF{
    public void f() {System.out.println("f()");}
}
class Ma<T extends HasF >{
    T obj;
    Ma(T t){ obj = t;}
    public void maf(){obj.f();}//这里是错的
}
public class Ex12 {
    public static void main(String[] args) {
        HasF hasf = new HasF();
        Ma<HasF> ma = new Ma<HasF>(hasf);
        ma.maf();
    }
}

边界<T extends HasF >声明T必须具有类型HasF或者从HasF导出的类型。

泛型类型参数将擦除到它的第一个边界,实际上编译器会把类型参数替换为它的擦除,上面的示例就是T擦除到HasF。

2、擦除的问题

泛型不能显式的引用运行时类型的操作中,例如转型、instanceof操作和new表达式,因为所有关于参数的类型信息都丢失了。例如:

class Foo<T>{
    T var;
}
class Cat{}

定义这样两个类,Foo是一个泛型类,Cat是一个普通类

Foo<Cat> foo = new Foo<Cat>();

这里看起来class Foo中的所有操作都像是使用Cat来操作,泛型好像也在告诉我们所有的类型T都将会被替换,但事实上我们必须提醒自己,被擦除了,它只是一个Object,也就是说这里没有边界,应该说边界就是Object。

import java.lang.reflect.Array;
import java.util.Arrays;

class ArrayMaker<T>{
    private Class<T> kind;
    ArrayMaker(Class<T> kind){ this.kind = kind;}
    T[] create(int size) {
        return (T[])Array.newInstance(kind, size);//这里会有警告
    }
}

public class Ex13 {
    public static void main(String[] args) throws Exception{
        ArrayMaker<String> stringMaker =
                new ArrayMaker<String>(String.class);
        String[] stringArray = stringMaker.create(9);
        System.out.println(Arrays.toString(stringArray));
    }    
}

输出:

[null, null, null, null, null, null, null, null, null]

即使这里的泛型被指定为Class<T>擦除就意味这kind实际被储存的是Class,由此在使用的时候,Array.newInstance(kind, size)并为拥有kind的类型信息,就是不知道kind是String.class,所以需要转型。

但是在创建一个容器情况稍有不同

public class Ex13<T> {
    List<T> create(){return new ArrayList<T>();}
    public static void main(String[] args) throws Exception{
        Ex13<String> ex = new Ex13<String>();
        List<String> stringList = ex.create();
    }    
}

这里的编译器 不会给出任何的警告,尽管我们知道create中的<T>被移除了。这样就能这样操作:

public class Ex13<T> {
    List<T> create(T t , int n){
        List<T> result = new ArrayList<T> ();
        for(int i = 0; i < n; i++) 
            result.add(t);
        return result;
        }
    public static void main(String[] args) throws Exception{
        Ex13<String> ex = new Ex13<String>();
        List<String> stringList = ex.create("hello", 4);
        System.out.println(stringList);
    }    
}

输出:

[hello, hello, hello, hello]

但是数组就不能这样做:

public class Ex13<T> {
    T[] create(T t , int n){
        T[] result = new T[n];//这里编译器无法编译
        for(int i = 0; i < n; i++) 
            result[i] = t;
        return result;
        }
    
    public static void main(String[] args) throws Exception{
        Ex13<String> ex = new Ex13<String>();
        List<String> stringList = ex.create("hello", 4);
        System.out.println(stringList);
    }    
}

因为T的类型被擦除了。一般都使用Array.newInstance()方法或者使用List。

3、擦除的补偿

public class Ex13<T> {
    public void f(Object o) {
        if(o instanceof T) {}//编译出错
        T var = new T();//编译出错
        T[] ts = new T[10];//编译出错
        T[] tss = (T)new Object[10];//编译出错
    }    
}

因为擦除使泛型代码失去某些执行某些操作的能力,想要绕过这些问题进行编程需要引入类型标签对擦除进行补偿。

  • instanceof的使用失败 可以使用动态的ISinstance)  
public class Ex13<T> {
    Class<T> kind;
    Ex13(Class<T> kind){
        this.kind = kind;
    }
    public boolean f(Object o) {
        return kind.isInstance(o);
    }
    public static void main(String[] args) {
        Ex13<String> ex = new Ex13<String>(String.class);
        System.out.println(ex.f("233"));
        System.out.println(ex.f(233));
    }
}

输出:

true
false
  • T var = new T();创建类型实例无法实现,一部分原因是因为擦除了,另一部分原因是因为编译器不能验证T具有默认(无参)的构造器,解决方法还是同样的使用Class对象。
public class Ex13<T> {
    Class<T> kind;
    Ex13(Class<T> kind){
        this.kind = kind;
    }
    public T f() {
        try {
            return kind.newInstance();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }        
    }
    public static void main(String[] args) {
        String a = new Ex13<String>(String.class).f();
    }
}

调用用Class的newInstance()方法即可,但是如果在main中这样使用

    public static void main(String[] args) {
        Integer b = new Ex13<Integer>(Integer.class).f();
    }

就会失败,因为Integer是没有任何的默认的构造器。

可以使用显式的工厂,并限制其类型,使得只能接受实现这个类。

interface Factoryi<T>{
    T create();
}

先创建一个泛型接口,create()方法将返回一个T类对象。

可以

class IntegerFactory implements Factoryi<Integer>{

    public Integer create() {
        return new Integer(0);
    }
    
}

可以直接实现这个接口。

class Widest{
    public static class Factory implements Factoryi<Widest>{
        public Widest create() {
            return new Widest();
        }
    }
}

可以在这个类中创建一个内部类去实现这个接口,

class Foo2<T>{
    private T x;
    public <F extends Factoryi<T>> Foo2(F factory){
        x = factory.create();
    }
}

这就是实际上的泛型类了,因为Integer和Widest都没有任何默认的构造器,所以就创建一个类实现Factoryi<T>接口,实现其中的create()方法然后在泛型类从使用这个Factoryi<T>作为边界,使用其中的create()方法,从而实现在泛型类从创建泛型类型实例。

  • 泛型数组

  不能创建泛型数组,一般的解决方案是使用Araayist:

public class Ex13<T> {
    public List<T> array = new ArrayList<T>();
    public void add(T item) {
        array.add(item);
    }
    public T get(int index) {
        return array.get(index);
    }
}

这样和数组其实也能实现和数组差不多的功能。

还能通过传入Class类型对象,然后使用Array.newInstance()方法创建一个数组

class ArrayMaker<T>{
    private Class<T> kind;
    ArrayMaker(Class<T> kind){ this.kind = kind;}
    T[] create(int size) {
        return (T[])Array.newInstance(kind, size);//这里会有警告
    }
}

public class Ex13 {
    public static void main(String[] args) {
        String[] s = new  ArrayMaker<String>(String.class).create(10);
    }
}

5、通配符

先看一个数组的特殊行为:

class Fruit{}
class Apple extends Fruit{}
class Orange extends Fruit{}
class Jon extends Apple{}
public class Ex14 {
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple();
        fruit[1] = new Jon();
        try {
        fruit[2] = new Fruit();
        }catch(Exception e) {
            System.out.println(e.toString());
        }
        try {
        fruit[3] = new Orange();
        }catch(Exception e) {
            System.out.println(e.toString());
        }
    }
}

这里的输出是:

java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange

这里创建了一个Apple数组,并将其赋值给一个Fruit数组。你可以将一个Apple或者Apple的子类对象放入数组,并不会有警告,和错误。

但是当你想要放入一个Fruit或是Orange对象的时候,编译器不会有警告但是运行的时候却能捕获到java.lang.ArrayStoreException异常,编译器不会给出警告是因为数组有一个Fruit[]引用,但是运行的时候数组机制知道这是一个Apple[]数组,所以就会抛出异常 。

类似的当我们试图使用泛型容器来代替数组的时候:

public class Ex14 {
    public static void main(String[] args) {
        List<Fruit> flist = new ArrayList<Apple>();
    }
}

这时编译器会直接给出警告,讲道理Apple是Fruit的子类,这应该属于向上转型,然而实际上这里并不是根本不是向上转型这里是两个List,Apple可以算是Fruit类型的,但是Apple的List并不是Fruit的List,这时如果想要建立某种类型的向上转型关系,就需要使用通配符了。

public class Ex14 {
    public static void main(String[] args) {
        List<? extends Fruit> flist = new ArrayList<Apple>();
        flist.add(new Orange());
        flist.add(new Apple());
        flist.add(new Jon());
    }
}

然后创建List就不会报错了,但是后面的调用add()方法还是会给出警告。

flist的类型现在是List<? extends Fruit>,可以将其读为“具有任何从Fruit继承的类型的列表”,但是并不就意味着这个List就可以持有任何类型的Fruit。通配符指定的是没有指定具体类型,其实就是只能接受不能修改

public class Ex14 {
    public static void main(String[] args) {
        List<Apple> list = new ArrayList<Apple>();
        list.add(new Apple());
        list.add(new Jon());
        List<? extends Fruit> flist = list;
    }
}

这样的使用就是没错的。