zoukankan      html  css  js  c++  java
  • Java 泛型进阶

    擦除

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

    例子1:

    //这个例子表明编译过程中并没有根据参数生成新的类型
    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>在运行时的确是同一种类型。

    例子2:

    //例子, 这个例子表明类的参数类型跟传进去的类型没有关系,泛型参数只是`占位符`
    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]
    */

    我们在运行期试图获取一个已经声明的类的类型参数,发现这些参数依旧是‘形参’,并没有随声明改变。也就是说在运行期,我们是拿不到已经声明的类型的任何信息。编译器会虽然在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。

    例子:

    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的参数类型是E,add方法的参数类型也是E,他们在类的内部是一致的,所以添加Integer类型的对象到stringList违反了内部类型一致,不能通过编译。

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

    例子:

    /** * Created by yxf on 16-5-28. */
    // 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的泛型类的确只有一份字节码,但是在使用泛型类的时候编译器做了特殊的处理。

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

    // 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行有一处类型转换的逻辑。它是编译器自动帮我们加进去的。所以在泛型类对象读取和写入的位置为我们做了处理,为代码添加约束。

    擦除的缺陷

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

    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];
      }
    }

    擦除的补偿

    1. 类型判断问题

    例子:

    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关键字来做判断。所以我们使用类类型来构造一个类型判断器,判断一个实例是否为特定的类型。

    2. 创建类型实例

    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());
      }
    }

    通过特定的工厂类实现特定的类型能够解决实例化类型参数的需求。

    3. 创建泛型数组

    一般不建议创建泛型数组。尽量使用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方法创建实例的方式。

    边界

    这里Java重用了 extend关键字。边界可以将类型参数的范围限制到一个子集当中。

    interface HasColor {
      Color getColor();
    }
    
    class Colored<T extends HasColor> {
      T item;
    
      public Colored(T item) {
        this.item = item;
      }
    
      public T getItem() {
        return item;
      }
    
      public Color color() {
        return item.getColor();
      }
    }
    
    class Dimension {
      public int x, y, z;
    }
    
    class ColoredDemension<T extends HasColor & Dimension> {
      T item;
    
      public ColoredDemension(T item) {
        this.item = item;
      }
    
      public T getItem() {
        return item;
      }
    
      Color color() {
        return item.getColor();
      }
    
      int getX() {
        return item.x;
      }
    
      int getY() {
        return item.y;
      }
    
      int getZ() {
        return item.z;
      }
    
    }
    
    interface Weight {
      int weight();
    }
    
    class Solid<T extends Dimension & HasColor & Weight> {
      T item;
    
      public Solid(T item) {
        this.item = item;
      }
    
      public T getItem() {
        return item;
      }
    
      Color color() {
        return item.getColor();
      }
    
      int getX() {
        return item.x;
      }
    
      int getY() {
        return item.y;
      }
    
      int getZ() {
        return item.z;
      }
    
      int weight() {
        return item.weight();
      }
    }
    
    class Bounded extends Dimension implements HasColor, Weight {
      @Override
      public Color getColor() {
        return null;
      }
    
      @Override
      public int weight() {
        return 0;
      }
    }
    
    public class BasicBound {
      public static void main(String[] args) {
        Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
        solid.color();
        solid.weight();
        solid.getZ();
      }
    }

    extends关键字声明中,有两个要注意的地方:

    • 类必须要写在接口之前;
    • 只能设置一个类做边界,其它均为接口。

    通配符

    协变

    public class Holder<T> {
      private T value;
    
      public Holder(T apple) {
      }
    
      public T getValue() {
        return value;
      }
    
      public void setValue(T value) {
        this.value = value;
      }
    
      @Override
      public boolean equals(Object o) {
        return value != null && value.equals(o);
      }
    
      public static void main(String[] args) {
        Holder<Apple> appleHolder = new Holder<Apple>(new Apple());
        Apple d = new Apple();
        appleHolder.setValue(d);
    
        // 不能自动协变
        // Holder<Fruit> fruitHolder=appleHolder;
    
        // 借助 ? 通配符和 extends 关键字可以实现协变
        Holder<? extends Fruit> fruitHolder = appleHolder;
    
        // 返回一个Fruit,因为添加边界之后返回的对象是 ? extends Fruit,
        // 可以把它转型为Apple,但是在不知道具体类型的时候存在风险
        d = (Apple) fruitHolder.getValue();
    
        //Fruit以及Fruit的父类,就不需要转型
        Fruit fruit = fruitHolder.getValue();
        Object obj = fruitHolder.getValue();
    
        try {
          Orange c = (Orange) fruitHolder.getValue();
        } catch (Exception e) {
          System.out.print(e);
        }
    
        // 编译不通过,因为编译阶段根本不知道子类型到底是什么类型
        // fruitHolder.setValue(new Apple());
        // fruitHolder.setValue(new Orange());
    
        //这里是可以的因为equals方法接受的是Object作为参数,并不是 ? extends Fruit
        System.out.print(fruitHolder.equals(d));
      }
    }

    在Java中父类型可以持有子类型。如果一个父类的容器可以持有子类的容器,那么我们就可以称为发生了协变。在java中,数组是自带协变的,但是泛型的容器没有自带协变。我们可以根据利用边界和通配符?来实现近似的协变。

    Holder<? extends Fruit>就是一种协变的写法。它表示一个列表,列表持有的类型是Fruit或其子类。

    这个Holder<? extends Fruit>运行时持有的类型是未知的,我们只知道它一定是Fruit的子类。正因为如此,所以我们无法向这个holder中放入任何类型的对象,Object类型的对象也不可以。但是,调用它的返回方法却是可以的。因为边界明确定义了它是Fruit类型的子类。

    逆变

    package wildcard;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class GenericWriting {
      static <T> void writeExact(List<T> list, T item) {
        list.add(item);
      }
    
      static List<Apple> apples = new ArrayList<Apple>();
      static List<Fruit> fruits = new ArrayList<Fruit>();
    
      static void f1() {
        writeExact(apples, new Apple());
        //this cannot be compile,said in Thinking in Java
        writeExact(fruits, new Apple());
      }
    
      static <T> void writeWithWildcard(List<? super T> list, T item) {
        list.add(item);
      }
    
      static void f2() {
        writeWithWildcard(apples, new Apple());
        writeWithWildcard(fruits, new Apple());
      }
    
      static <T> readWithWildcard(List<? super T> list, int index) {
        //Compile Error, required T but found Object
        return list.get(index);
      }
      public static void main(String[] args) {
        f1();
        f2();
      }
    }

    如果一个类的父类型容器可以持有该类的子类型的容器,我们称这种关系为逆变。声明方式List<? super Integer>, List<? super T> list。

    不能给泛型参数给出一个超类型边界;即不能声明List<T super MyClass>。

    上面的例子中,writeExact(fruits,new Apple());在《Java编程思想》中说是不能通过编译的,但我试了一下,在Java1.6,Java1.7中是可以编译的。不知道是不是编译器比1.5版本升级了。

    由于给出了参数类型的‘下界’,所以我们可以在列表中添加数据而不会出现类型错误。但是使用get方法获取返回类型的时候要注意,由于声明的类型区间是Object到T具有继承关系的类。所以返回的类型为了确保没有问题,都是以Object类型返回回来的。比如过例子中list.get(index)的返回类型就是Object。

    无界通配符

    无界通配符<?> 意味着可以使用任何对象,因此使用它类似于使用原生类型。但它是有作用的,原生类型可以持有任何类型,而无界通配符修饰的容器持有的是某种具体的类型。举个例子,在List<?>类型的引用中,不能向其中添加Object, 而List类型的引用就可以添加Object类型的变量。

    一些需要注意的问题

    1. 任何基本类型都不能作为类型参数。

    2. 实现参数化接口。

    例子:

    interface Payable<T>{}
    class Employee implements Payable<Employee> {}
    //Compile Error
    class Hourly extends Employee implements Payable<Hourly> {}

    因为擦除的原因,Payable<Employee> 与 Payable<Hourly>简化为相同的Payable<Object>,例子中的代码意味着重复两次实现相同的接口。但他们的参数类型却是不相同的。

    3. 转型和警告

    使用带有泛型类型参数的转型或者instanceof不会有任何效果。因为他们在运行时都会被擦除到上边界上。所以转型的时候用的类型实际上是上边解对应的类型。

    4. 重载

    //Compile Error. 编译不能通过
    public class UseList<W,T>{
      void f(List<T> v){}
      void f(List<W> v){}
    }

    由于擦除的原因,重载方法将产生相同的类型签名。避免这种问题的方法就是换个方法名。

    5. 基类劫持接口。

    例子:

    public class ComparablePet implements Comparable<ComparablePet>{
      public int compareTo(ComparablePet arg) {return 0;}
    }
    class Cat extends ComparablePet implements Comparable<Cat>{
      // Error: Comparable connot be inherited with
      // different arguments: <Cat> and <ComparablePet>
      public int compareTo(Cat arg);
    }

    父类中我们为Comparable确定了ComparablePet参数,那么其它任何类型都不能再与ComparablePet之外的对象再比较。子类中不能对同一个接口用不同的参数实现两次。这有点类似于第四点中的重载。但是我们可以在子类中覆写父类中的方法。


  • 相关阅读:
    百度和谷歌,你选择谁?
    数据库的另一种设计方法
    超级IO操作类
    WEB工具类,很强很大
    JS在AJAX中获取鼠标坐标
    弃掉HTML标记的小巧代码
    XML工具操作类,很强大
    FTP 下载功能代码
    db4o开门之篇
    ASP.NET程序中常用代码汇总(转载)
  • 原文地址:https://www.cnblogs.com/itfky/p/13728875.html
Copyright © 2011-2022 走看看