一、什么时候使用泛型会报错?
报错几乎都发生在编译时期,这正是泛型保护我们的地方。它们源于对泛型规则(特别是继承规则)的误解。
错误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) 原则是指导我们正确使用通配符、避免编译错误的最佳实践。理解了不变性是基础,协变和逆变是工具,就能绝大多数泛型相关的编译错误。
到此这篇关于java泛型使用一些常见报错总结的文章就介绍到这了,更多相关java泛型使用报错内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论