biGpython

生亦何欢,死亦何苦? 予我長袖,我必善舞!

导航

java中的泛型一

1.泛型和子类继承
 
     让我们测试一下我们对泛型的理解。下面的代码片断合法么?
 
List<String> ls = new ArrayList<String>(); //1
 
List<Object> lo = ls; //2
 
第1行当然合法,但是这个问题的狡猾之处在于第2行。
 
这产生一个问题:
 
一个String的List是一个Object的List么?大多数人的直觉是回答:“当然!”。
 
好,在看下面的几行:
 
lo.add(new Object()); // 3
 
String s = ls.get(0); // 4: 试图把Object赋值给String
 
这里,我们使用lo指向ls。我们通过lo来访问ls,一个String的list。我们可以插入任意对象进去。结果是ls中保存的不再是String。当我们试图从中取出元素的时候,会得到意外的结果。
 
java编译器当然会阻止这种情况的发生。第2行会导致一个编译错误。
 
总之,如果Foo是Bar的一个子类型(子类或者子接口),而G是某种泛型声明,那么G<Foo>是G<Bar>的子类型并不成立!!
 
这可能是你学习泛型中最难理解的部分,因为它和你的直觉相反。
 
这种直觉的问题在于它假定这个集合不改变。我们的直觉认为这些东西都不可改变。
 
举例来说,如果一个交通部(DMV)提供一个驾驶员里表给人口普查局,这似乎很合理。我们想,一个List<Driver>是一个List<Person>,假定Driver是Person的子类型。实际上,我们传递的是一个驾驶员注册的拷贝。然而,人口普查局可能往驾驶员list中加入其他人,这破坏了交通部的记录。
 
为了处理这种情况,考虑一些更灵活的泛型类型很有用。到现在为止我们看到的规则限制比较大。
 
2.1通配符(Wildcards)
 
考虑写一个例程来打印一个集合(Collection)中的所有元素。下面是在老的语言中你可能写的代码:
 
            void printCollection(Collection c) {
 
                 Iterator i = c.iterator();
 
                 for (int 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作为参数,而新版本则只能使用Collection<Object>,我们刚才阐述了,它不是所有类型的collections的父类。
 
那么什么是各种collections的父类呢?它写作: Collection<?>(发音为:"collection of unknown"),就是,一个集合,它的元素类型可以匹配任何类型。显然,它被称为通配符。我们可以写:
 
void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}
 
现在,我们可以使用任何类型的collection来调用它。注意,我们仍然可以读取c中的元素,其类型是Object。这永远是安全的,因为不管collection的真实类型是什么,它包含的都是objects。但是将任意元素加入到其中不是类型安全的
 
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 编译时错误
 
因为我们不知道c的元素类型,我们不能向其中添加对象。
 
add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类
因为我们不知道那是什么类型,所以我们无法传任何东西进去。唯一的例外是null,它是所有类型的成员。
 
另一方面,我们可以调用get()方法并使用其返回值。
返回值是一个未知的类型,但是我们知道,它总是一个Object,因此把get的返回值赋值给一个Object类型的对象或者放在任何希望是Object类型的地方是安全的。
 
2.2 有限制的通配符(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) {
        // ...
    }
}
 
这些类可以在一个画布(Canvas)上被画出来:
 
public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
    }
}
 
所有的图形通常都有很多个形状。假定它们用一个list来表示,Canvas里有一个方法来画出所有的形状会比较方便:
 
    public void drawAll(List<Shape> shapes) {
        for (Shape s : shapes) {
             s.draw(this);
         }
    }
 
现在,类型规则导致drawAll()只能使用Shape的list来调用。它不能,比如说对List<Circle>来调用。这很不幸,因为这个方法所作的只是从这个list读取shape,因此它应该也能对List<Circle>调用。我们真正要的是这个方法能够接受一个任意种类的shape:
 
public void drawAll(List<? extends Shape> shapes) { //..}
 
这里有一处很小但是很重要的不同:我们把类型 List<Shape> 替换成了 List<? extends Shape>。
现在drawAll()可以接受任何Shape的子类的List,所以我们可以对List<Circle>进行调用。
 
List<? extends Shape>是有限制通配符的一个例子。这里?代表一个未知的类型,就像我们前面看到的通配符一样。但是,在这里,我们知道这个未知的类型实际上是Shape的一个子类(它可以是Shape本身或者Shape的子类而不必是extends自Shape)。我们说Shape是这个通配符的上限(upper bound)。
 
像平常一样,要得到使用通配符的灵活性有些代价。这个代价是,现在像shapes中写入是非法的。比如下面的代码是不允许的:
 
    public void addRectangle(List<? extends Shape> shapes) {
        shapes.add(0, new Rectangle()); // compile-time error!
    }
 
你应该能够指出为什么上面的代码是不允许的。因为shapes.add的第二个参数类型是? extends Shape —— 一个Shape未知的子类。因此我们不知道这个类型是什么,我们不知道它是不是Rectangle的父类;它可能是也可能不是一个父类,所以这里传递一个Rectangle不安全。
 
有限制的通配符正是我们解决DMV给人口普查局传送名单的例子所需要的。我们的例子假定数据用一个姓名(String)到people(用Person或其子类来表示,比如Driver)。Map<K,V>是一个有两个类型参数的泛型类型的例子,表示map的键key和值value。
 
再一次,注意形式类型参数的命名习惯——K代表keys,V代表vlaues。
 
public class Census {
    public static void addRegistry(Map<String, ? extends Person> registry) { ...}
}
Map<String, Driver> allDrivers = ...;
Census.addRegistry(allDrivers);
 
3.泛型方法
 
考虑写一个方法,它用一个Object的数组和一个collection作为参数,完成把数组中所有object放入collection中的功能。
 
下面是第一次尝试:
 
static void fromArrayToCollection(Object[] a, Collection<?> c) {
    for (Object o : a) {
        c.add(o); // 编译期错误
    }
}
 
现在,你应该能够学会避免初学者试图使用Collection<Object>作为集合参数类型的错误了。或许你已经意识到使用 Collection<?>也不能工作。回忆一下,你不能把对象放进一个未知类型的集合中去。
 
解决这个问题的办法是使用generic methods。就像类型声明,方法的声明也可以被泛型化——就是说,带有一个或者多个类型参数。
 
static <T> void fromArrayToCollection(T[] a, Collection<T> c){
    for (T o : a) {
        c.add(o); // correct
    }
}
 
我们可以使用任意集合来调用这个方法,只要其元素的类型是数组的元素类型的父类。
 
      Object[] oa = new Object[100];
 
      Collection<Object> co = new ArrayList<Object>();
 
      fromArrayToCollection(oa, co);// T 指Object
 
      String[] sa = new String[100];
 
      Collection<String> cs = new ArrayList<String>();
 
      fromArrayToCollection(sa, cs);// T inferred to be String
 
      fromArrayToCollection(sa, co);// T inferred to be Object
 
      Integer[] ia = new Integer[100];
 
      Float[] fa = new Float[100];
 
      Number[] na = new Number[100];
 
      Collection<Number> cn = new ArrayList<Number>();
 
      fromArrayToCollection(ia, cn);// T inferred to be Number
 
      fromArrayToCollection(fa, cn);// T inferred to be Number
 
      fromArrayToCollection(na, cn);// T inferred to be Number
 
      fromArrayToCollection(na, co);// T inferred to be Object
 
      fromArrayToCollection(na, cs);// compile-time error
 
注意,我们并没有传送真实类型参数(actual type argument)给一个泛型方法。编译器根据实参为我们推断类型参数的值。它通常推断出能使调用类型正确的最明确的类型参数(原文是:It will generally infer the most specific type argument that will make the call type-correct.)。
 
现在有一个问题:我们应该什么时候使用泛型方法,又什么时候使用通配符类型呢?
 
为了理解答案,让我们先看看Collection库中的几个方法。
 
public interface Collection<E> {
 
    boolean containsAll(Collection<?> c);
 
    boolean addAll(Collection<? extends E> c);
 
}
 
我们也可以使用泛型方法来代替:
 
public interface Collection<E> {
 
        <T> boolean containsAll(Collection<T> c);
 
        <T extends E> boolean addAll(Collection<T> c);
 
        // hey, type variables can have bounds too!
 
}
 
但是,在 containsAll 和 addAll中,类型参数 T 都只使用一次。返回值的类型既不依赖于类型参数(type parameter)也不依赖于方法的其他参数(这里,只有简单的一个参数)。这告诉我们类型参数(type argument)被用作多态(polymorphism),它唯一的效果是允许在不同的调用点,可以使用多种实参类型(actual argument)。
如果是这种情况,应该使用通配符。通配符就是被设计用来支持灵活的子类化的,这是我们在这里要强调的。
 
泛型函数允许类型参数被用来表示方法的一个或多个参数之间的依赖关系,或者参数与其返回值的依赖关系。如果没有这样的依赖关系,不应该使用泛型方法。
 
(原文:Generic methods allow type parameters to be used to express dependencies among the types of one or more arguments to a method and/or its return type. If there isn’t such a dependency, a generic method should not be used.)
 
一前一后的同时使用泛型方法和通配符也是可能的。下面是方法 Collections.copy():
 
class Collections {
 
    public static <T> void copy(List<T> dest, List<? extends T> src){...}
 
}
 
注意两个参数的类型的依赖关系。任何被从源list从拷贝出来的对象必须能够将其指定为目标list(dest) 的元素的类型——T类型。因此源类型的元素类型可以是T的任意子类型,我们不关心具体的类型。
 
copy方法的签名使用一个类型参数表示了类型依赖,但是使用了一个通配符作为第二个参数的元素类型。我们也可以用其他方式写这个函数的签名而根本不使用通配符:
 
class Collections {
 
    public static <T, S extends T> void copy(List<T> dest, List<S> src){...}
 
}
 
这也可以,但是第一个类型参数在dst的类型和第二个参数的类型参数S的上限这两个地方都有使用,而S本身只使用一次,在src的类型中——没有其他的依赖于它。
这意味着我们可以用通配符来代替S使用通配符比声明显式的类型参数更加清晰和准确,所以在可能的情况下使用通配符更好
 
通配符还有一个优势式他们可以在方法签名之外被使用,比如field的类型,局部变量和数组。这就有一个例子。
 
回到我们的画图问题,假定我们想要保持画图请求的历史记录。我们可以把历史记录保存在Shape类的一个静态成员变量里,在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
如果一个泛型函数在一个泛型类里边出现,最好避免在方法的类型参数和类的类型参数中使用同样的名字来避免混淆。
对内部类也是同样。
 
 


通过 Wiz 发布


posted on 2012-03-06 19:45  biGpython  阅读(269)  评论(0编辑  收藏  举报