Java:原始类型与泛型

Java: Raw Types vs. Generics

考虑下面的示例代码

/* The new Java 5 or later Generic code */
class TestInserter {
    public static void main(String[] ar) {
        List<Integer> myList = new ArrayList<Integer>();
        myList.add(20);
        myList.add(42);
        Inserter ins = new Inserter();
        ins.insert(myList);
    }
}

/* Legacy Code */
class Inserter {
    public void insert(List list) {
        list.add(new Integer(55));
    }
}

编译和执行上面的代码 运行 很好,编译器或 JVM 没有任何抱怨。 non type-safe insert() 方法在我们的 type-safe ArrayList 中插入一个新的 Integer 对象。但是,如果我将插入方法更改为如下所示:

public void insert(List list) {
        list.add(new String("55"));
}

说什么?!上面的代码会运行吗?想一想,是的,肯定会 运行,很奇怪,但是是的,上面的代码可以编译并且 运行 很好。 这与 Arrays 有点不同,Arrays 为您提供编译时间和 运行 时间保护,并防止此类事情发生。他们为什么这样做泛型?为什么 Java 允许泛型输入指定类型以外的值?!

现在来回答。使用 insert 方法将 Integer 添加到我们的列表中是非常安全和允许的,因为它与我们为 myList 变量指定的类型相匹配。但是当我们试图将一个 String 插入到一个只包含 Integer 值的 ArrayList 中时,就会出现问题, 不是在编译时而是在 运行time,当您尝试在错误添加的 String 实例上调用特定于 Integer 的方法时。

为了理解整个问题及其目的,您应该了解一件事 - JVM 不知道您试图将 String 插入 ArrayList 意味着仅包含 Integers。 您所有的泛型及其类型安全性 仅限于编译时 。通过称为“type erasure”的过程,编译器从泛型代码中删除所有类型参数。换句话说,即使你写了这样的东西:

List<Integer> myList = new ArrayList<>();

编译完成后变成如下:

List myList = new ArrayList();

但是,为什么他们要保留这样的泛型?

答案很简单!如果不是因为这种奇怪的行为,那么 Java 早期版本的遗留代码就会被破坏,百万 Java 开发人员将不得不编辑数万亿的旧 Java 代码让它再次工作!!

但不要责怪编译器;当您尝试 运行 代码时,编译器会尝试向您发出以下警告:

$> javac TestInserter.java  
Note: TestInserter.java uses unchecked or unsafe operations.  
Note: Recompile with -Xlint:unchecked for details.

当你按照编译器要求你做如下:

$> javac -Xlint:unchecked TestInserter.java
TestInserter.java:15: warning: [unchecked] unchecked call to add(E) as a member of the raw type List  
        list.add(new String("55"));  
                 ^  
where E is a type-variable:  
  E extends Object declared in interface List  
1 warning    

就编译器而言,它试图告诉您它怀疑您程序中的某些代码可能最终会遇到麻烦。

总而言之,将泛型视为编译时保护。编译器使用类型信息(参数中指定的类型)来确保您不会将错误的内容插入到集合(或用户定义的泛型类型)中,并且您不会从错误的引用类型中获取值。 所有通用保护都是编译时的!就是这样。

编译时泛型涵盖几乎所有需要泛型类型断言的情况。对于需要在 运行 时间内设计泛型类型安全性的极少数情况,您可以存储一个 运行 时间类型令牌 (RTTT),一个实例Class<T> 其中 T 是通用参数。

public class Foo<T> {
  private final Class<T> rttt;
  public Foo(Class<T> rttt) {
    if (rttt == null) {
      throw new IllegalArgumentException("null rttt");
    }
    this.rttt = rttt;
  }
  // etc. RTTT handles runtime type safety
}

除了大多数时候不需要 运行time 泛型之外,Java 在引入泛型时遇到了问题。让它们 运行time enforceable ("reifiable") 要么破坏一堆遗留代码,要么使语言复杂得可怕。默认情况下使泛型仅在编译时允许遗留代码作为原始类型存在并且仍然与编写更好的代码共存。

那里有一个妥协的因素,例如编写原始类型的诱惑。不要那样做。有时我们不得不绕过未经检查的转换。但它几乎总能完成工作。