泛型或:我如何开始担心和讨厌类型擦除
当你通过编译的源代码给它一系列bytecodes/literals 时,你的 JVM 试图确定你的意思。 “通常”使用具体的类类型(如String)或具体的原始类型(如int)很好地声明所有内容。例如,6 的值带有一些元数据,告诉 JVM 它是 int 文字而不是其他类型。一系列字节本身没有意义;您需要一种方法来告诉 JVM 二进制数实际编码的内容。相同的二进制值可以代表无限数量的想法或状态,因此包含类型元数据以帮助理解这一切非常重要。如果没有类型,JVM 将无法区分一个值为'a' 的char 和一个值为0x61 的字节。当您提供泛型类型时,例如List 接口的实现所使用的类型(例如您使用的ArrayList),您是在告诉编译器“这不是特定的具体类型,它可以是任何东西”(尽管使用extends 你可以稍微限制一下)。现在,通常这可能不是问题,但由于我不会涉及的向后兼容性问题,动态类型在称为类型擦除的过程中被删除(查看this answer 以获得非常详细的推理解释) .解决您的问题的简短而甜蜜的答案是简单地考虑泛型类型的额外复杂性,并积极检查和防止不允许的强制转换。因此,类型安全主要可以通过手动检查泛型类型可分配性来实现,而不是像使用具体类型时那样依赖自动类型安全。
示例方法
这是一个相对类型安全的代码版本(在最坏的情况下,您将收到运行时错误消息)检查和泛型类型边界(ObjectInputStream 和 ObjectOutputStream 使用来自 @987654338 的方法@ 接口来实现它们对流的Object 实例的读写)。
public static <OUTPUT extends Serializable, INPUT extends Serializable>
OUTPUT getObjectFromFile(Class<INPUT> type, CharSequence fileName)
throws IOException, ClassNotFoundException {
final String filenameString = Objects.requireNonNull(fileName).toString();
final Class<INPUT> inputType = Objects.requireNonNull(type);
try (ObjectInput ois = new ObjectInputStream(new FileInputStream(filenameString))) {
final Object rawObject = ois.readObject();
@SuppressWarnings("unchecked")
final OUTPUT output = (OUTPUT) (rawObject == null ? null : inputType.cast(rawObject));
return output;
}
}
public static <DESIRED extends Serializable, STORED extends Serializable>
boolean addArrayListFromFile(Collection<DESIRED> out, Class<STORED> type, CharSequence fileName)
throws IOException, ClassNotFoundException {
@SuppressWarnings("unchecked")
final ArrayList<DESIRED> inList = getObjectFromFile(type, fileName);
return inList != null && out.addAll(inList);
}
更多详情
这些方法从文件中读取序列化的对象,并通过方法参数指定类型安全和文件位置的细节。顶层方法的返回值实际上是一个boolean表示成功或失败(true是成功),但是实际输出的数据是写入到被调用方提供的Collection实例中。与可以使用熟悉的a = (a) b; 语法安全地显式指定强制转换的非泛型方法不同,具有泛型类型(如DESIRED、STORED、INPUT 和OUTPUT)的泛型方法不能完全确定他们将处理什么类型。他们通过称为类型擦除的过程丢失了有关类型的这些信息。
通过类型擦除,开发人员和 JVM 运行时会丢失有关泛型类型的具体类型的元数据。结果,运行时和开发人员受制于可分配给所需输出类型的输入类型。只有编译器甚至可能具有强制转换的可证明性,并且它不能像验证具体类型那样在一般意义上验证泛型类型。通常情况下,直到运行时才可能进行类型安全检查。运行时的 List<?> 可能实际上已被编译和编程为 List<String> ,但我们无法在运行时确定两个列表之间的差异,因为编译器无法维护或确定我们需要做的信息所以。
除了类型擦除导致类型元数据丢失之外,编译器还可能对程序的运行时类型图了解不完整。诸如链接引入我们当前编译器无法识别的新类型之类的可能性意味着我们永远无法避免使用完全算法方法对泛型类型进行不安全强制转换。事实上,对于泛型类型,没有办法以可概括的方式满足这种类型安全问题,因为它相当于一个布尔可满足性问题。但是,我们可以在其他地方获得有关方法调用者想要和提供的类型的具体信息……开发人员!开发人员必须为泛型类型指定明确的意图以提供类型安全。通过让方法调用者(最终是开发人员)负责通过将Class<T> 实例传递给代码/方法来告诉运行时期望什么,我们可以尝试以避免ClassCastException 的方式检查泛型类型.
如果我们有一个List<T>,它就变成了一个List,所以我们不能再识别为与任何其他List 不同的类型。但是,Class<T> 并非如此,我们可以利用它在编译器周围传递类型元数据并进入运行时(因为 Class 对象将类型信息存储在对象图中,而不是专门使用 Java 的类型系统)。然而,最终让程序员负责为每个方法调用提供正确的Class<T>。通过使运行时检查类型的有效性,编译器在编译期间检测类型安全错误的能力主要牺牲了使用泛型类型的表达式。编译时不会再收到“int 不能转换为String”之类的警告,而只会收到应用程序异常或必须在运行时适当处理的转换安全验证失败。
如果你想玩弄一些东西,看看当你使用这个方法时会发生什么,我在 GitHub 上创建了a gist,其中包含一个带有 main 方法的简短的小单文件类,你可以作为一个小测试程序运行。