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之外的对象再比较。子类中不能对同一个接口用不同的参数实现两次。这有点类似于第四点中的重载。但是我们可以在子类中覆写父类中的方法。


  • 相关阅读:
    leetcode 13. Roman to Integer
    python 判断是否为有效域名
    leetcode 169. Majority Element
    leetcode 733. Flood Fill
    最大信息系数——检测变量之间非线性相关性
    leetcode 453. Minimum Moves to Equal Array Elements
    leetcode 492. Construct the Rectangle
    leetcode 598. Range Addition II
    leetcode 349. Intersection of Two Arrays
    leetcode 171. Excel Sheet Column Number
  • 原文地址:https://www.cnblogs.com/itfky/p/13728875.html
Copyright © 2011-2022 走看看