12.3 上界和下界
泛型类型和泛型方法与一般类型和方法的区别在于可以声明形式类型参数。在实际的类型声明中,形式类型参数会被替换成具体的类型。虽然形式类型参数在源代码中可以使用,但其使用方式是受限的。编译器把形式类型参数当成实际类型的占位符。编译器不能对实际声明时可能使用的类型做出任何假定,只能使用最严格的检查方式,以避免出现类型安全错误。如代码清单12-1中的ObjectHolder类的形式类型参数T,在setObject方法中作为参数的类型。在setObject方法中,可以对类型为T的参数对象进行的操作并不多。编译器所允许的是把该参数对象当成Object类的对象来处理,只能调用Object类中的方法。除此之外的任何操作都可能带来类型安全问题。
如果希望限制在实际类型声明时可以使用的类型,那么可以为形式类型参数添加上界。在添加了上界之后,泛型类型在实例化时只能使用由上界表示的类型及其子类型。代码清单12-9通过extends关键词为形式类型参数T添加了上界Comparable<T>接口。声明泛型类型时的实际类型必须实现Comparable接口,否则会出现编译错误。这样可以确保实际类型实现了Comparable接口,可以在代码中调用参数对象的compareTo方法。如果没有显式指定上界,那么默认的上界是Object类。形式类型参数虽然有上界,但是没有下界,这是因为下界在实际中几乎没有作用。
代码清单12-9 形式类型参数的上界
public class ComparableObjectHolder<T extends Comparable<T>>{
private T obj;
public int compareTo(T anotherObj){
return obj.compareTo(anotherObj);
}
}
形式类型参数上界的作用是限制泛型类型实例化时可以使用的类型,可以在代码中使用上界类型中提供的公开成员,包括公开的方法、域和嵌套类型,但是不包括构造方法。这是因为构造方法是不被继承的,子类的构造方法的参数可以与父类构造方法不同。所以使用类似“new T()”这样的表达式并不能保证可以正确创建出所需的对象。
除了基本类型和数组类型之外的其他类型都可以作为形式类型参数的上界,其中除了一般的类和接口之外,还包括参数化类型和形式类型参数。代码清单12-9中的上界用的是参数化类型。对于int和float等基本类型,可以使用其对应的包装类来作为上界;对于数组类型,可以使用集合类框架中的类或接口来替代。代码清单12-10中的SampleClass类使用了两个形式类型参数S和T,且T是S的上界。这就要求实例化时第一个类型与第二个类型相同,或者第一个类型是第二个类型的子类型。类似“SampleClass<String, Comparable<String>>”的实例化形式是合法的。
代码清单12-10 使用形式类型参数作为上界
public class SampleClass<S extends T, T>{
public void test(){
SampleClass<String, Comparable<String>>obj=new SampleClass<>();
}
}
一个形式类型参数可以包含多个上界,不同上界之间使用“&”来分隔。在实例化时所能使用的类型是所有上界的子类型。代码清单12-11中类的形式类型参数使用Cloneable接口和java.io.Serializable接口作为共同的上界。
代码清单12-11 形式类型参数包含多个上界的示例
public class CloneableSerializable<T extends Cloneable&Serializable>{
public void serialize(T obj){
}
}
这些上界之间的顺序虽然不会对实例化时所能使用的具体类型的范围产生影响,但是会影响类型擦除之后使用的类型。在类型擦除过程中,形式类型参数会被最左边的上界所替代。例如代码清单12-11中的类,在类型擦除之后,serialize方法的参数的类型是Cloneable接口。如果在声明时使用的形式是“<T extends Serializable&Cloneable>”,那么在类型擦除之后,serialize方法的参数的类型是Serializable接口。不过由于实际类型是所有上界的子类型,具体使用哪个上界类型来表示实际上是没有差别的。
除了形式类型参数之外,泛型类型实例化时的实际类型也可以包含上界或下界。参数化类型中的上界或下界都是与通配符一块来使用的。