Java基础学习——泛型(generics)二

通配符(Wildcard)

考虑一个打印集合内所有元素的问题。下面这个可能是在Java旧版本中的写法:

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

下面是一个用泛型的天真尝试(同时使用foreach语法):

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

问题是新版本并不比旧版本更有益处。而旧版代码可以使用任何类型集合当参数传入,而新的代码确只能接受* Collection*类型,正如上文介绍,Collection并不是其他任何泛型集合的父类型。

那么什么才是所有泛型集合的父类型呢?那就是被写成Collection(表示“未知类型的集合” “collection of unknown”)的集合,一种元素类型可以匹配任何类型的集合。这就是它被叫做通配类型(wildcard type)的直白原因。我们可以这样写:

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

现在,我们可以用任何泛型集合当参数调用了。注意一下,在printCollection函数内部,我们依然是用Object类型去读取集合内的元素。无论集合内真实类型是什么,这样做是安全的,因为集合真的包含了Object。但是随意添加Object对象却不一定是安全的。

Collection c = new ArrayList<String>();
c.add(new Object()); // Compile time error

因为我们不知道c的元素类型时,我们是不能往里面添加对象的。方法add需要一个E类型的参数,E表示集合元素类型。什么时候实际类型参数是?呢?它代表着一些未知的类型(unknown type)。我们传入add方法的参数都必须是未知类型的子类型。因为我们不知道未知类型是什么,所以我们不能传入任何东西。唯一的例外就是null,null是所有类型的成员。

另外一方面,给定List,我们能够调用get()方法,并且利用返回值。返回值是一个未知类型,但我们总是能知道它是一个对象。因此,我们总是能安全地将get()的结果赋值给一个Object类型变量或者将它用在任何期望Object类型的地方。

受限通配符(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) {
        ...
    }
}

这些类都可以在画布上绘出:

public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
   }
}

一个图纸总是会包含很多形状。假设图纸用一个list表示,有个方面的函数Canvas可以画出所有图形:

public void drawAll(List shapes) {
    for (Shape s: shapes) {
        s.draw(this);
   }
}

现在,类型规则说明drawAll()函数只能以Shape的list为参数被调用。它实际上被不能以List为参数。这是不幸的,因为所有方法做的就是从List里读取shape,所以,它也应该能接受List为参数。其实,我们真实的想法,是接受一个shape子类类型的list:

public void drawAll(List shapes) {
    ...
}

上面的代码有一个很小却很重要的区别:我们用List

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

你应该能指出上面的代码为啥不能被允许。因为add方法的第二个参数类型是? extends Shape——Shape的未知子类型。因为我们不知道类型是啥,我们并不知道它是不是Rectangle的父类型。它可能是,也可能不是一个超类型,所以,这个地方传入Rectangle是不安全的。

受限通配符(Bounded wildcards)正好解决之前那个从人口普查局(the census bureau)传送数据给DMV的例子。之前的例子假设,数据是存放在map里的,用人名作key,人员信息(可以用Person类或其子类,比如Driver,代表)为value。Map是一个有两个类型参数的泛型例子,两个参数代表map的key与value。

再次提醒,参数类型的命名惯例是:K表示key,V表示values。

public class Census {
    public static void addRegistry(Map registry) {
}
...

Map allDrivers = ... ;
Census.addRegistry(allDrivers);

以上内容翻译自Java 官网泛型教程之通配符

泛型方法(Generic Methods)

考虑一下,需要写个将一个Object数组的元素都写入另外一个collection(集合)的方法。下面是第一次尝试:

static void fromArrayToCollection(Object[] a, Collection c) {
    for (Object o : a) { 
        // 编译错误
        c.add(o); // compile-time error
    }
}

到目前为止,你可能已经学会避免刚开始时用Collection来代替所有集合的错误。你可能已经认识到,也可能还没有,Collection

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

我们可以用任何类型的集合调用此函数,只要集合的元素类型是数组元素类型的父类型。

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();

// T inferred to be Object
// 将T自动推导为Object
fromArrayToCollection(oa, co); 

String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();

// T inferred to be String
// 将T自动推导为String
fromArrayToCollection(sa, cs);

// T inferred to be Object
// T 自动推导为Object
fromArrayToCollection(sa, co);

Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();

// T inferred to be Number
// T 自动推导为Number
fromArrayToCollection(ia, cn);

// T inferred to be Number
// T 自动推导为Number
fromArrayToCollection(fa, cn);

// T inferred to be Number
// T 自动推导为Number
fromArrayToCollection(na, cn);

// T inferred to be Object
// T 自动推导为Object
fromArrayToCollection(na, co);

// compile-time error
// 编译错误
fromArrayToCollection(na, cs);

注意一下,我们并没有直接传入类型给泛型函数。Java编译器会根据实际参数的类型自动推导T的类型。

但是有一个问题:什么时候我们需要泛型函数,什么时候我们需要通配符类型。为了搞清楚这个问题。我们拿出几个jdk里的Collection类方法作为例子。

interface Collection {
    public boolean containsAll(Collection c);
    public boolean addAll(Collection c);
}

这里我们也可以使用泛型函数。

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

但是,在containsAll和addAl函数里,类型参数T都仅仅被用过一次。返回值既不依赖类型参数,也没有其他参数依赖此类型(在这个例子里,方法只有一个参数)。这便是告诉我们这里的类型参数只是用于多态,仅仅为了可以让方法被各种各样的参数类型调用。如果是上面这种情况,就是应该使用通配符(wildcards)。通配符正是设计用来支持灵活的子类型,像上面的例子那样。

泛型方法用来使类型参数可以体现多个参数之间或参数与返回值之间的类型依赖关系。如果没有这些依赖关系,则不应该使用泛型方法。

泛型方法和通配符是可以串联使用的。比如Collections.copy():

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

注意一下,上面的依赖关系是两个参数类型之间的依赖。list src里的元素都必须可以赋值给dst list的元素类型T。所以,src的元素类型可以是T的任何类型,我们也不关心具体是哪个子类型。copy函数的声明体现了类型参数的依赖,并且在第二个参数的类型使用了通配符。

我们也可以用完全不用通配符的方式,改写上面函数的声明:

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

改写后,第一个参数类型,既用在了dst,同时又是第二个类型参数S的上界,同时,S仅仅只在src里用了一次。这便是用通配符替换S的标志。这里,使用通配符则比类型参数更加简洁清晰。因此,如果合适,则应首选通配符。

通配符同时还有其他优点,它可以在方法外面被使用,比如成员变量,局部变量和数组等。这里有个例子。
回到之前的绘画程序的例子。假设我们想保留绘画的历史操作。我们可以使用一个静态变量保留历史,然后,在drawAll()函数中,将操作保存到历史静态变量里。

static List<List extends Shape>> 
    history = new ArrayList<List extends Shape>>();

public void drawAll(List extends Shape> shapes) {
    history.addLast(shapes);
    for (Shape s: shapes) {
        s.draw(this);
    }
}

在结束前,让我们再次强调下,类型参数的命名约定。当我们没有更加详细内容来说明的类型,我们使用T代表类型。在泛型函数里,经常出现这种情况。如果有多个参数类型,我们就是T旁边的字面,比如S。如果一个泛型方法出现在泛型类(generic class)里,为了避免混淆,最好不要使用相同的类型参数名称。这也使用于泛型嵌套类。

以上内容翻译自 Java 官网

你可能感兴趣的:(java)