Java 中的泛型 – 第 II 部分






4.63/5 (12投票s)
本文讨论了 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
方法中,您将一个泛型Collection
的Integer
发送到方法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 中的泛型是为了提供类型安全而创建的。它们仅在语言级别实现。这个概念没有向下传递到字节码级别。它旨在提供与遗留代码的兼容性。因此,泛型缺乏它们原本的目标——类型安全。