文档

Java™教程
隐藏目录
与旧代码互操作
路径:额外奖励
课程:泛型

与旧代码互操作

到目前为止,我们所有的例子都假设了一个理想化的世界,每个人都在使用支持泛型的最新版本的Java编程语言。

然而,现实情况并非如此。已经有数百万行的代码是用早期版本的语言编写的,而且不可能一夜之间全部转换。

稍后,在将旧代码转换为使用泛型一节中,我们将解决将旧代码转换为使用泛型的问题。在本节中,我们将专注于一个更简单的问题:如何使旧代码和泛型代码可以互操作?这个问题有两个部分:在泛型代码中使用旧代码和在旧代码中使用泛型代码。

在泛型代码中使用旧代码

在你自己的代码中如何使用旧代码,同时又享受泛型带来的好处呢?

以一个例子来说明,假设你想使用com.Example.widgets这个包。Example.com的人们市场上有一个用于库存控制的系统,其中一些亮点如下:

package com.Example.widgets;

public interface Part {...}

public class Inventory {
    /**
     * 向库存数据库中添加新的组件。
     * 组件的名称为name,由parts指定。
     * parts集合中的所有元素必须支持Part接口。
     **/ 
    public static void addAssembly(String name, Collection parts) {...}
    public static Assembly getAssembly(String name) {...}
}

public interface Assembly {
    // 返回一个Parts的集合
    Collection getParts();
}

现在,你想添加一个使用上述API的新代码。最好能确保你总是以正确的参数调用addAssembly(),也就是说,传递给它的集合确实是Part类型的Collection。当然,泛型就是为此而生:

package com.mycompany.inventory;

import com.Example.widgets.*;

public class Blade implements Part {
    ...
}

public class Guillotine implements Part {
}

public class Main {
    public static void main(String[] args) {
        Collection<Part> c = new ArrayList<Part>();
        c.add(new Guillotine()) ;
        c.add(new Blade());
        Inventory.addAssembly("thingee", c);
        Collection<Part> k = Inventory.getAssembly("thingee").getParts();
    }
}

当我们调用addAssembly时,它期望第二个参数的类型是Collection。而实际的参数是Collection<Part>类型。这样是行得通的,但为什么呢?毕竟,大多数集合不包含Part对象,所以一般来说,编译器无法知道类型Collection指的是什么类型的集合。

在合适的泛型代码中,Collection 总是伴随着一个类型参数。当一个泛型类型像 Collection 在没有类型参数的情况下使用时,它被称为 原始类型

大多数人的第一直觉是 Collection 实际上意味着 Collection<Object>。然而,正如我们之前看到的,将一个 Collection<Part> 传递给需要 Collection<Object> 的地方是不安全的。更准确地说,类型 Collection 表示一种未知类型的集合,就像 Collection<?>

但是等等,这也不对!考虑对 getParts() 的调用,它返回一个 Collection。然后将其赋值给 k,它是一个 Collection<Part>。如果调用的结果是 Collection<?>,赋值将会出错。

实际上,这个赋值是合法的,但是它会生成一个 未经检查的警告。这个警告是必要的,因为事实是编译器无法保证其正确性。我们无法检查 getAssembly() 中的遗留代码,以确保返回的确实是 Part 的集合。代码中使用的类型是 Collection,人们可以合法地将各种对象插入这样的集合中。

那么,这不应该是一个错误吗?理论上来说,是的;但实际上,如果泛型代码要调用遗留代码,这是必须允许的。由你作为程序员来满足自己,在这种情况下,赋值是安全的,因为 getAssembly() 的契约规定它返回一种 Part 的集合,尽管类型签名没有显示这一点。

因此,原始类型与通配符类型非常相似,但它们的类型检查没有那么严格。这是一个故意的设计决策,允许泛型与现有的遗留代码互操作。

从泛型代码调用遗留代码本质上是危险的;一旦将泛型代码与非泛型遗留代码混合使用,通常提供的泛型类型系统的所有安全保证都将失效。然而,相比不使用泛型,你仍然更好一些。至少你知道自己的代码是一致的。

目前,非泛型代码远远超过泛型代码,不可避免地会出现它们必须混合使用的情况。

如果发现必须混合使用遗留代码和泛型代码,请密切关注未经检查的警告。仔细思考如何证明引发警告的代码的安全性。

如果你仍然犯了一个错误,导致警告的代码确实不安全,会发生什么呢?让我们看看这样的情况。在这个过程中,我们将对编译器的工作原理有一些了解。

擦除和转换

public String loophole(Integer x) {
    List<String> ys = new LinkedList<String>();
    List xs = ys;
    xs.add(x); // 编译时未检查警告
    return ys.iterator().next();
}

在这里,我们给字符串列表和普通列表起了一个别名。我们将一个Integer插入到列表中,并尝试提取一个String。这显然是错误的。如果我们忽略警告并尝试执行这段代码,它将在我们尝试使用错误类型的地方失败。在运行时,这段代码的行为如下:

public String loophole(Integer x) {
    List ys = new LinkedList;
    List xs = ys;
    xs.add(x); 
    return(String) ys.iterator().next(); // 运行时错误
}

当我们从列表中提取一个元素,并尝试将其强制转换为String时,我们将得到一个ClassCastException。与泛型版本的loophole()发生的事情完全相同。

之所以会这样,是因为泛型在Java编译器中是通过一种称为擦除的前端转换来实现的。你可以(几乎)把它看作是一种源代码到源代码的转换,其中泛型版本的loophole()被转换为非泛型版本。

结果是,即使存在未检查的警告,Java虚拟机的类型安全性和完整性也不会受到威胁

基本上,擦除会去掉(或擦除)所有的泛型类型信息。所有的尖括号之间的类型信息都被丢弃,所以,例如,一个参数化类型如List<String>被转换为List。所有剩余的类型变量的使用都被替换为类型变量的上界(通常是Object)。并且,每当生成的代码不符合类型规范时,会插入一个适当类型的强制转换,就像loophole的最后一行那样。

关于擦除的详细信息超出了本教程的范围,但我们刚刚给出的简单描述并不远离真相。了解一些关于这个的知识是很好的,特别是如果你想要做一些更复杂的事情,比如将现有的API转换为使用泛型(参见将遗留代码转换为使用泛型部分),或者只是想要理解为什么事情会变成现在这个样子。

在遗留代码中使用泛型代码

现在让我们考虑相反的情况。假设Example.com选择将他们的API转换为使用泛型,但其中一些客户端还没有这样做。所以现在代码看起来像:

package com.Example.widgets;

public interface Part { 
    ...
}

public class Inventory {
    /**
     * 将一个新的组件添加到库存数据库中。
     * 组件的名称是name,并且由parts指定的一组零件组成。
     * 集合parts的所有元素必须支持Part接口。
     **/ 
    public static void addAssembly(String name, Collection<Part> parts) {...}
    public static Assembly getAssembly(String name) {...}
}

public interface Assembly {
    // 返回一组零件
    Collection<Part> getParts();
}

客户端代码如下:

 com.mycompany.inventory;

导入 com.Example.widgets.*;

public class Blade implements Part {
...
}

public class Guillotine implements Part {
}

public class Main {
    public static void main(String[] args) {
        Collection c = new ArrayList();
        c.add(new Guillotine()) ;
        c.add(new Blade());

        // 1:未检查的警告
        Inventory.addAssembly("thingee", c);

        Collection k = Inventory.getAssembly("thingee").getParts();
    }
}

客户端代码是在引入泛型之前编写的,但它使用了com.Example.widgets包和集合库,两者都使用了泛型类型。客户端代码中所有的泛型类型声明都是原始类型。

第1行生成了一个未检查的警告,因为正在传递一个原始的Collection,而期望的是一个PartCollection,编译器无法确保原始的Collection真的是PartCollection

作为一种替代方案,您可以使用source 1.4标志编译客户端代码,以确保不会生成任何警告。然而,在这种情况下,您将无法使用JDK 5.0引入的任何新语言特性。



上一页: 泛型方法
下一页: 详细内容