Generic 范型 - type parameter

2021-05-15  本文已影响0人  赵阳_c149

范型简介

从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的异常出现。

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会失败。因此,创建范型数组是不合法的。

上一篇下一篇

猜你喜欢

热点阅读