# 问题

6. List<? extends Number>List<? super Integer> 的区别?

# 标准答案

List<? extends Number> 表示一个包含 Number 或其子类型的列表,这意味着列表的元素类型是某种 Number 类型或其派生类。它适用于只读取元素的场景,因为我们无法确定其具体类型,也不能向其中添加元素(除了 null)。而 List<? super Integer> 表示一个包含 Integer 或其父类型的列表,适用于写入元素的场景,因为可以向该列表添加 Integer 类型或其子类的元素(如 IntegerNumber)。总结来说,? extends 用于限定读取的类型范围,而 ? super 用于限定写入的类型范围。

# 答案解析

在Java泛型中,? extends T? super T 是通配符的两种常见用法,它们分别表示对泛型类型的上界下界进行约束。它们分别具有不同的使用场景和语义,关键区别在于对集合类型操作的灵活性和类型安全性。

# 核心原理:

  1. 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
  2. List<? super Integer>

    • 下界通配符(Lower Bounded Wildcard),表示该列表可以包含 Integer 或其父类的任何类型(如 NumberObject)。
    • 该集合适用于写入操作,因为我们知道可以向其中添加 Integer 类型或其子类(如果有的话)元素。对于读取操作,返回的元素类型是 Object,因为我们无法确定具体类型,只能从 Object 类型读取。
    • 示例:
      List<? super Integer> list = new ArrayList<Number>();
      list.add(10);  // 可以添加 Integer 类型的元素
      // Integer value = list.get(0); // 编译错误,无法知道返回值类型
      
      1
      2
      3
  3. 区别总结

    • ? extends T 适用于 只读 操作,适合用来读取元素,因为它允许你从集合中获取元素,并将其视为 TT 的某个子类。
    • ? super T 适用于 只写 操作,适合用来添加元素,因为它允许你向集合中添加类型 T 或其子类的元素。

# 常见错误:

  1. 错误使用List<? extends Number>进行写入操作:由于无法确定具体类型,编译器禁止向 List<? extends Number> 中添加元素。误认为可以添加某些类型的元素会导致编译错误。

    • 错误示例
      List<? extends Number> list = new ArrayList<Integer>();
      list.add(1); // 编译错误,无法确定类型
      
      1
      2
  2. 错误使用List<? super Integer>进行读取操作:由于返回的类型是 Object,我们无法直接将其赋给 Integer 类型,可能会导致类型转换问题。

    • 错误示例
      List<? super Integer> list = new ArrayList<Number>();
      Integer num = list.get(0);  // 编译错误,返回类型是 Object,不能直接赋值给 Integer
      
      1
      2

# 最佳实践:

  1. 读取数据时使用? extends T:如果你只需要读取数据并且能够接受所有子类的元素(例如,只关心 Number 类型或其任何子类),使用 ? extends T 是最合适的选择。它能确保你读取的数据类型是安全的。

    • 示例
      List<? extends Number> list = new ArrayList<Integer>();
      for (Number num : list) {
          System.out.println(num);  // 安全地读取
      }
      
      1
      2
      3
      4
  2. 写入数据时使用? super T:如果你希望向集合中添加元素,并且你知道自己会使用 T 类型的元素,可以使用 ? super T。它允许你向列表中添加 T 类型及其子类的元素。

    • 示例
      List<? super Integer> list = new ArrayList<Number>();
      list.add(10);  // 安全地写入 Integer 类型
      
      1
      2
  3. 综合使用

    • 在某些场合下,你可能会遇到需要同时支持读取和写入的场景。此时,你可能需要对不同的集合类型做适当的封装,并根据需求选择合适的通配符。
    • 示例
      // 只读操作
      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>),如何确保类型安全和正确的元素操作?

# 相关面试题

  • 泛型的协变与逆变:在泛型中,如何理解协变与逆变?它们与通配符有何关系?
  • 泛型与多态:如何在多态环境中使用泛型,确保类型安全并提高代码的可维护性?
  • 类型擦除的影响:类型擦除如何影响泛型的使用,特别是在集合类中的通配符使用上?