Java泛型类型擦除 以及 数据污染中堆污染和类型转换异常讲解

Java泛型类型擦除 以及 数据污染中堆污染和类型转换异常讲解

类型擦除

介绍

Java 泛型中的类型擦除是指编译器在编译期间会将泛型类型擦除,将所有泛型类型转化为它们的边界类型(泛型的上限类型,默认Object但是打印出来为E)。
类型擦除的过程中,编译器会对泛型类型进行检查和转换,以确保类型的安全性和正确性。

原理

Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。

原则:

  1. 消除类型参数声明,即删除<>及其包围的部分。
  2. 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
  3. 为了保证类型安全,必要时插入强制类型转换代码。
  4. 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
案例
public class TypeErasureDemo {
    public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2);

        List<String> list2 = new ArrayList<>();
        list2.add("a");
        list2.add("b");

        // 使用反射获取list1和list2的类型参数
        Class<?> clazz1 = list1.getClass();
        Class<?> clazz2 = list2.getClass();
        System.out.println(clazz1 == clazz2); // true

        // 使用反射获取list1和list2的类型参数
        Class<?>[] typeArgs1 = clazz1.getTypeParameters();
        Class<?>[] typeArgs2 = clazz2.getTypeParameters();
        System.out.println(typeArgs1[0]); // class java.lang.Integer
        System.out.println(typeArgs2[0]); // class java.lang.String
    }
}

在上面的案例中,我们创建了两个泛型集合 list1list2,一个存储整数,一个存储字符串。

使用 getClass() 方法获取这两个列表的实际类型,并比较它们是否相等。会发现它们的类型都是 java.util.ArrayList,这是因为类型擦除将所有泛型类型转换为它们的边界类型。

最后使用反射获取了 list1list2 的类型参数并打印出它们的实际类型。

设上边界的情况

如果在泛型类型参数上设置了上边界,类型擦除将使用边界类型作为替换类型。例如,对于List<T extends Number>,因为类型擦除会将T替换为Number。这意味着在使用List<T>时,所有的T实例方法和实例变量都会被视为Number。因此,类型擦除将保持边界信息,以保证类型安全性。

类型擦除的缺点主要包括以下几点:

  1. 丢失类型信息:类型擦除会导致泛型类型的实际类型信息在运行时丢失,这使得编译器无法确保程序的类型安全性。例如,在运行时,无法知道泛型类型中具体的类型参数是什么。
  2. 不能使用基本类型:类型擦除的机制只适用于对象类型,因此不能使用基本类型作为泛型参数。这意味着,例如 List<int> 这样的类型是无效的。
  3. 不能创建泛型数组:类型擦除使得在运行时无法创建泛型数组。这是因为 Java 的数组必须是协变的,也就是说,必须能够在编译时确定数组的元素类型。但由于类型擦除,编译器在编译时无法确定泛型类型的实际类型参数,因此无法创建泛型数组。
  4. 限制了泛型类型参数的行为:类型擦除限制了泛型类型参数的行为。例如,不能使用泛型类型参数来创建新的对象或调用其构造函数。这使得泛型类型参数的能力受到了一定的限制。

总结

总的来说,类型擦除虽然使得 Java 的泛型具有了更好的兼容性和互操作性,但也在一定程度上降低了程序的类型安全性和灵活性。

数据污染

数据污染指的是在使用Java泛型时,将不符合泛型类型要求的数据存入了泛型容器中,导致在运行时出现不可预测的错误。

特征 堆污染 类型转换异常
发生时机 运行时 运行时
主要原因 存储了错误的泛型类型 错误的类型转换
异常类型 ClassCastException ClassCastException
堆污染的影响 可能导致程序崩溃或出现未知错误 可能导致程序崩溃或出现未知错误
解决方法 使用泛型边界和类型转换 使用泛型边界和类型转换

堆污染

堆污染是数据污染的一种,指的是在使用Java泛型时,将不符合泛型类型要求的数据存入了泛型数组中,导致在运行时出现 ClassCastException 异常。

案例
import java.util.ArrayList;
import java.util.List;

public class HeapPollutionExample {

    public static void main(String[] args) {
        // 创建一个泛型列表
        List<String> stringListA = new ArrayList<>();
        stringListA.add("A");

        // 将泛型列表传递给一个原始类型列表
        List rawList = stringListA;  // 产生堆污染

        // 创建另一个泛型列表
        List<Integer> intListB = new ArrayList<>();
        intListB.add(1);

        // 将另一个泛型列表添加到原始类型列表中
        rawList.add(intListB);  // 产生堆污染

        // 从原始类型列表中获取元素
        String elementA = stringListA.get(1);  // 抛出ClassCastException异常
    }
}

类型转换异常

类型转换异常也是数据污染的一种,指的是在将一个泛型类型转换为另一个泛型类型时,由于类型不匹配而出现 ClassCastException 异常。

案例
import java.util.ArrayList;
import java.util.List;

public class TypeConversionErrorExample {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("hello");
        stringList.add("world");

        List integerList = (List) stringList;
        integerList.add(10); // 将整数添加到原本是String类型的列表中

        String s = stringList.get(2); // 从原本是String类型的列表中获取一个整数
    }
}

总结

因此,可以说数据污染是堆污染和类型转换异常的根源,而堆污染和类型转换异常是数据污染的两种具体表现。

推荐文章

Java泛型机制详解