与旧版代码互操作

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

las,实际上并非如此。数百万行代码已用该语言的早期版本编写,并且不会一 overnight 而就地转换。

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

在通用代码中使用旧版代码

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

例如,假设您要使用软件包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之类的泛型时,称为原始类型。

大多数人的第一个直觉是Collection的意思是Collection<Object>。但是,正如我们前面所看到的,在需要Collection<Object>的地方传递Collection<Part>是不安全的。准确地说,类型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); // Compile-time unchecked warning
    return ys.iterator().next();
}

在这里,我们为一个字符串 列表和一个普通的旧列表加上了别名。我们将Integer插入列表,然后try提取String。这显然是错误的。如果我们忽略警告并try执行此代码,则它将在try使用错误类型的时候完全失败。在运行时,此代码的行为类似于:

public String loophole(Integer x) {
    List ys = new LinkedList;
    List xs = ys;
    xs.add(x); 
    return(String) ys.iterator().next(); // run time error
}

当我们从列表中提取一个元素,并try通过将其转换为String来将其视为字符串 时,我们将获得ClassCastException。通用版本loophole()完全相同。

这样做的原因是,泛型由 Java 编译器实现为称为“擦除”的前端转换。您可以(几乎)将其视为源到源的转换,从而将loophole()的通用版本转换为非通用版本。

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

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

擦除的全部细节不在本教程的讨论范围之内,但是我们刚刚给出的简单描述与事实并非遥不可及。最好对此有所了解,特别是如果您想做更复杂的事情,例如将现有的 API 转换为使用泛型(请参见将旧版代码转换为使用泛型部分),或者只是想了解事情为何如此。

在旧版代码中使用通用代码

现在让我们考虑相反的情况。想象一下 Example.com 选择将其 API 转换为使用泛型,但其中一些 Client 端尚未使用。因此,现在的代码如下所示:

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();
}

Client 端代码如下所示:

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();
    }
}

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

第 1 行生成未经检查的警告,因为在期望Part s 的Collection处传递了原始Collection,并且编译器无法确保原始Collection确实是Part s 的Collection

或者,您可以使用源 1.4 标志编译 Client 端代码,以确保不生成任何警告。但是,在这种情况下,您将无法使用 JDK 5.0 中引入的任何新语言功能。