[译] 泛型 6.与老式代码交互

本文探讨了在Java中,泛型代码如何与非泛型的老式代码进行交互,包括在泛型代码中使用老式代码和在老式代码中使用泛型代码的情况。解释了原始类型和未经检查的警告的概念,以及Java编译器如何通过擦除来处理泛型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

<< 泛型方法 · 目录 · 细则 >>

                                                                                  

与老式代码交互 (Interoperating with Legacy Code)

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

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

稍后,在 转换老式代码为使用泛型 一节中,我们将处理将老式代码转换为使用泛型的问题。在本节,我们将关注一个更简单的问题:老式代码与泛型代码怎样交互?这个问题有两个部分:在泛型代码中使用老式代码和在老式代码中使用泛型代码。

在泛型代码中使用老式代码

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

举个例子,假设您想要使用com.Example.widgets包。在Example.com上,人们销售一种库存控制系统,其要点如下所示:

package com.Example.widgets;

public interface Part {...}

public class Inventory {
    /**
     * 向库存数据库添加一个新的零件组合Assembly
     * 该零件组合给定名称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集合。当然,泛型是为此量身定做的:

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

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

但是等等,那也不太对!考虑对getParts()的调用,它返回一个Collection。然后将其分配给k,是一个Collection<Part>类型。如果调用的结果是Collection<?>,则分配将是一个错误。

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

所以,这不应该是个错误吗?理论上讲是的,但实际上,如果泛型代码要调用老式代码,则必须允许这样做。由程序员自己决定,在这种情况下,分配是安全的,因为getAssembly()的契约说它返回部件集合,尽管类型签名没有显示这一点。

所以原始类型非常类似于通配符类型,但是它们没有严格地进行类型检查。这是一个经过深思熟虑的设计决策,允许泛型与已有的老式代码进行交互。

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

目前非泛型代码比泛型代码多得多,而且不可避免地会出现它们必须混合的情况。

如果发现必须混合老式代码和泛型代码,请密切注意未经检查的警告。仔细考虑如何证明引起警告的代码的安全性。

如果您仍然犯了一个错误,并且导致警告的代码确实不安全,那么会发生什么情况呢?让我们来看看这种情况。在此过程中,我们将些许深入了解编译器的工作原理。

擦除和翻译

public String loophole(Integer x) {
    List<String> ys = new LinkedList<String>();
    List xs = ys;
    xs.add(x); // 编译时 uncheck 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)的前端转换。您可以(几乎)将其视为源到源(source-to-source)的转换,从而将泛型版本的loophole()转换为非泛型版本。

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

基本上,擦除会消除所有泛型类型信息。所有的尖括号之间的类型信息都被抛弃,因此,例如,将List<String>这样的参数化类型转换为List。类型变量的所有剩余用途都被替换成类型变量的上界(通常是Object)。而且,每当生成的代码类型不正确时,就会插入合适的类型转换,就像loophole的最后一行一样。

“擦除”的全部细节超出了本教程的范围,但我们刚才给出的简单描述与事实不相上下。了解一下这一点很好,特别是如果您想做更复杂的事情,比如转换现有的API以使用泛型(请参阅 转换老式代码为使用泛型 小节),或者只是想了解为什么事情是这样的。

在老式代码中使用泛型

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

package com.Example.widgets;

public interface Part { 
    ...
}

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

public interface Assembly {
    // 返回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.Examp e.widget包和集合库,这两个库都使用泛型类型。客户端代码中所有使用泛型类型声明的都是原始类型。

第1行生成未经检查的警告,因为在需要Part集合的地方传递了原始集合,而且编译器不能确保原始集合确实是Part集合。

另一种选择是,您可以使用源1.4标志编译客户机代码,以确保不会生成警告。但是,在这种情况下,您将无法使用JDK5.0中引入的任何新语言特性。

                                                                                  

<< 泛型方法 · 目录 · 细则 >>

本文译自:https://docs.oracle.com/javase/tutorial/extra/generics/legacy.html

转载于:https://my.oschina.net/tita/blog/2886664

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值