65.9K
CodeProject 正在变化。 阅读更多。
Home

Java 中的泛型 – 第 II 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (12投票s)

2008 年 7 月 8 日

CPOL

6分钟阅读

viewsIcon

31065

本文讨论了 Java 中泛型的实现方式,并深入探讨了其中的一些问题。

摘要

第一部分中,我们介绍了 Java 5 中泛型的优点和用法。在本部分(第二部分)中,我们将讨论其在 Java 中的实现方式,并深入探讨其中的一些问题。在第三部分中,我们将讨论混合泛型和非泛型代码的问题,以及将非泛型遗留代码转换为泛型的问题。

未检查警告

如果 Java 编译器无法验证类型安全,它会向您发出警告。当您混合使用泛型和非泛型代码(这并不是一个好主意)时,就会出现这种情况。在开发应用程序时忽视这类警告是一种风险。最好将警告视为错误

考虑以下示例

public class Test
{
    public static void foo1(Collection c)
    {
    }

    public static void foo2(Collection<Integer> c)
    {
    }

    public static void main(String[] args)
    {
        Collection<Integer> coll = new ArrayList<Integer>();
        foo1(coll);

        ArrayList lst = new ArrayList();
        foo2(lst);
    }
}

您有一个接受传统Collection作为参数的方法foo1。而方法foo2接受Collection的泛型版本。您正在将一个传统的ArrayList对象发送到方法foo2。由于ArrayList可能包含不同类型的对象,在foo2方法内部,编译器无法保证Collection<span class="code-keyword"><Integer>仅包含Integer的实例。在这种情况下,编译器会发出如下所示的警告。

Warning:  line (22) [unchecked] unchecked conversion found : 
          java.util.ArrayList required: 
java.util.Collection<java.lang.Integer>

虽然收到此警告总比没有收到潜在问题的警报要好,但如果它是一个错误而不是一个警告,那就更好了。请使用编译标志–Xlint确保您不会忽略此警告。

还有另一个问题。在main方法中,您将一个泛型CollectionInteger发送到方法foo1。即使编译器对此没有抱怨,这也是危险的。如果在foo1方法内部向集合中添加了除Integer之外的其他类型对象,会怎么样?这将破坏类型安全。

您可能想知道,编译器最初是如何允许您将泛型类型视为传统类型的。简而言之,原因是字节码级别上不存在泛型的概念。我将在“泛型实现”部分详细介绍这一点。

限制

使用泛型存在许多限制。您不允许创建泛型集合的数组。允许创建通配符集合的数组,但从类型安全的角度来看,这是危险的。您不能创建原始类型的泛型。例如,不允许使用ArrayList<int>。您不允许在泛型类中创建参数化静态字段,或使用参数化类型作为参数的静态方法。例如,考虑以下代码:

class MyClass<T>
{
    private Collection<T> myCol1; // OK
    private static Collection<T> myCol2; // ERROR
}

在泛型类中,您不能实例化参数化类型的对象或对象数组。例如,如果您有一个泛型类MyClass<span class="code-keyword"><T>,在该类的某个方法中,您不能这样写:

new T();

new T[10];

您可以抛出泛型类型的异常;但是,在catch块中,您必须使用特定类型而不是泛型类型。

您可以让您的类继承自另一个泛型类;但是,您不能继承自参数化类型。例如,虽然

class MyClass2<T> extends MyClass<T>
{
}

是 OK 的,

class MyClass2<T> extends T
{
}

则不是。

您不允许继承自同一个泛型类型的两个实例化。例如,虽然

class MyList implements MyCollection<Integer>
{
    //...
}

是 OK 的,

class MyList implements MyCollection<Integer>, MyCollection<Double>
{
    //...
}

则不是。

这些限制的原因是什么?这些限制很大程度上源于泛型的实现方式。通过理解 Java 中实现泛型所使用的机制,您可以了解这些限制的来源以及它们为何存在。

泛型实现

泛型是 Java 语言级别的特性。泛型的设计目标之一是保持字节码级别的二进制兼容性。通过无需更改 JVM 并保持类文件(字节码)的格式不变,您可以轻松地混合使用泛型代码和非泛型代码。然而,这需要付出代价。您可能会失去泛型最初旨在提供的功能——类型安全。

泛型仅是语言级别而不是实际字节码级别的,这重要吗?有两个原因需要关注。一、如果这仅仅是语言级别的特性,当有其他语言被期望在 JVM 上运行时会发生什么?如果其他在 JVM 上运行的语言是动态语言(Groovy、Ruby、Python 等),那么这可能不是什么大问题。但是,如果您尝试在 JVM 上运行强类型语言,这可能会成为一个问题。二、如果这仅仅是一个语言级别的特性(本质上是一个非常强大的宏),那么使用反射等方式,就可以在运行时传入正确的类型。

不幸的是,Java 中的泛型并未提供足够的类型安全。它并未完全实现其创建的初衷。

擦除

那么,如果泛型是语言级别的特性,当您编译泛型代码时会发生什么?您的代码将被剥离所有参数化类型,并且对参数化类型的每个引用都将被替换为一个类(通常是Object或更具体的类型)。这个过程有一个花哨的名字——类型擦除。

根据文档:“这种方法的主要优点是它提供了泛型代码与使用非参数化类型(技术上称为原始类型)的遗留代码之间的完全互操作性。主要缺点是参数类型信息在运行时不可用,并且在与行为不当的遗留代码互操作时,自动生成的强制类型转换可能会失败。但是,有一种方法可以实现对泛型集合的保证运行时类型安全,即使在与行为不当的遗留代码互操作时也是如此。”

虽然这提供了泛型代码和非泛型代码的互操作性,但它不幸地损害了类型安全。让我们看看擦除对您的代码的影响。

考虑示例代码

class MyList<T>
{
    public T ref;
}

通过运行javap –c,您可以查看字节码内容,如下所示:

javap -c MyList
Compiled from "Test.java"
class com.agiledeveloper.MyList extends java.lang.Object{
public java.lang.Object ref;

com.agiledeveloper.MyList();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

类成员ref的类型T已被擦除为(替换为)Object类型。

并非所有类型都会始终被擦除为或替换为Object。请看这个例子:

class MyList<T extends Vehicle>
{
    public T ref;
}

在这种情况下,类型T被替换为Vehicle,如下所示:

javap -c MyList
Compiled from "Test.java"
class com.agiledeveloper.MyList extends java.lang.Object{
public com.agiledeveloper.Vehicle ref;

com.agiledeveloper.MyList();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

现在,考虑这个例子:

class MyList<T extends Comparable>
{
    public T ref;
}

在这里,类型T被替换为Comparable接口。

最后,如果您使用多界限约束,如下所示:

class MyList<T extends Vehicle & Comparable>
{
    public T ref;
}

那么类型T将被替换为Vehicle。多界限约束中的第一个类型将用作擦除中的类型。

擦除的影响

让我们看看擦除对使用泛型类型的代码的影响。考虑示例:

ArrayList<Integer> lst  = new ArrayList<Integer>();
lst.add(new Integer(1));
Integer val = lst.get(0);

这被翻译为:

ArrayList lst = new ArrayList();
lst.add(new Integer(1));
Integer val = (Integer) lst.get(0);

当您将lst.get(0)分配给val时,在翻译后的代码中会执行类型转换。如果您在不使用泛型的情况下编写代码,您也会这样做。在这方面,Java 中的泛型仅仅充当了语法糖。

我们现在在哪里?

我们已经讨论了 Java 中泛型的处理方式。我们探讨了类型安全的程度。我们将在下一部分(第三部分)中讨论更多与泛型相关的问题。

结论

Java 中的泛型是为了提供类型安全而创建的。它们仅在语言级别实现。这个概念没有向下传递到字节码级别。它旨在提供与遗留代码的兼容性。因此,泛型缺乏它们原本的目标——类型安全。

参考文献

  1. Java 中的泛型,第一部分
© . All rights reserved.