1. 擦除的补偿
由于擦除丢失了在泛型代码中执行某些操作的能力,任何在运行时需要知道确切类型信息的操作都无法完成:
public class Erased<T>{
private final int SIZE = 100;
public static void f(Object arg){
// if(arg instanceof T) { // 错误
// T var = new T(); // 错误
// T[] array = new T[SIZE]; // 错误
// T[] array = (T)new Object[SIZE]; //正确但有警告
}
}
通过引入类型标签来对擦除进行补偿,这意味着你需要显式地传递你的类型的Class对象,以便你可以在类型表达式中使用它。如果想实现上述错误代码的功能可以显示传递类型的Class对象:
public class Erased<T> {
public static Class kind;
public Erased(Class kind) {
this.kind = kind;
}
public static void f(Object arg) {
if (kind.isInstance(arg)) {
}
}
}
从上面的代码可以看出,引入类型标签,就可以转而使用动态的isInstance(),这样编译器将确保类型标签可以匹配泛型参数了。
1)创建类型实例
在错误的例子中使用泛型参数:new T()创建对象是无法实现的,部分原因是因为擦除,而另一部分原因是因为编译器不能验证T具有无参构造函数。解决方案是传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象是Class对象,使用newInstance来创建这个类型的对象。但是有些没有无参构造函数的类无法使用这种方法来创建,并且这个错误无法再编译期被捕获,所以Java的创建者不赞成这种方式,他们建议使用显示的工厂,并将其限制类型。
T create();
}
class IntegerFactory implements Factory<Integer> {
public Integer create() {
return new Integer(0);
}
}
class Widget {
public static class WidgetFactory implements Factory<Widget> {
public Widget create() {
return new Widget();
}
}
}
class Foo2<T> {
private T x;
public <F extends Factory<T>> Foo2(F factory) {
x = factory.create();
}
}
class FactoryContraint {
public static void main(String[] args) {
new Foo2<Integer>(new IntegerFactory());
new Foo2<Widget>(new Widget.WidgetFactory());
}
}
注意,这确实只是传递Class的一种变体。两种方式都传递了工厂对象,Class碰巧是内建的工厂对象,而上面的方式创建了一个显示的工厂对象,获得了编译器的检查。
另一种方式使模板方法设计模式,在下面的例子中get()是模板方法,而create()是在子类中定义的、用来产生子类类型的对象:
abstract class GenericWithCreate<T> {
final T element;
GenericWithCreate() {
element = create();
}
abstract T create();
}
class X {
}
class Creator extends GenericWithCreate<X> {
X create() {
return new X();
}
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
public class CreatorGeneric {
public static void main(String[] args) {
Creator creator = new Creator();
creator.f();
}
}
2)泛型数组
在任何想要创建泛型数组的地方都使用ArrayList,例如,可以声明一个泛型数组的引用:
class Generic{
}
public class ArrayOfGeneric {
static Generic[] gia;
}
在这个程序中不能创建确切泛型类型的数组:gia = new Generic[10],这种写法是错误的。如果想把Object[]转型为Generic运行时会抛出ClassCastException。这个问题在于数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的,因此即使gia已经被转型为Generic[],但是这个信息只存在于编译期。在运行时,它仍旧是Object数组,而这将引发问题。成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。接下来是一个更加复杂的例子:
public class GenericArray<T>{
private T[] array;
public GenericArray(int sz) {
array = (T[]) new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() {
return array;
}
public static void main(String[] args){
GenericArray gai = new GenericArray(10);
// Integer[] ia = gai.rep();
Object[] oa = gai.rep();
System.out.println(oa);
}
}
当程序运行到Integer[] ia = gai.rep();语句时会抛出ClassCastException。因为泛型擦除,在实际运行时数组array的类型是Object[],而要将其转型为Integer[]就会产生异常。所以在上个例子中,较好的方式是将array引用的类型声明为Object[],因为在运行时T[]和Object[]是相同的,而Object[]不会引起误会。当然调用Integer[] ia = gai.rep();依旧会出错的。创建泛型数组的最好方法是持有一个泛型类型标识(Class类的对象),然后使用(T[])Array.newInstance(Class, size);创建数组。
2. 边界
Java泛型重用了extends关键字,它在泛型边界上下文环境中和在普通情况下所具有的意义是完全不同的。
它要点是上界可以指定单独的类或者方法,也可以指定一个类和(一个或者多个)方法,用&符号分隔,此时类必须在最前面,例如:
interface HasColor{
java.awt.Color getColor();
}
class Colored<T extends HasColor>{
T item;
Colored(T item){
this.item = item;
}
T getItem(){
return item;
}
java.awt.Color color(){
return item.getColor();
}
}
class Dimention{
public int x,y,z;
}
class ColoredDimention<T extends Dimention & HasColor>{
T item;
ColoredDimention(T item){
this.item = item;
}
T getTtem(){
return item;
}
java.awt.Color color(){
return item.getColor();
}
int getX(){
return item.x;
}
int getY(){
return item.y;
}
int getZ(){
return item.z;
}
}
interface Weight{
int weight();
}
class Solid<T extends Dimention & HasColor & Weight>{
T item;
Solid(T item){
this.item = item;
}
T getItem(){
return item;
}
java.awt.Color color(){
return item.getColor();
}
int getX(){
return item.x;
}
int getY(){
return item.y;
}
int getZ(){
return item.z;
}
int weight(){
return item.weight();
}
}
class Bounded extends Dimention implements HasColor,Weight{
@Override
public int weight() {
return 0;
}
@Override
public Color getColor() {
return null;
}
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
solid.color();
solid.getY();
solid.weight();
}
}
在这个例子中,展示了边界的基本要素,同时在BasicBounds类中包含可以通过继承消除的冗余。值得注意的是,通配符被限制为单一边界。
3. 通配符
之前也看到一些使用通配符的示例,例如在泛型参数表达式中使用的问号,现在开始入手的示例是展示数组的一种特殊的行为:可以向到处类型的数组赋予基类型的数组引用,首先创建几个用于例子的类,注意它们的继承体系。
class Fruit {
}
class Apple extends Fruit {
}
class Jonathan extends Apple {
}
class Orange extends Fruit{
}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple();
fruit[1] = new Jonathan();
try {
fruit[0] = new Fruit();
} catch (Exception e) {
System.out.println(e);
}
try {
fruit[0] = new Orange();
} catch (Exception e) {
System.out.println(e);
}
}
}
main()中第一行创建了一个Apple数组,并将其赋值给一个Fruit数组引用。这是有意义的,因为Apple也是一种Fruit,因此Apple数组应该也是一个Fruit数组。但是这里实际的数组类型是Apple[],所以只能在其中放置Apple或Apple的子类型,这在编译器和运行时可以工作。但是,编译器允许将Fruit放置到这个数组中,而运行时的数组机制知道它处理的是Apple[],因此会在向数组中放置异构类型时抛出异常。实际上向上转型不适合在这里。数组的行为应该是它可以持有其他对象。
相对于数组,泛型容器不允许这样的向上转型,比如:List flist = new ArrayList();
//这样做编译器是不允许的!
由于泛型不知道类型信息,因此它拒绝向上转型。与数组不同,泛型没有内建的协变类型。数组在语言中完全定义的,因此可以内建了编译期和运行时的检查,但是在使用泛型时,编译器和运行时系统都不知道你想用类型做什么,以及应该采用什么样的规则。
如果想要在两个类型之间建立某种类型的向上转型关系,可以使用通配符:
List<? extends Fruit> fList = new ArrayList<Fruit>();
flist.add(new Apple()); //编译错误
flist.add(new Fruit()); //编译错误
flist.add(new Object());//编译错误
flist.add(null); //正确
引用的类型是List可以将其视为“具有任何从Fruit继承的类型的列表”。但是这实际上并不意味着这个List将持有任何类型的List。通配符引用的是明确的类型,因此它意味着”flist引用持有某种没有指定具体的类型“。可以看到一旦执行了上例子中的向上转型,就会丢失传递任何对象的能力,甚至是Object也不行,只能传入null。
1)超类型通配符
可以声明通配符是由某个特定类的任何基类来界定的,方法是指定,也可以使用类型参数,但不能对泛型参数给出一个超类型边界,即不能声明(我的理解,超类型通配符是在引用类型中使用的,不能再定义中使用)。这使得可以安全的传递一个类型对象到泛型类型中。有了超类型通配符,可以向Collection写入对象了:
class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
//apples.add(new Fruit()); 编译错误
}
}
参数apples是Apple的某种基类型的List,这样就可以知道向其中添加Apple或Apple的子类型是安全的。但是,Apple是下界,那么可以知道向这样的List中添加Fruit是不安全的。可以根据如果能够像一个泛型类型“写入”(传递给一个方法),以及如何能够从一个泛型类型中“读取”(从一个方法中返回),来思考子类型和超类型边界。
下面这个例子表现出超类型边界放松了在可以向方法传递的参数上所做的限制:
public class GenericWriting {
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruits = new ArrayList<Fruit>();
static void f1() {
writeExact(apples, new Apple());
}
static <T> void writeWithWildcard(List<? super T> list, T item) {
list.add(item);
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruits, new Apple());
}
public static void main(String[] args) {
f1();
f2();
}
}
writeExact()方法使用了确切的泛型参数类型(无通配符),它不允许将Apple放置到List中,即使知道这是可以的。writeWithWildcard()方法泛型参数是List<? super T>
,因此这个List将持有从T导出的某种具体类型,这样就可以安全的将一个T类型的对象或者从T导出的任何对象作为参数传递给List方法。
2)无界通配符<?>
无界通配符<?>
看起来意味着“任何事物”,因此使用无界通配符好像等家用使用原生类型。事实上编译器看起来是支持这种判断的:
public class UnboundedWildcards {
static List list1;
static List<?> list2;
static List<? extends Object> list3;
static void assign1(List list){
list1 = list;
list2 = list;
// list3 = list;
}
static void assign2(List<?> list){
list1 = list;
list2 = list;
list3 = list;
}
static void assign3(List<? extends Object> list){
list1 = list;
list2 = list;
list3 = list;
}
public static void main(String[] args) {
assign1(new ArrayList());
assign2(new ArrayList());
assign1(new ArrayList<String>());
assign2(new ArrayList<String>());
assign3(new ArrayList<String>());
List<?> wildlList = new ArrayList<String>();
assign1(wildlList);
assign2(wildlList);
assign3(wildlList);
}
}
看似List<?>
与List相同,但是从上例子中可以看出还是有区别的。实际上它是在声明:“我是想用Java的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型”。当在处理多个泛型参数时,允许一个参数可以是任何类型,同时为其他参数确定某种特定类型有时很重要。
4. 泛型的问题
1)泛型不能持有基本类型,但可以使用对应的包装器类,但是这会在一定程度上影响效率。org.apache.commons.collections.primitives是适配基本类型的容器版本。另外自动包装机制不能用于数组。
2)一个类不能实现同一个泛型接口的两种变体,下面的例子是产生冲突的情况:
interface Payable<T> {
}
class Employee implements Payable<Employee> {
}
class Hourly extends Employee implements Payable<Hourly> {
}
Hourly不能编译,因为擦除会将Payable和Payable简化为相同的类Payable。如果把两个Payable的泛型参数都去掉Hourly可以编译通过。
3)带泛型的参数类型无法作为重载的依据,例如下面的程序是不能通过编译的:
public class UseList<W, T> {
void f(List<T> v){}
void f(LIst<W> v){}
}
由于擦除的原因,重载方法将产生相同的类型签名。以此不同的是,当被擦除的参数不能产生唯一的参数列表时,必须提供明显有区别的方法名。