美文网首页
Generic 范型 - type parameter

Generic 范型 - type parameter

作者: 赵阳_c149 | 来源:发表于2021-05-15 21:57 被阅读0次

    范型简介

    从JDK 5.0开始,范型作为一种新的扩展被引入到了java语言中。

    有了范型,我们可以对类型(type=class+interface)进行抽象。最常见的例子是容器类型。

    List myIntList = new LinkedList(); // 1
    myIntList.add(new Integer(0)); // 2
    Integer x = (Integer) myIntList.iterator().next(); // 3      
    

    可以用范型对以上代码进行优化:

    List<Integer> 
        myIntList = new LinkedList<Integer>(); // 1'
    myIntList.add(new Integer(0)); // 2'
    Integer x = myIntList.iterator().next(); // 3'
    

    优化带来两点改进:

    1. 省去了造型(cast)的麻烦。
    2. 除了代码上的整洁,范型还在compile-time保证了代码的类型正确。如果没有范型,无法保证放入list的对象是Integer型。

    定义简单的范型

    从package java.util中摘录下接口List和Iterator的定义:

    public interface List <E> {
        void add(E x);
        Iterator<E> iterator();
    }
    
    public interface Iterator<E> {
        E next();
        boolean hasNext();
    }
    

    这里声明了type parameter:E。Type parameters在范型的全部声明中都可以用,就像使用其他普通的类型一样。

    调用范型的时候,需要为type parameter E 指定一个真实的类型变量【1】(又称为parameterized type),例如:

    List<Integer> myIntList = new LinkedList<Integer>();
    

    可以想象List<Integer>是List的一个版本,在这个版本里面,所有的 type parameter (E)都被Integer替换了:

    public interface IntegerList {
        void add(Integer x);
        Iterator<Integer> iterator();
    }
    

    这种想象很有帮助,因为parameterized type的List<Integer> 确实包含了类似的方法;但是也容易带来误导,因为每次调用范型并不会生成代码的一个拷贝,通过编译,一个范型类型的声明只会编译一次,生成一个class文件;每次调用范型,类似于给一个方法传入了一个argument,只是这里传入的是一个普通的类型。

    【1】这里用的是argument,即传给方法的值;区别parameter,parameter是作为方法签名的一部分,用于定义方法。

    范型和子类型

    假设Foo是Bar的子类型(class或者interface),G是一个范型类型声明,G<Foo> 不是G<Bar>的子类型,这点有些反直觉。

    wildcards 通配符

    接着上一节的讨论,假设Foo是Bar的子类型(class或者interface),G是一个范型类型声明,G<Foo> 不是G<Bar>的子类型。可是,如果我们确实需要在G<Foo> 和G<Bar>之间建立父子关系呢?具体来说,假设有以下一段代码:

    void printCollection(Collection c) {
        Iterator i = c.iterator();
        for (k = 0; k < c.size(); k++) {
            System.out.println(i.next());
        }
    }
    

    用范型对其进行优化,这里是一种错误的方式:

    void printCollection(Collection<Object> c) {
        for (Object e : c) {
            System.out.println(e);
        }
    }
    

    这样写本身没有错误,但是他对Collection中元素的类型进行了限制,只能是Object!那么,所有collection的超类是神马呢?就是Collection<?>(读作"collection of unknown"),这个Collection的元素类型可以任意匹配,被称作wildcard type

    void printCollection(Collection<?> c) {
        for (Object e : c) {
            System.out.println(e);
        }
    }
    

    嗯,不错,现在我们可以从c中读取出任意类型的元素。可以,这样一来,又出现了新的问题:什么样的元素可以放到c里面去呢?答案是:任何类型的元素都无法放到c里面去!因为无法知道c中的type parameter(也许写作E)是什么类型。

    Bounded Wildcards 有界通配符

    可能是考虑到?过于宽泛,java引入了Bounded Wildcards,有界通配符。假设有以下代码:

    public abstract class Shape {
        public abstract void draw(Canvas c);
    }
    
    public class Circle extends Shape {
        private int x, y, radius;
        public void draw(Canvas c) {
            ...
        }
    }
    
    public class Rectangle extends Shape {
        private int x, y, width, height;
        public void draw(Canvas c) {
            ...
        }
    }
    
    // These classes can be drawn on a canvas:
    public class Canvas {
        public void draw(Shape s) {
            s.draw(this);
       }
    }
    // Assuming that they are represented as a list, 
    // it would be convenient to have a method in Canvas that draws them all:
    public void drawAll(List<Shape> shapes) {
        for (Shape s: shapes) {
            s.draw(this);
       }
    }
    

    看上去不错,但是问题又来了,类型方法drawAll的签名参数中的ShapeCircle的超类,尽管CircleShape的子类,但是List<Circle>不是List<Shape>的子类。所以要想drawAll可以处理List<Circle>,可以将其定义为:

    public void drawAll(List<? extends Shape> shapes) {
        ...
    }
    

    Bounded Wildcards 也面临着?面临的问题,那就是他们都过于宽泛,因此无法
    确定什么样的元素可以放到集合里面:

    public void addRectangle(List<? extends Shape> shapes) {
        // Compile-time error!
        shapes.add(0, new Rectangle());
    }
    

    范型方法

    前面讨论了范型type的声明,其实,同样可以声明范型方法:

    static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
        for (T o : a) {
            c.add(o); // Correct
        }
    }
    

    所谓范型方法,就是在方法签名内的修饰符和方法返回类型之间,加入了type parameter,例如<T>。在调用方法的时候,并不需要传入type argument,编译器会根据actual argument的类型推断(infer)出type argument。

    较之于范型类型,范型方法的声明要稍微复杂一些。具体来说,范型方法包含返回值和若干parameter,而他们之间可能会存在着类型的依赖关系。而这种依赖关系就带来一个问题,什么时候应该使用通配符,什么时候应该使用范型方法呢?
    比如,查看JDK文档:

    interface Collection<E> {
        public boolean containsAll(Collection<?> c);
        public boolean addAll(Collection<? extends E> c);
    }
    

    为什么不写成:

    interface Collection<E> {
        public <T> boolean containsAll(Collection<T> c);
        public <T extends E> boolean addAll(Collection<T> c);
        // Hey, type variables can have bounds too!
    }
    

    在containsAll 和 addAll中,type parameter T 仅使用了1次。返回值和其他parameter并不依赖于它,这种情况下,应该使用通配符。只有当返回值和parameter之间存在依赖的情况下,才应该使用范型方法。例如:

    class Collections {
        public static <T> void copy(List<T> dest, List<? extends T> src) {
        ...
    }
    

    范型是如何实现的

    范型是通过编译器对代码的erasure转换实现的。可以把这一过程想象成source-to-source的翻译。例如:

    public String loophole(Integer x) {
        List<String> ys = new LinkedList<String>();
        List xs = ys;
        xs.add(x); // Compile-time unchecked warning
        return ys.iterator().next();
    }
    

    将被翻译成:

    public String loophole(Integer x) {
        List ys = new LinkedList;
        List xs = ys;
        xs.add(x); 
        return(String) ys.iterator().next(); // run time error
    }
    

    在第二段代码中,我们从list中取出一个元素,并试图通过将其cast(造型)把它当成String处理,这里会得到一个ClassCastException。

    因为在编译阶段,编译器对代码进行了erasure,<>内的一切都被删除了,所以所有对范型类型的调用(Invocations,或者说实例)共享同一个run-time class,随之而来的,static变量和方法也被这些实例共享,所以在static方法中,也无法引用type parameter;同时,Cast 和InstanceOf操作也就都失去了意义。

    Collection cs = new ArrayList<String>();
    // Illegal.
    if (cs instanceof Collection<String>) { ... }
    
    // Unchecked warning,
    Collection<String> cstr = (Collection<String>) cs;
    //gives an unchecked warning, since this isn't something the runtime system is //going to check for you.
    

    同理,对于方法来说,type variables(<T>在方法中叫type variables,在类型声明中叫parameterized type)也不存在于run-time:

    // Unchecked warning. 
    <T> T badCast(T t, Object o) {
        return (T) o;
    }
    

    如何定义范型数组

    private E[] elements = (E[]) new Object[10];
    
    • 数组和范型对类型的检查是不同的。

    对于数组来说,下面的语句是合法的:

    Object[] arr = new String[10];
    

    Object[] 是 String[]的超类,因为Object是String的超类。然而,对于范型来说,就没有这样的继承关系,因此,以下声明无法通过编译:

    List<Object> list = new ArrayList<String>(); // Will not compile. generics are invariant.
    

    java中引入范型,是为了在编译阶段强化类型检查。同时,因为type erasure,范型也没有runtime的任何信息。所以,List<String> 只有静态类型的 List<String>,和一个动态类型 List

    但是,数组携带了runtime的类型信息。在runtime,数组用Array Store Check来检查将要插入的元素是否和真实的数组类型兼容。因此,以下代码能很好的编译,但是由于Array Store Check,会在runtime失败:

    Object[] arr = new String[10];
    arr[0] = new Integer(10);
    

    回到范型,编译器会提供编译阶段的检查,避免这种以这种方式创建索引,防止runtime的异常出现。

    • 那么,创建范型数组有什么问题呢?
      创建元素的类型是type parameter, parameterized type 或者bounded wildcard parameterized type的数组是type-unsafe的。考虑如下代码:
    public <T> T[] getArray(int size) {
        T[] arr = new T[size];  // Suppose this was allowed for the time being.
        return arr;
    }
    

    在rumtime,T的类型未知,实际上创建的数组是Object[],因此在runtime,上面的方法像是:

    public Object[] getArray(int size) {
        Object[] arr = new Object[size];
        return arr;
    }
    

    假设,有以下调用:

    Integer[] arr = getArray(10);
    

    这就是问题,这里将Object[] 指派给了一个Integer[]类型的索引,这段代码编译没有问题,但是在runtime会失败。因此,创建范型数组是不合法的。

    相关文章

      网友评论

          本文标题:Generic 范型 - type parameter

          本文链接:https://www.haomeiwen.com/subject/ilmudltx.html