创建泛型和泛型方法
创建一个简单的泛型是非常容易的。首先,在一对尖括号(< )中声明类型变量,以逗号间隔变量名列表。在类的实例变量和方法中,可以在任何类型的地方使用那些类型变量。切记,类型变量仅在编译时存在,所以不能使用instanceof和new这类运行时操作符来操作类型变量。
让我们以一个简单的例子来开始这部分的学习,而后将精简这个例子。这段代码定义了一个树形数据结构,使用类型变量V代表存储在各个树结点中的值。
import java.util.*;/** * A tree is a data structure that holds values of type V. * Each tree has a single value of type V and can have any number of * branches, each of which is itself a Tree. */public class Tree<V {
// The value of the tree is of type V.
V value;
// A Tree<V can have branches, each of which is also a Tree<V
List<Tree<V
branches = new ArrayList<Tree<V();
// Here's the constructor.
Note the use of the type variable V.
public Tree(V value) { this.value = value; }
// These are instance methods for manipulating the node value and branches.
// Note the use of the type variable V in the arguments or return types.
V getValue() { return value; }
void setValue(V value) { this.value = value; }
int getNumBranches() { return branches.size(); }
Tree<V getBranch(int n) { return branches.get(n); }
void addBranch(Tree<V
branch) { branches.add(branch); }}
正如你所看到的,命名一个类型变量习惯于一个大写字母。使用一个字母可以同现实中那些具有描述性的,长的实际变量名有所区别。使用大写字母要同变量命名规则一致,并且要区别于局部变量,方法参数,成员变量,而这些变量常常使用一个小写字母。集合类中,比如java.util中常常使用类型变量E代表“Element type”。T和S常常用来表示范型变量名(好像使用i和j作为循环变量一样)。
注意到,当一个变量被声明为泛型时,只能被实例变量和方法调用(还有内嵌类型)而不能被静态变量和方法调用。原因很简单,参数化的泛型是一些实例。静态成员是被类的实例和参数化的类所共享的,所以静态成员不应该有类型参数和他们关联。方法,包括静态方法,可以声明和使用他们自己的类型参数,但是,调用这样一个方法,可以被不同地参数化。这些内容将在本章后面谈到。
类型变量绑定
上面例子中的Tree<V中的类型变量V是不受约束的:Tree可以被参数化为任何类型。以前我们常常会设置一些约束条件在需要使用的类型上:也许我们需要强制一个类型参数实现一个或多个接口,或是一个特定类的子类。这可以通过指明类型绑定来完成。我们已经看到了统配符的上界,而且使用简单的语法可以指定一般类型变量的上界。后面的代码,还是使用Tree这个例子,并且通过实现Serializable和Comparable来重写。为了做到这点,例子中使用类型变量绑定来确保值类型的Serializable和Comparable。
import java.io.Serializable;import java.util.*;public class Tree<V extends Serializable & Comparable<V
implements Serializable, Comparable<Tree<V{
V value;
List<Tree<V
branches = new ArrayList<Tree<V();
public Tree(V value) { this.value = value; }
// Instance methods
V getValue() { return value; }
void setValue(V value) { this.value = value; }
int getNumBranches() { return branches.size(); }
Tree<V getBranch(int n) { return branches.get(n); }
void addBranch(Tree<V
branch) { branches.add(branch); }
// This method is a nonrecursive implementation of Comparable<Tree<V
// It only compares the value of this node and ignores branches.
public int compareTo(Tree<V that) {
if (this.value == null && that.value == null) return 0;
if (this.value == null) return -1;
if (that.value == null) return 1;
return this.value.compareTo(that.value);
}
// javac -Xlint warns us if we omit this field in a Serializable class
private static final long serialVersionUID = 833546143621133467L;}
一个类型变量的绑定是通过extends后的名字和一个类型列表(这可以是参数化的,就像Comparable一样)表达的。注意当有不止一个绑定时,就像上面例子中的,绑定的类型要用&作为分隔符,而不是使用逗号。都后用来分隔类型变量,如果用来分隔类型变量绑定,就会模棱两可。一个类型变量可以有任何数量的绑定,包括任何数量的借口和至多一个类。
范型中的通配符
上一章的例子中我们看到了通配符和控制参数化类型的通配符绑定。这些在范型中同样非常有用。当前设计的Tree要求每个节点有相同类型的值,V。也许这样太严格了,也许我们应该让Tree的branches能够存放V的子类而不全是V。这个版本的Tree(删除了Comparable和Serializable接口的实现)这样做会更灵活。
public class Tree<V {
// These fields hold the value and the branches
V value;
List<Tree<? extends V
branches = new ArrayList<Tree<? extends V();
// Here's a constructor
public Tree(V value) { this.value = value; }
// These are instance methods for manipulating value and branches
V getValue() { return value; }
void setValue(V value) { this.value = value; }
int getNumBranches() { return branches.size(); }
Tree<? extends V getBranch(int n) { return branches.get(n); }
void addBranch(Tree<? extends V
branch) { branches.add(branch); }}
通配符绑定允许我们在枝节点上增加一个Tree<Integer,比如,一个树枝Tree<Number:
Tree<Number t = new Tree<Number(0);
// Note autoboxingt.addBranch(new Tree<Integer(1));
// int 1 autoboxed to Integer
通过getBranch()查询树枝,而树枝的返回类型不知道,所以必须使用统配符来表达。接下来的两个是合法的,但第三个不是:
Tree<? extends Number b = t.getBranch(0);Tree<? b2 = t.getBranch(0);Tree<Number b3 = t.getBranch(0);
// compilation error
当我们这样来查询一个树枝时,不能精确确定它的返回类型,但是存在类型的上限,所以,我们可以这样做:
Tree<? extends Number b = t.getBranch(0);Number value = b.getValue();
那我们不能做什么呢?设定树枝的值,或者在原有的树枝上添加新的树枝。早前章节解释的,上界的存在不会改变返回值的类型不可知,编译器没有足够的信息让我们安全的给setValue()或者一个树枝(包括值类型)的addBranch()传递一个值。下面的两行代码都是非法的:
b.setValue(3.0); // Illegal, value type is unknownb.addBranch(new Tree<Double(Math.PI));
这个例子在设计时找到了一个平衡点:使用绑定通配符使得数据结构更加灵活,但是减少了安全使用其中方法的可能。这个设计是好是坏就要根据上下文联系了。通常,好的范型设计是非常困难的。幸运的是,大多我们要使用的已经在java.util包中设计好了,而不用我们自己再去设计。
范型方法
正如前面说的,范型只能被实例成员调用,而不是静态成员。同实例方法一样,静态方法也可以使用通配符。尽管静态方法不能使用包含他们的类中的类型变量,但是他们可以声明自己的类型变量。当一个方法声明了自己的类型变量,就叫做范型方法。
这里有一个要添加到Tree中的静态方法。他不是一个范型方法,但是使用了绑定的通配符,就好像先前我们看到的sumList()一样:
/** Recursively compute the sum of the values of all nodes on the tree */public static double sum(Tree<? extends Number t) {
double total = t.value.doubleValue();
for(Tree<? extends Number b : t.branches) total += sum(b);
return total;}
通过通配符的上界绑定,声明自己的类型变量来重写这个方法:
public static <N extends Number double sum(Tree<N t) {
N value = t.value;
double total = value.doubleValue();
for(Tree<? extends N b : t.branches) total += sum(b);
return total;}
范型的sum()不比通配符版本的简单,而且声明变量并没有让我们获得什么。这种情况下,通配符方案要比范型方法更有效,当一个类型变量用来表达两个参数之间或者参数和返回值之间的关系时,范型方法才是需要的。请看下面的例子:
// This method returns the largest of two trees, where tree size//
is computed by the sum() method.
The type variable ensures that //
both trees have the same value type and that both can be passed to sum().public static <N extends Number
Tree<N max(Tree<N t, Tree&