文档

Java™ 教程-Java Tutorials 中文版
与旧代码互操作
Trail: Bonus
Lesson: Generics

与旧代码互操作

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

唉,实际情况并非如此。在早期版本的语言中已经编写了数百万行代码,并且它们不会在一夜之间全部转换。

稍后,在 Converting Legacy Code to Use Generics 部分中,我们将解决将旧代码转换为使用泛型的问题。在本节中,我们将关注一个更简单的问题:遗留代码和泛型代码如何互操作?这个问题有两个部分:在泛型代码中使用遗留代码和在遗留代码中使用泛型代码。

在泛型代码中使用旧代码

如何使用旧代码,同时仍然在自己的代码中享受泛型的好处?

例如,假设你要使用包 com.Example.widgets。Example.com 的人们推出了一个库存控制系统,其亮点如下所示:

package com.Example.widgets;

public interface Part {...}

public class Inventory {
    /**
     * Adds a new Assembly to the inventory database.
     * The assembly is given the name name, and 
     * consists of a set parts specified by parts. 
     * All elements of the collection parts
     * must support the Part interface.
     **/ 
    public static void addAssembly(String name, Collection parts) {...}
    public static Assembly getAssembly(String name) {...}
}

public interface Assembly {
    // Returns a collection of Parts
    Collection getParts();
}

现在,你想要添加使用上述 API 的新代码。确保你始终使用正确的参数调用 addAssembly() 会很好 - 也就是说,你传入的集合确实是 PartCollection 。当然,泛型是为此量身定制的:

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 的泛型类型而没有类型形参时,它被称为 raw type (原始类型)

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

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

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

那么,这不应该是一个错误吗?从理论上讲,是的;但实际上,如果泛型代码要调用遗留代码,则必须允许这样做。程序员可以自己确定,在这种情况下,赋值是安全的,因为 getAssembly() 的契约表示它返回 Part 的集合,即使类型签名没有显示这一点。

因此原始类型非常类似于通配符类型,但它们并没有严格地进行类型检查。这是一个深思熟虑的设计决策,允许泛型与预先存在的遗留代码进行互操作。

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

目前有非常多的非泛型代码,然后有泛型代码,并且不可避免地会出现需要混合的情况。

如果你发现必须混合使用旧代码和泛型代码,请密切注意未经检查的警告。仔细考虑如何证明产生警告的代码的安全性。

如果你仍然犯了错误会发生什么,导致警告的代码确实不是类型安全的?我们来看看这种情况。在此过程中,我们将深入了解编译器的工作原理。

擦除和翻译

public String loophole(Integer x) {
    List<String> ys = new LinkedList<String>();
    List xs = ys;
    xs.add(x); // Compile-time unchecked warning
    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(); // run time error
}

当我们从列表中提取元素并尝试通过将其转换为 String 将其视为字符串时,我们将得到 ClassCastException。与 loophole() 的泛型版本完全相同。

原因是,Java 编译器将泛型实现为名为 erasure (擦除) 的前端转换。你可以(几乎)将其视为源到源的转换,其中 loophole() 的泛型版本将转换为非泛型版本。

因此, 即使存在未经检查的警告,Java 虚拟机的类型安全性和完整性也不会存在风险

基本上,擦除消除了(或 erases (擦除))所有泛型类型信息。抛弃尖括号之间的所有类型信息,例如,像 List<String> 这样的参数化类型被转换为 List。类型变量的所有剩余使用都由类型变量的上界替换(通常为 Object)。并且,只要结果代码不是类型正确的,就会插入适当类型的强制转换,如 loophole 的最后一行。

擦除的全部细节超出了本教程的范围,但我们刚刚给出的简单描述与事实相差无几。了解一点这一点很好,特别是如果你想做更复杂的事情,比如将现有的 API 转换为使用泛型(参见 Converting Legacy Code to Use Generics 部分),或者只是想了解为什么事情是这样的。

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

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

package com.Example.widgets;

public interface Part { 
    ...
}

public class Inventory {
    /**
     * Adds a new Assembly to the inventory database.
     * The assembly is given the name name, and 
     * consists of a set parts specified by parts. 
     * All elements of the collection parts
     * must support the Part interface.
     **/ 
    public static void addAssembly(String name, Collection<Part> parts) {...}
    public static Assembly getAssembly(String name) {...}
}

public interface Assembly {
    // Returns a collection of Parts
    Collection<Part> getParts();
}

客户端代码如下所示:

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 c = new ArrayList();
        c.add(new Guillotine()) ;
        c.add(new Blade());

        // 1: unchecked warning
        Inventory.addAssembly("thingee", c);

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

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

第 1 行生成未经检查的警告,因为正在传递原始 Collection,其中需要 PartCollection,并且编译器无法确保原始 Collection 确实是 PartCollection

作为替代方法,你可以使用 source 1.4 标志编译客户端代码,确保不生成任何警告。但是,在这种情况下,你将无法使用 JDK 5.0 中引入的任何新语言功能。



Previous page: Generic Methods
Next page: The Fine Print