Java 中文官方教程 2022 版(六)(3)https://developer.aliyun.com/article/1486309
类型擦除和桥接方法的影响
原文:
docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html
有时类型擦除会导致一个你可能没有预料到的情况。以下示例展示了这种情况是如何发生的。有时编译器会在类型擦除过程中创建一个合成方法,称为桥接方法。
给定以下两个类:
public class Node<T> { public T data; public Node(T data) { this.data = data; } public void setData(T data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node<Integer> { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } }
考虑以下代码:
MyNode mn = new MyNode(5); Node n = mn; // A raw type - compiler throws an unchecked warning n.setData("Hello"); // Causes a ClassCastException to be thrown. Integer x = mn.data;
经过类型擦除后,这段代码变成:
MyNode mn = new MyNode(5); Node n = mn; // A raw type - compiler throws an unchecked warning // Note: This statement could instead be the following: // Node n = (Node)mn; // However, the compiler doesn't generate a cast because // it isn't required. n.setData("Hello"); // Causes a ClassCastException to be thrown. Integer x = (Integer)mn.data;
下一节将解释为什么在 n.setData("Hello");
语句处抛出 ClassCastException
。
桥接方法
当编译一个继承参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分。通常情况下,你不需要担心桥接方法,但如果在堆栈跟踪中出现一个,你可能会感到困惑。
经过类型擦除后,Node
和 MyNode
类变成:
public class Node { public Object data; public Node(Object data) { this.data = data; } public void setData(Object data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } }
经过类型擦除后,方法签名不匹配;Node.setData(T)
方法变成了 Node.setData(Object)
。因此,MyNode.setData(Integer)
方法不会覆盖 Node.setData(Object)
方法。
为了解决这个问题并在类型擦除后保留泛型类型的多态性,Java 编译器生成一个桥接方法来确保子类型化按预期工作。
对于 MyNode
类,编译器为 setData
生成了以下桥接方法:
class MyNode extends Node { // Bridge method generated by the compiler // public void setData(Object data) { setData((Integer) data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } // ... }
桥接方法 MyNode.setData(object)
委托给原始的 MyNode.setData(Integer)
方法。因此,n.setData("Hello");
语句调用了 MyNode.setData(Object)
方法,由于 "Hello"
无法转换为 Integer
,导致抛出 ClassCastException
。
非可实例化类型
原文:
docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html
章节 类型擦除 讨论了编译器删除与类型参数和类型参数相关的信息的过程。类型擦除对于具有非可实例化类型的可变参数(也称为 varargs)方法有相关后果。有关可变参数方法的更多信息,请参见 传递信息给方法或构造函数 中的 任意数量的参数 章节。
本页涵盖以下主题:
- 非可实例化类型
- 堆污染
- 具有非可实例化形式参数的可变参数方法的潜在漏洞
- 防止具有非可实例化形式参数的可变参数方法产生警告
非可实例化类型
可实例化 类型是一种在运行时完全可用的类型信息的类型。这包括原始类型、非泛型类型、原始类型和未绑定通配符的调用。
非可实例化类型 是在编译时通过类型擦除删除了信息的类型 —— 未定义为未限定通配符的泛型类型的调用。非可实例化类型在运行时不具备所有信息。非可实例化类型的示例包括 List
和 List
;JVM 无法在运行时区分这些类型。如 泛型的限制 所示,有一些情况下不能使用非可实例化类型:例如,在 instanceof
表达式中,或作为数组中的元素。
堆污染
堆污染 发生在参数化类型的变量引用不是该参数化类型的对象时。如果程序执行了一些操作导致在编译时产生未经检查的警告,则会出现这种情况。如果在编译时(在编译时类型检查规则的限制范围内)或在运行时无法验证涉及参数化类型的操作的正确性(例如,强制转换或方法调用),则会生成 未经检查的警告。例如,当混合使用原始类型和参数化类型,或执行未经检查的强制转换时,就会发生堆污染。
在正常情况下,当所有代码同时编译时,编译器会发出未经检查的警告,以引起您对潜在的堆污染的注意。如果您分别编译代码的各个部分,很难检测到堆污染的潜在风险。如果确保您的代码在没有警告的情况下编译通过,那么就不会发生堆污染。
具有非可实例化形式参数的可变参数方法的潜在漏洞
包含可变参数输入参数的泛型方法可能导致堆污染。
考虑以下ArrayBuilder
类:
public class ArrayBuilder { public static <T> void addToList (List<T> listArg, T... elements) { for (T x : elements) { listArg.add(x); } } public static void faultyMethod(List<String>... l) { Object[] objectArray = l; // Valid objectArray[0] = Arrays.asList(42); String s = l[0].get(0); // ClassCastException thrown here } }
以下示例HeapPollutionExample
使用了ArrayBuiler
类:
public class HeapPollutionExample { public static void main(String[] args) { List<String> stringListA = new ArrayList<String>(); List<String> stringListB = new ArrayList<String>(); ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine"); ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve"); List<List<String>> listOfStringLists = new ArrayList<List<String>>(); ArrayBuilder.addToList(listOfStringLists, stringListA, stringListB); ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!")); } }
编译时,ArrayBuilder.addToList
方法的定义会产生以下警告:
warning: [varargs] Possible heap pollution from parameterized vararg type T
当编译器遇到可变参数方法时,它将可变参数形式参数转换为数组。然而,Java 编程语言不允许创建参数化类型的数组。在方法ArrayBuilder.addToList
中,编译器将可变参数形式参数T... elements
转换为形式参数T[] elements
,一个数组。然而,由于类型擦除,编译器将可变参数形式参数转换为Object[] elements
。因此,存在堆污染的可能性。
以下语句将可变参数形式参数l
赋给Object
数组objectArgs
:
Object[] objectArray = l;
这个语句可能会引入堆污染。一个与可变参数形式参数l
的参数化类型不匹配的值可以赋给变量objectArray
,从而可以赋给l
。然而,在这个语句中,编译器并不生成未经检查的警告。编译器在将可变参数形式参数List... l
翻译为形式参数List[] l
时已经生成了警告。这个语句是有效的;变量l
的类型是List[]
,它是Object[]
的子类型。
因此,如果您将任何类型的List
对象分配给objectArray
数组的任何数组组件,编译器不会发出警告或错误,如下所示:
objectArray[0] = Arrays.asList(42);
这个语句将包含一个类型为Integer
的对象的List
对象分配给objectArray
数组的第一个数组组件。
假设您使用以下语句调用ArrayBuilder.faultyMethod
:
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
在运行时,JVM 在以下语句处抛出ClassCastException
:
// ClassCastException thrown here String s = l[0].get(0);
存储在变量l
的第一个数组组件中的对象的类型是List
,但这个语句期望的是类型为List
的对象。
防止具有非可重复形式参数的可变参数方法产生警告
如果您声明一个具有参数化类型参数的可变参数方法,并确保方法体不会因为对可变参数形式参数的不当处理而抛出ClassCastException
或其他类似异常,您可以通过在静态和非构造方法声明中添加以下注解来防止编译器为这些类型的可变参数方法生成警告:
@SafeVarargs
@SafeVarargs
注解是方法契约的一部分;此注解断言方法的实现不会不当处理可变参数形式参数。
也可以通过在方法声明中添加以下内容来抑制此类警告,尽管这种做法不太理想:
@SuppressWarnings({"unchecked", "varargs"})
然而,这种方法并不会抑制从方法调用点生成的警告。如果你对@SuppressWarnings
语法不熟悉,请参见 Annotations。
泛型的限制。
原文:
docs.oracle.com/javase/tutorial/java/generics/restrictions.html
。
要有效地使用 Java 泛型,您必须考虑以下限制:
- 不能用原始类型实例化泛型类型。
- 不能创建类型参数的实例。
- 不能声明其类型为类型参数的静态字段。
- 不能在参数化类型中使用强制类型转换或
instanceof
。 - 不能创建参数化类型的数组。
- 不能创建、捕获或抛出参数化类型的对象。
- 不能重载形式参数类型擦除为相同原始类型的方法。
不能用原始类型实例化泛型类型。
考虑以下参数化类型:
class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } // ... }
在创建Pair
对象时,您不能用原始类型替换类型参数K
或V
:
Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error
您只能用非原始类型替换类型参数K
和V
:
Pair<Integer, Character> p = new Pair<>(8, 'a');
请注意,Java 编译器会将8
自动装箱为Integer.valueOf(8)
,将'a'
自动装箱为Character('a')
:
Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));
有关自动装箱的更多信息,请参见自动装箱和拆箱中的数字和字符串课程。
不能创建类型参数的实例。
你不能创建一个类型参数的实例。例如,以下代码会导致编译时错误:
public static <E> void append(List<E> list) { E elem = new E(); // compile-time error list.add(elem); }
作为一种解决方法,您可以通过反射创建一个类型参数的对象:
public static <E> void append(List<E> list, Class<E> cls) throws Exception { E elem = cls.newInstance(); // OK list.add(elem); }
您可以按以下方式调用append
方法:
List<String> ls = new ArrayList<>(); append(ls, String.class);
不能声明其类型为类型参数的静态字段。
类的静态字段是所有非静态对象共享的类级变量。因此,不允许类型参数的静态字段。考虑以下类:
public class MobileDevice<T> { private static T os; // ... }
如果允许类型参数的静态字段,则以下代码将会混淆:
MobileDevice<Smartphone> phone = new MobileDevice<>(); MobileDevice<Pager> pager = new MobileDevice<>(); MobileDevice<TabletPC> pc = new MobileDevice<>();
因为静态字段os
被phone
、pager
和pc
共享,os
的实际类型是什么?它不能同时是Smartphone
、Pager
和TabletPC
。因此,您不能创建类型参数的静态字段。
不能在参数化类型中使用强制类型转换或instanceof
。
因为 Java 编译器会擦除泛型代码中的所有类型参数,所以无法在运行时验证泛型类型的参数化类型:
public static <E> void rtti(List<E> list) { if (list instanceof ArrayList<Integer>) { // compile-time error // ... } }
传递给rtti
方法的参数化类型集合为:
S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }
运行时不会跟踪类型参数,因此无法区分ArrayList
和ArrayList
之间的区别。您最多可以使用无界通配符来验证列表是否是ArrayList
:
public static void rtti(List<?> list) { if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type // ... } }
通常情况下,除非使用无界通配符进行参数化,否则不能进行参数化类型的强制转换。例如:
List<Integer> li = new ArrayList<>(); List<Number> ln = (List<Number>) li; // compile-time error
但是,在某些情况下,编译器知道类型参数始终有效并允许强制转换。例如:
List<String> l1 = ...; ArrayList<String> l2 = (ArrayList<String>)l1; // OK
不能创建参数化类型的数组。
你不能创建参数化类型的数组。例如,以下代码无法编译:
List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time error
以下代码说明了当不同类型插入数组时会发生什么:
Object[] strings = new String[2]; strings[0] = "hi"; // OK strings[1] = 100; // An ArrayStoreException is thrown.
如果你尝试对一个泛型列表做同样的事情,会出现问题:
Object[] stringLists = new List<String>[2]; // compiler error, but pretend it's allowed stringLists[0] = new ArrayList<String>(); // OK stringLists[1] = new ArrayList<Integer>(); // An ArrayStoreException should be thrown,
如果允许参数化列表的数组,上述代码将无法抛出期望的ArrayStoreException
。
无法创建、捕获或抛出参数化类型的对象
一个泛型类不能直接或间接地扩展Throwable
类。例如,以下类将无法编译:
// Extends Throwable indirectly class MathException<T> extends Exception { /* ... */ } // compile-time error // Extends Throwable directly class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
一个方法无法捕获类型参数的实例:
public static <T extends Exception, J> void execute(List<J> jobs) { try { for (J job : jobs) // ... } catch (T e) { // compile-time error // ... } }
但是,你可以在throws
子句中使用类型参数:
class Parser<T extends Exception> { public void parse(File file) throws T { // OK // ... } }
无法重载形式参数类型擦除为相同原始类型的方法
一个类不能有两个在类型擦除后具有相同签名的重载方法。
public class Example { public void print(Set<String> strSet) { } public void print(Set<Integer> intSet) { } }
这些重载将共享相同的类文件表示,并将生成编译时错误。