# 问题
6. List<? extends Number>
和 List<? super Integer>
的区别?
# 标准答案
List<? extends Number>
表示一个包含 Number
或其子类型的列表,这意味着列表的元素类型是某种 Number
类型或其派生类。它适用于只读取元素的场景,因为我们无法确定其具体类型,也不能向其中添加元素(除了 null
)。而 List<? super Integer>
表示一个包含 Integer
或其父类型的列表,适用于写入元素的场景,因为可以向该列表添加 Integer
类型或其子类的元素(如 Integer
和 Number
)。总结来说,? extends
用于限定读取的类型范围,而 ? super
用于限定写入的类型范围。
# 答案解析
在Java泛型中,? extends T
和 ? super T
是通配符的两种常见用法,它们分别表示对泛型类型的上界和下界进行约束。它们分别具有不同的使用场景和语义,关键区别在于对集合类型操作的灵活性和类型安全性。
# 核心原理:
List<? extends Number>
:- 上界通配符(Upper Bounded Wildcard),表示该列表可以包含
Number
类或其子类的任何类型,但你无法向其中添加具体类型的元素(除了null
)。 - 该集合只适合读取操作,因为它的元素类型是不确定的。在这种情况下,编译器无法确定元素的具体类型,所以只能安全地读取
Number
类型的数据,但无法进行写入操作。 - 示例:
List<? extends Number> list = new ArrayList<Integer>(); Number num = list.get(0); // 读取时类型安全 // list.add(10); // 编译错误,无法添加,因为无法确定具体类型
1
2
3
- 上界通配符(Upper Bounded Wildcard),表示该列表可以包含
List<? super Integer>
:- 下界通配符(Lower Bounded Wildcard),表示该列表可以包含
Integer
或其父类的任何类型(如Number
或Object
)。 - 该集合适用于写入操作,因为我们知道可以向其中添加
Integer
类型或其子类(如果有的话)元素。对于读取操作,返回的元素类型是Object
,因为我们无法确定具体类型,只能从Object
类型读取。 - 示例:
List<? super Integer> list = new ArrayList<Number>(); list.add(10); // 可以添加 Integer 类型的元素 // Integer value = list.get(0); // 编译错误,无法知道返回值类型
1
2
3
- 下界通配符(Lower Bounded Wildcard),表示该列表可以包含
区别总结:
? extends T
适用于 只读 操作,适合用来读取元素,因为它允许你从集合中获取元素,并将其视为T
或T
的某个子类。? super T
适用于 只写 操作,适合用来添加元素,因为它允许你向集合中添加类型T
或其子类的元素。
# 常见错误:
错误使用
List<? extends Number>
进行写入操作:由于无法确定具体类型,编译器禁止向List<? extends Number>
中添加元素。误认为可以添加某些类型的元素会导致编译错误。- 错误示例:
List<? extends Number> list = new ArrayList<Integer>(); list.add(1); // 编译错误,无法确定类型
1
2
- 错误示例:
错误使用
List<? super Integer>
进行读取操作:由于返回的类型是Object
,我们无法直接将其赋给Integer
类型,可能会导致类型转换问题。- 错误示例:
List<? super Integer> list = new ArrayList<Number>(); Integer num = list.get(0); // 编译错误,返回类型是 Object,不能直接赋值给 Integer
1
2
- 错误示例:
# 最佳实践:
读取数据时使用
? extends T
:如果你只需要读取数据并且能够接受所有子类的元素(例如,只关心Number
类型或其任何子类),使用? extends T
是最合适的选择。它能确保你读取的数据类型是安全的。- 示例:
List<? extends Number> list = new ArrayList<Integer>(); for (Number num : list) { System.out.println(num); // 安全地读取 }
1
2
3
4
- 示例:
写入数据时使用
? super T
:如果你希望向集合中添加元素,并且你知道自己会使用T
类型的元素,可以使用? super T
。它允许你向列表中添加T
类型及其子类的元素。- 示例:
List<? super Integer> list = new ArrayList<Number>(); list.add(10); // 安全地写入 Integer 类型
1
2
- 示例:
综合使用:
- 在某些场合下,你可能会遇到需要同时支持读取和写入的场景。此时,你可能需要对不同的集合类型做适当的封装,并根据需求选择合适的通配符。
- 示例:
// 只读操作 public void printNumbers(List<? extends Number> list) { for (Number num : list) { System.out.println(num); } } // 只写操作 public void addInteger(List<? super Integer> list) { list.add(10); }
1
2
3
4
5
6
7
8
9
10
11
# 性能优化:
- 避免过度使用通配符:虽然通配符提供了灵活性,但过度使用会使得代码变得难以理解和维护。在大多数情况下,明确指定类型参数是更好的选择。
- 类型安全与性能权衡:对于泛型集合,最好在编译时确保类型安全,而不是在运行时进行类型转换或强制类型检查,这样能减少运行时的错误和性能开销。
# 深入追问
🔹 ? extends T
与? super T
的通用策略:
- 如何根据实际业务场景,选择适合的通配符(
? extends T
或? super T
)来平衡类型安全性与灵活性?
🔹 多层次继承中的通配符使用:
- 如果我们处理一个多层次继承的泛型(比如
List<? extends T1>
和List<? super T2>
),如何确保类型安全和正确的元素操作?
# 相关面试题
- 泛型的协变与逆变:在泛型中,如何理解协变与逆变?它们与通配符有何关系?
- 泛型与多态:如何在多态环境中使用泛型,确保类型安全并提高代码的可维护性?
- 类型擦除的影响:类型擦除如何影响泛型的使用,特别是在集合类中的通配符使用上?