Java泛型:变量声明类型与实例类型能否不同?
嘿,这个问题问得相当精准!先直接给你结论:你举的例子List<Object> list = new ArrayList<Integer>();在Java里是编译不通过的,但确实存在泛型变量声明类型和实例化类型不同的合法场景,我给你一步步讲清楚:
为什么你的例子行不通?
Java的泛型是**不可变(invariant)**的——哪怕Integer是Object的子类,List<Integer>也不是List<Object>的子类。为啥要这么设计?举个极端点的例子:如果允许这种赋值,你后续可能会写list.add("我是字符串"),而实际底层是ArrayList<Integer>,运行时就会抛出ClassCastException,编译器直接把这种风险掐死在摇篮里。
合法的“声明与实例化类型不同”场景
下面这些场景都是编译器认可的,本质是利用泛型通配符实现协变/逆变:
1. 上界通配符 ? extends(协变)
当你需要一个能读取“某个父类及其子类”元素的集合时,可以这么写:
List<? extends Number> numList = new ArrayList<Integer>(); List<? extends Number> numList2 = new ArrayList<Double>();
这种情况下,你可以安全地从numList里读取元素(类型是Number),但不能往里面添加任何元素(除了null)——因为编译器无法确定集合实际存储的是Integer还是Double,随便加会破坏类型安全。
2. 下界通配符 ? super(逆变)
当你需要一个能写入“某个子类及其父类”元素的集合时,用下界通配符:
List<? super Integer> intList = new ArrayList<Object>(); List<? super Integer> intList2 = new ArrayList<Number>();
这里你可以往intList里添加Integer或者它的子类(比如Integer的包装类本身,或者自定义的子类),但读取元素时只能拿到Object类型——因为集合实际可能存储的是更宽泛的父类类型。
3. 无界通配符 ?
如果你的操作不依赖集合的具体元素类型(比如只是清空集合、获取大小),可以用无界通配符:
List<?> anyList = new ArrayList<String>(); anyList.clear(); // 合法,不依赖元素类型 int size = anyList.size(); // 合法
这种场景下,读取元素只能转成Object,也不能添加任何元素(除了null)。
4. 不推荐的原始类型兼容(合法但有风险)
Java为了兼容旧代码,允许泛型集合赋值给原始类型变量,反过来也可以:
List rawList = new ArrayList<String>(); // 会触发unchecked警告 List<String> strList = rawList; // 同样会有警告
这种写法绕过了泛型的类型检查,很容易在运行时抛出类型转换异常,实际开发中强烈不推荐使用。
总结
泛型变量声明和实例化类型不同的合法场景,核心是通过通配符实现的协变/逆变,而直接用两个具体的泛型类型(比如List<Object>和ArrayList<Integer>)赋值是不允许的,这是Java泛型类型安全的保障。
内容的提问来源于stack exchange,提问作者Paco Kwon

