在JDK 1.5中,几个新的特征被引入Java语言。其中之一就是泛型(generics)。
定义泛型
public interface List<E> {
void add(E x);
Iterator<E> iterator();
}
public interface Interator<E> {
E next();
boolean hasNext();
}
这是一段Collection里代码,一个完整的泛型定义。尖括号里的E就是形式类型参数(formal type parameters)。在泛型定义中,类型参数的用法就像一般具体类型那样。
在引言中,我们看到初始化了一个泛型List——List<Integer>。在这里,类型参数被赋于实际类型参数(actual type argument)Integer。
你可以想象List<Integer>将获得这样的代码:
public interface List {
void add(Integer x);
Iterator< Integer > iterator();
}
泛型和子类
下面的这段代码合法么?
List<String> ls = new ArrayList<String> ();// 1
List<Object> lo = ls;// 2
假设这两行代码是正确的,那么下面的操作:
lo.add(new Object());// 3
String str = ls.get(0);// 4
将导致运行时刻错误。通过别名lo存取ls时,我们可以插入任意类型的对象——ls就不再仅仅持有String了。
Java编译器消除了这种错误发生的可能性。第2行将导致编译时刻错误。
一般地说,如果Foo是Bar的子类,G定义为某种泛型,那么G<Foo>不是G<Bar>的子类。
通配符
如果,我们试图使用泛型的方法编写一个打印Collection内所有元素的函数,要怎么做?
void printCollection (Collection<Objcet> c) {
for (Objcet obj : c) {// jdk 1.5中新增的语法,见5.1
System.out.println(obj);
}
}
显然这样是不行的,因为Collection<Object>不是任何Collection的父类。
那么,所有Collection的父类是什么?Collection<?>——未知类型的Collection(collection of unknown),一个元素可以匹配为任意类型的Collection。“?”被称作通配类型。上述的代码,可以改写成这样:
void printCollection(Collection<?> c) {
for (Object obj : c) {
System.out.println(obj);
}
}
现在,我们可以使用任意类型的Collection作为参数了。注意,在printCollection内,用Objcet类型访问c的元素是安全的,因为任何一种具体类型都是Object的子类。
但是这样的操作是错误的:
List<?> list = new ArrayList<String>();
list.add(…);// compile-time error!
因为list被定义为List<?>,“?”指代了一个未知类型。list.add(…)无法保证插入的对象类型就是list实际包含的类型。唯一的例外就是null——null可以是任意类型的值。
但是,通过一个List<?>引用,调用get()函数是可以的——即不会修改Collection的函数,就像printCollection里那样。尽管不能确定具体的类型,但是都是Object的子类。
受限通配符
现在要创建一个简单的作图程序。我们定义了接口Shape:
public abstract class Shape {
public abstract void draw();
}
然后定义了2个子类:
public class Circle extends Shape {
…….
public void draw() { … }
}
public class Rectangle extends Shape {
……
public void draw() {……}
}
很自然地,我们也会设计这样一个函数:
void drawAll (List<…> shapes) {
for (Shape s : shapes) {
s.draw();
}
}
尖括号里应该填写什么了?显然,List<Shape>是行不通的,这在3.1里已经说明了。List<?>可以,但是不好,因为如果这样使用:
List<Object> list = new ArrayList<Object>();// 1
list.add(new Object());// 2
drawAll(list);// 3
编译器认为没有问题,但是运行时刻肯定报错。在drawAll里,我们实际需要的是Shape的子类,但是List<?>无法在编译时刻保证这一点。
这里的解决方案是受限通配符(bounded wildcard)。这样做:
void drawAll(List<? extends Shape> shapes) { .. … }
如果,再像前一个例子的第3行那样使用的话,编译器会报错。因为编译器要求shapes的每一个元素的实际类型都是Shape的子类。
同使用一般通配符一样,shapes.add(…)是不允许的,因为,编译器只能保证插入的是Shape的子类对象,而不能肯定与Collection实际包含的类型是匹配的。
泛型函数
考虑设计这样一个函数——把一个数组中的对象依次插入一个Collection中。我们首先这样尝试:
void addFromArray(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o);// compile-time error!
}
}
从前面的介绍中,可以明确这样是不行的。当然Collection<Object>同样是错误的。
解决这类问题的方法就是使用泛型函数:
static <T> void addFromArray(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o);
}
}
但是必须注意,当我们执行addFromArray时,编译器将根据参数的类型检查是否安全:
addFromArray(new String[10], new ArrayList<String>());// OK!
addFromArray(new String[10], new ArrayList<Object>());// OK!
addFromArray(new Object[10], new ArrayList<String>());// compile-time error!
addFromArray(new String[10], new ArrayList<Integer>());// compile-time error!
第3,4行的错误是很容易理解的,无论是把一个Object类型对象插入String的List还是把一个String插入Integer的List都是不安全的。
不过,如果这样的代码是没有问题的:
<T> void foo(T t1, T t2) {
System.out.println(t1.getClass());
System.out.println(t2.getClass());
}
foo(new Object(), new String());// 显示 class java.lang.Objectclass.lang.String
foo(new Integer(), new String();// 显示 class java.lang.Integerclass.lang.String
foo(new Object[10], new ArrayList<String>());
// 显示 class [Ljava.lang.Object;class.util.ArrayList
foo(new String[10], new ArrayList<Integer>());
// 显示 class [Ljava.lang.String;class.util.ArrayList
至于每一种调用T究竟是匹配了哪种类型。注意:这不是C++。经过编译,foo只生成一段代码,T就是Object。编译器只是在恰当的地方做了恰当的类型转换。
泛型函数和通配符的选择
什么时候应当使用泛型函数,什么时候应当使用通配符呢?
先看一段来自Collection里的代码:
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);
}
在containsAll和addAll中,类型参数T仅仅被使用了一次。函数返回值并不依赖于类型参数。这就告诉我们,类型参数是被用于实现多态的;它的作用仅仅是允许不同的实际类型在不同的场合下可以被使用。如果是这种情况的话,应当使用通配符。通配符用来实现弹性的子类化——就像这里试图表达的那样。
泛型函数允许类型参数用来表达函数以及它的返回值和一个或多个类型参数之间的依赖性。如果,不存在这样的依赖性的话,泛型函数就不应当被使用。
泛型函数和通配符有时是可以一起使用的,如:
class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src) { … }
}
注意两个参数之间的类型依赖性。src内包含的对象必须满足is-a T,只有这样才能够被安全的插入dest,因为dest包含的对象是T类型的。当然这样也可以的:
public static <T, S extends T> void copy(List<T> dest, List<S> src) { … }
但是推荐第一种用法。因为T同时对dest和src起作用,而S仅仅作用于src,没有其他的什么依赖于它——这种情况下,用通配符取代S比较好。用通配符更加清晰、明了。
java中的泛型编程
最新推荐文章于 2024-09-17 15:52:04 发布