Java泛型使用常见报错
一、什么时候使用泛型会报错?
报错几乎都发生在编译时期,这正是泛型保护我们的地方。它们源于对泛型规则(特别是继承规则)的误解。
错误1:误认为泛型类型存在继承关系(最常见!)
核心规则( invariance - 不可变性):
Box<String>
和 Box<Object>
没有任何继承关系,即使 String
是 Object
的子类。
List<String> strList = new ArrayList<>();
List<Object> objList = strList; // 编译错误! incompatible types(不兼容的类型)
为什么会报错?
如果编译器允许这样做,就会破坏类型安全。因为 objList
的类型是 List<Object>
,意味着你可以向里面添加任何 Object
的子类,比如 Integer
。
// 假设上面的代码不报错,会发生什么:
objList.add(new Integer(100)); // 这看起来是合法的,因为Integer是Object的子类
String firstElement = strList.get(0); // !!!运行时错误:ClassCastException
// 你试图从一个声称只装String的列表里,取出一个Integer并当成String
编译器通过报错阻止了这种可能引发运行时灾难的代码。这就是泛型提供的编译期类型安全。
错误2:试图创建泛型数组
T[] array = new T[10]; // 编译错误! generic array creation(无法创建泛型数组)
List<String>[] listArray = new List<String>[10]; // 编译错误!
为什么会报错?
因为类型擦除。在运行时,T
和 String
都被擦除了,变成了 Object
。JVM 无法在创建数组时确认 array
和 listArray
的具体类型(数组需要知道其确切的组件类型以强制保证类型安全)。如果允许创建,可能会导致上述的类型安全问题。
绕过方法(但不安全):
你可以创建一个 Object[]
然后强制转型,但这会收到编译器警告,且需要自己保证类型安全。
List<String>[] listArray = (List<String>[]) new List<?>[10]; // 有警告,但不报错
错误3:误用通配符集合的写入操作
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(new Integer(100)); // 编译错误!
numbers.add(new Float(3.14f)); // 编译错误!
numbers.add(null); // 这是唯一可以的,因为null没有类型
为什么会报错?
List<? extends Number>
的意思是“一个只读的列表,其元素是某种未知的、继承自 Number
的类型”。它可能是 ArrayList<Integer>
,也可能是 ArrayList<Double>
。编译器无法确定到底是哪一种,所以为了绝对的类型安全,它禁止你加入任何元素(除了 null
),因为你可能会把 Double
加到 ArrayList<Integer>
里。
错误4:误用通配符集合的读取操作
List<? super Integer> list = new ArrayList<Number>();
Integer num = list.get(0); // 编译错误!
Number num2 = list.get(0); // 编译错误!
Object obj = list.get(0); // 这是唯一可以的
为什么会报错?
List<? super Integer>
的意思是“一个只写的列表,其元素是某种未知的、Integer
的父类型”。你从里面取出的元素,编译器只能确定它是 Integer
的某个父类,但无法确定具体是 Number
还是 Object
。因此,为了保证安全,它只允许你赋值给最顶层的 Object
引用。
二、泛型“继承”关系的正确认知
如何理解这张图:
不变性 (Invariance - 红色部分):这是最根本的规则。
List<String>
和List<Object>
在类型系统上是完全不同的类型,没有继承关系。你不能将它们互相赋值。试图这么做是绝大多数编译错误的根源。协变 (Covariance - 通过
? extends
实现,绿色部分):虽然List<Integer>
不是List<Number>
的子类,但你可以通过上界通配符List<? extends Number>
来获得一个“只读”的、安全的“继承”视图。一个ArrayList<Integer>
可以被当作一个List<? extends Number>
来使用。这模拟了协变(子类容器可以被视为父类容器)。逆变 (Contravariance - 通过
? super
实现,绿色部分):同样,你可以通过下界通配符List<? super Integer>
来获得一个“只写”的、安全的“继承”视图。一个ArrayList<Number>
甚至ArrayList<Object>
都可以被当作一个List<? super Integer>
来使用。这模拟了逆变(父类容器可以被视为子类容器,但用途相反)。
记住:
<T>
: 用于当你既需要“读”也需要“写”操作时。这是最常用、最直接的方式。<? extends T>
: 只读。当你只需要从集合中获取元素时使用(Producer)。<? super T>
: 只写。当你只需要向集合中添加元素时使用(Consumer)。
三、常见问题总结
Q:“谈谈Java泛型中使用不当会导致的问题和泛型的继承关系。”
A:
“使用泛型最容易出错的地方是对泛型容器继承关系的误解。核心规则是泛型具有不变性:Container<SubClass>
和 Container<SuperClass>
没有继承关系,即使 SubClass
继承自 SuperClass
。试图将它们相互赋值是主要的编译错误来源。
之所以这样设计,是为了保证绝对的编译期类型安全。如果允许这种赋值,就可以把 SuperClass
对象放入一个声明为只装 SubClass
的容器中,从而在后续读取时引发运行时 ClassCastException
。
为了在需要时实现类似继承的灵活性,Java引入了通配符:
<? extends T>
实现了协变,让我们能安全地读取元素,将容器视为元素的生产者。<? super T>
实现了逆变,让我们能安全地写入元素,将容器视为元素的消费者。
其他常见错误还包括试图创建泛型数组或实例化类型参数,这些都源于泛型在运行时类型信息被擦除的实现机制。
PECS(Producer-Extends, Consumer-Super) 原则是指导我们正确使用通配符、避免编译错误的最佳实践。理解了不变性是基础,协变和逆变是工具,就能绝大多数泛型相关的编译错误。