为何Java允许泛型集合赋值非泛型引用,却禁止List<Object>指向ArrayList<String>?
这问题问得特别到位,正好说到了Java泛型设计里最核心的兼容性权衡,我给你一步步拆解明白:
为什么Java允许泛型Collection赋值给非泛型引用?
Java的泛型是JDK 5才引入的特性,而在此之前,所有集合都是不带类型参数的“原始类型”(raw type)。为了让新的泛型代码能和大量遗留的非泛型老代码无缝协作,Java特意设计了泛型与原始类型的兼容规则:允许泛型类型向上转型为对应的原始类型。
举个例子,Collection<String> strCol = new ArrayList<>();可以赋值给Collection rawCol = strCol;——这不是漏洞,而是刻意的兼容性设计。如果不允许这种操作,那所有老系统里使用原始类型集合的代码,都无法直接使用新的泛型集合实例,迁移成本会高到离谱。当然,这么做的代价是你会失去泛型带来的编译期类型安全检查,编译器会给你一个unchecked conversion的警告,提醒你后续操作可能有风险。
为什么List<Object> list = new ArrayList<String>()不行,但List l = new ArrayList<String>()可以?
这里的关键是要区分泛型的不变性和原始类型的特殊性:
先看
List<Object> list = new ArrayList<String>()被禁止的原因:
Java的泛型是不变的(invariant),也就是说List<String>并不是List<Object>的子类——哪怕String是Object的子类也不行。如果允许这种赋值,你后续就可以通过list.add(new Integer(123))往这个原本只能存String的集合里加Integer,而编译器不会拦你(因为它认为list是List<Object>)。等到运行时你把元素取出来当String用的时候,就会抛出ClassCastException,这会破坏泛型要保证的类型安全。所以编译器在编译期就直接禁止了这种不安全的赋值,把问题扼杀在摇篮里。再看
List l = new ArrayList<String>()被允许的原因:List是原始类型,它相当于“关闭了泛型的类型检查开关”,回到了JDK 5之前的状态。Java允许这种赋值,同样是为了兼容老代码——老代码里全是这种不带泛型的集合声明,如果禁止的话,老代码根本没法用新的泛型集合实例。当然,这么做的代价也是明显的:你可以往
l里加任何类型的对象(比如l.add(123)),编译器不会报错,但等到你把元素取出来强转为String时,运行时就会炸。编译器会给你一个警告,但不会阻止你——因为这是你主动选择放弃类型安全来兼容老代码的结果。
总结一下:这两种情况的差异,本质上是Java在类型安全和版本兼容性之间做出的权衡——泛型之间的严格约束是为了保证类型安全,而原始类型的宽松规则是为了让新旧代码能共存。
内容的提问来源于stack exchange,提问作者Vikram Singh

