Java泛型进阶

原文链接

1.在泛型代码内部,无法获得任何有关泛型参数类型的信息。

“在泛型代码内部,无法获得任何有关泛型参数类型的信息”,这并不是说使用泛型代码时给定的类型参数会消失(要不然泛型就没有存在的意义了,外部将T定为比如String类型后,之后的T就被定为String类型了),而是说我们只知道这是个泛型类型,可以代表多种类型,即我们不能通过T访问与某种特定类有关的方法,属性等(当然,有泛型边界的是个例外)。

2.关于擦除:擦除:List<T>被擦除为List,T被擦除为Object

//这个例子表明编译过程中并没有根据参数生成新的类型
public class Main2 {
    public static void main(String[] args) {
        Class c1 = new ArrayList<Integer>().getClass();
        Class c2 = new ArrayList<String>().getClass();
        System.out.print(c1 == c2);
    }
}
/* output
true
*/

List<String> 中添加 Integer 将不会通过编译,但是List<Sring>List<Integer>在运行时的确是同一种类型。

//例子, 这个例子表明类的参数类型跟传进去的类型没有关系,泛型参数只是`占位符`
public class Table {
}
public class Room {
}
public class House<Q> {
}
public class Particle<POSITION, MOMENTUM> {
}
public class Main {
    public static void main(String[] args) {
        List<Table> tableList = new ArrayList<Table>();
        Map<Room, Table> maps = new HashMap<Room, Table>();
        House<Room> house = new House<Room>();
        Particle<Long, Double> particle = new Particle<Long, Double>();
        System.out.println(Arrays.toString(tableList.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(maps.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(house.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
    }
}
/** output
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
*/

我们在运行期试图获取一个已经声明的类的类型参数,发现这些参数依旧是‘形参’,并没有随声明改变。也就是说在运行期,我们是拿不到已经声明的类型的任何信息。(只是被擦除,让你编写代码时不知道泛型T是什么,但编写完了后使用这个泛型类或泛型方法时一旦将T具体化了后,就能知道了)

只要涉及到List<T>,不管是编写泛型类,还是使用泛型类都会被擦除,所以看下面的图:List<Shape>被擦除为List,尽管知道这是Shape类型,但是就能用s.draw(不能知道详细类型信息)(加个泛型边界可以解决这个错误,因为加了后就是被擦除为泛型边界而不是Object)


编译器会虽然在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。

List<String> stringList=new ArrayList<String>();
//可以通过编译
stringList.add("wakaka");
//编译不通过
//stringList.add(new Integer(0));

//List.java
public interface List<E> extends Collection<E> {
//...
boolean add(E e);
//...
}

List的参数类型是Eadd方法的参数类型也是E,他们在类的内部是一致的,所以添加Integer类型的对象到stringList违反了内部类型一致,不能通过编译。

重用 extends 关键字。通过它能给与参数类型添加一个边界。

泛型参数将会被擦除到它的第一个边界(边界可以有多个)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object。下面的例子中,可以把泛型参数T当作HasF类型来使用。

// HasF.java
public interface HasF {
    void f();
}

//Manipulator.java
public class Manipulator<T extends HasF> {
    T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
}
extend关键字后后面的类型信息决定了泛型参数能保留的信息。

补充:Java擦除的基本原理:

刚看到这里可能有些困惑,一个泛型类型没有保留具体声明的类型的信息,那它是怎么工作的呢?在把《Java编程思想》书中这里的边界与上文的边界区分开来之后,终于想通了。Java的泛型类的确只有一份字节码,但是在使用泛型类的时候编译器做了特殊的处理。

这里根据作者的思路,自己动手写了两个类SimpleHolderGenericHolder,然后编译拿到两个类的字节码,直接贴在这里:

// SimpleHolder.java
public class SimpleHolder {
    private Object obj;
    public Object getObj() {
        return obj;
    }
    public void setObj(Object obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.setObj("Item");
        String s = (String) holder.getObj();
    }
}
// SimpleHolder.class
public class SimpleHolder {
  public SimpleHolder();
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return        

  public java.lang.Object getObj();
    Code:
       0: aload_0       
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn       

  public void setObj(java.lang.Object);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return        

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class SimpleHolder
       3: dup           
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1      
       8: aload_1       
       9: ldc           #5                  // String Item
      11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
      14: aload_1       
      15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2      
      22: return        
}

//GenericHolder.java
public class GenericHolder<T> {
    T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.setObj("Item");
        String s = holder.getObj();
    }
}

//GenericHolder.class
public class GenericHolder<T> {
  T obj;

  public GenericHolder();
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return        

  public T getObj();
    Code:
       0: aload_0       
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn       

  public void setObj(T);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return        

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class GenericHolder
       3: dup           
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1      
       8: aload_1       
       9: ldc           #5                  // String Item
      11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
      14: aload_1       
      15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2      
      22: return        
}

经过一番比较之后,发现两分源码虽然不同,但是对应的字节码逻辑部分确是完全相同的。

在编译过程中,类型变量的信息是能拿到的。所以,set方法在编译器可以做类型检查,非法类型不能通过编译。但是对于get方法,由于擦除机制,运行时的实际引用类型为Object类型。为了‘还原’返回结果的类型,编译器在get之后添加了类型转换。所以,在GenericHolder.class文件main方法主体第18行有一处类型转换的逻辑。它是编译器自动帮我们加进去的。

所以在泛型类对象读取和写入的位置为我们做了处理,为代码添加约

3.擦除的缺陷

泛型类型不能显式地运用在运行时类型的操作当中,例如:转型、instanceofnew。因为在运行时,所有参数的类型信息都丢失了。(例如下面的代码中,编译不通过是因为都规定了在编写泛型代码时不让你知道T的类型信息,你还想用instanceof T去判断,所以编译会报错)

public class Erased<T> {
    private final int SIZE = 100;
    public static void f(Object arg) {
        //编译不通过
        if (arg instanceof T) {
        }
        //编译不通过
        T var = new T();
        //编译不通过
        T[] array = new T[SIZE];
        //编译不通过
        T[] array = (T) new Object[SIZE];
    }
}

4.擦除的补偿

①类型判断问题:

class Building {}
class House extends Building {}
public class ClassTypeCapture<T> {
    Class<T> kind;
    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }
    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }
    public static void main(String[] args) {
        ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
        System.out.println(ctt2.f(new Building()));
        System.out.print(ctt2.f(new House()));
    }
}
//output
//true
//true
//false
//true

泛型参数的类型无法用instanceof关键字来做判断。所以我们使用类类型来构造一个类型判断器,判断一个实例是否为特定的类型。

②创建类型实例:

Erased.java中不能new T()的原因有两个,一是因为擦除,不能确定类型;而是无法确定T是否包含无参构造函数。

为了避免这两个问题,我们使用显式的工厂模式:

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

class Foo2<T> {
    private T x;

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

class IntegerFactory implements IFactory<Integer> {
    @Override
    public Integer create() {
        return new Integer(0);
    }
}

class Widget {
    public static class Factory implements IFactory<Widget> {
        @Override
        public Widget create() {
            return new Widget();
        }
    }
}

public class FactoryConstraint {
    public static void main(String[] args) {
        new Foo2<Integer>(new IntegerFactory());
        new Foo2<Widget>(new Widget.Factory());
    }
}
通过特定的工厂类实现特定的类型能够解决实例化类型参数的需求。

③创建泛型数组:

一般不建议创建泛型数组。尽量使用ArrayList来代替泛型数组。但是在这里还是给出一种创建泛型数组的方法。

public class GenericArrayWithTypeToken<T> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int sz) {
        array = (T[]) Array.newInstance(type, sz);
    }

    public void put(int index, T item) {
        array[index] = item;
    }

    public T[] rep() {
        return array;
    }

    public static void main(String[] args) {
        GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class, 10);
        Integer[] ia = gai.rep();
    }
}

这里我们使用的还是传参数类型,利用类型的newInstance方法创建实例的方式。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值