泛型

1. 为什么引入泛型? (Motivation)

  • 参数化类型: 泛型的核心目的是实现参数化类型。这意味着可以在不创建新类型的情况下,通过参数指定操作的数据类型。
  • 解决类型限制: 在泛型使用过程中,操作的数据类型被指定为一个参数,这个类型参数可以用于类、接口和方法,分别构成泛型类、泛型接口和泛型方法。

2. 引入泛型的意义 (Benefits)

  • 代码复用:
    • 适用多种数据类型执行相同代码: 泛型允许编写可以应用于多种数据类型的通用代码,避免为每种数据类型重载方法或类。
    • 示例: 加法操作 ( add 方法) 可以通过泛型 T extends Number 实现对 int, float, double 等数值类型的复用,而无需为每种类型编写单独的方法。
  • 类型安全:
    • 编译时类型检查: 泛型在编译时提供类型约束,确保类型安全,避免在运行时出现 ClassCastException 等类型转换异常。
    • 无需强制类型转换: 使用泛型集合 (如 List<String>) 时,编译器会检查类型,从集合中取出元素时无需手动强制类型转换,直接获得目标类型,更加方便安全。
    • 示例: List 在没有泛型的情况下可以添加任何 Object 类型元素,取出时需要强制转换,容易出错。而 List<String> 在编译时就限定了只能添加 String 类型,类型更安全。

3. 泛型的基本使用 (Basic Usage)

泛型有三种主要的使用方式:

  • 泛型类 (Generic Classes)

    • 定义: 在类名后使用 <T> (或其他标识符) 声明类型参数。
    • 类型参数作用域: 类中可以使用类型参数 T 定义属性和方法的参数/返回值类型。
    • 实例化: 创建泛型类对象时,需要指定具体的类型,例如 Point<String>Notepad<String, Integer>
    • 示例:
      • Point<T> 类:var 属性的类型由外部指定。
      • Notepad<K, V> 类:keyvalue 属性的类型由外部指定,展示了多元泛型。
    • 泛型接口 (Generic Interfaces)

    • 定义: 在接口名后使用 <T> 声明类型参数。

    • 类型参数作用域: 接口中的方法可以使用类型参数 T 定义返回值类型。
    • 实现: 实现泛型接口的类,可以选择继续使用泛型,或者指定具体的类型。
    • 示例:
      • Info<T> 接口:定义了返回泛型类型 TgetVar() 方法。
      • InfoImpl<T> 类:实现了 Info<T> 接口,并继续使用泛型。
    • 泛型方法 (Generic Methods)

    • 定义: 在方法返回值类型之前使用 <T> 声明类型参数。

    • 类型参数作用域: 方法的参数、返回值和方法体内部可以使用类型参数 T
    • 调用: 调用泛型方法时,通常不需要显式指定类型,编译器会自动进行类型推断。 如果需要显式指定,可以使用 方法名.<类型参数>(参数) 的形式。
    • Class<T> 参数: 泛型方法中可以使用 Class<T> 类型的参数,用于在运行时获取类型信息,并结合反射创建泛型类的对象。
    • 灵活性: 泛型方法比泛型类更灵活,因为可以在调用方法时才指定类型,而泛型类在实例化时就需要指定类型。
    • 示例: 定义一个泛型方法,参数为 Class<T>,可以创建不同类型的对象。

4. 泛型的上下限 (Generics Bounds)

  • 解决类型转换问题: 泛型中,List<B> 不是 List<A> 的子类型,即使 BA 的子类。为了解决这种隐含的类型转换问题,引入了泛型的上下限。
  • 上限 (<? extends E>)
    • 含义: 表示类型参数可以是 E 类型本身,或者是 E 的任何子类。
    • 作用: 限制类型参数的上界,确保类型是 E 或其子类,从而可以安全地进行类型转换。
    • 示例: List<? extends A> 可以接受 List<A>List<B> (如果 B 继承自 A)。
  • 下限 (<? super E>)
    • 含义: 表示类型参数可以是 E 类型本身,或者是 E 的任何父类。
    • 作用: 限制类型参数的下界,确保类型是 E 或其父类。
    • 示例: Info<? super String> 可以接受 Info<String>Info<Object> (因为 ObjectString 的父类)。
  • 无限制通配符 (<?>)
    • 含义: 表示类型参数可以是任何类型。
    • 作用: 当不需要关心具体的类型,或者只需要进行一些不依赖于具体类型参数的操作时使用。
  • 使用原则 (PECS - Producer Extends, Consumer Super)
    • 生产者 (Producer): 如果参数化类型是用来生产 (读取) T 类型的数据,使用 <? extends T> (上限)。
    • 消费者 (Consumer): 如果参数化类型是用来消费 (写入) T 类型的数据,使用 <? super T> (下限)。
    • 既是生产者又是消费者: 如果既需要生产又需要消费,则不使用通配符,使用精确的参数类型。
  • 多重限制 (<T extends Type1 & Type2>)
    • 使用 & 符号: 可以使用 & 符号同时指定多个类型参数的上限,类型参数必须同时满足所有上限的约束。
    • 示例: <T extends Staff & Passenger> 表示类型 T 必须同时是 StaffPassenger 的子类型。

5. 泛型数组 (Generic Arrays)

  • 限制: Java 中不能直接创建泛型数组,例如 new ArrayList<String>[10] 是非法的。
  • 原因: 类型擦除和数组的协变性导致泛型数组的创建存在类型安全问题。
  • 常用规避方法:
    • 讨巧的使用场景 (变长参数): 使用泛型方法接收可变参数 T... arg,可以返回泛型数组 T[],但这实际上是利用了编译器的一些特性,本质上仍然不是直接创建泛型数组。
    • 合理的创建方式 (反射 Array.newInstance): 通过反射 Array.newInstance(type, size) 可以创建指定类型的数组,并进行强制类型转换 (T[]),但这会产生警告,需要开发者自行保证类型安全。

6.伪泛型与类型擦除

  • Java 泛型的本质是“伪泛型”:为了兼容旧版本,Java 泛型并非真正的泛型,而是一种编译时的概念。
  • 类型擦除:在编译阶段,Java 会将代码中的泛型类型信息擦除,转换为其原始类型,就像没有泛型一样。运行时,JVM 中不存在泛型类型信息。

7.类型擦除原则:

  1. 删除泛型声明:移除 <> 及其内部的类型参数。
  2. 替换类型参数为原始类型
    • 无限定类型参数(<T><?>)替换为 Object
    • 有限定上界的类型参数(<T extends Number><? extends Number>)替换为最左边限定类型(即上界,如 Number)。
    • 有限定下界的类型参数(<? super Number>)替换为上界(对于 super 来说,上界是 Object)。
  3. 插入类型转换代码:为了类型安全,在必要时插入强制类型转换代码。
  4. 生成桥接方法:为了保持泛型擦除后的多态性,自动生成“桥接方法”。

8.如何证明类型擦除?

  • 原始类型相等

    • ArrayList<String>ArrayList<Integer> 在运行时 getClass() 返回结果相同,表明泛型类型信息已被擦除,只剩下原始类型 ArrayList

    Java

    ArrayList<String> list1 = new ArrayList<String>(); ArrayList<Integer> list2 = new ArrayList<Integer>(); System.out.println(list1.getClass() == list2.getClass()); // true

  • 反射添加其他类型元素

    • 通过反射可以绕过编译期的泛型检查,向 ArrayList<Integer> 实例中添加字符串,证明泛型类型检查只存在于编译期,运行时类型信息已被擦除。

    Java

    ArrayList<Integer> list = new ArrayList<Integer>(); list.getClass().getMethod("add", Object.class).invoke(list, "asd"); // 运行时可添加 String

9.原始类型详解

  • 原始类型定义:擦除泛型信息后,字节码中类型变量的真正类型。
  • 无限定类型参数的原始类型Object。 例如,Pair<T> 的原始类型为 Pair,其中 TObject 替换。
  • 有限定类型参数的原始类型:第一个边界的类型变量类。 例如,Pair<T extends Comparable> 的原始类型为 Comparable

10.泛型的编译期检查

  • 编译期检查先于类型擦除:Java 编译器先进行泛型类型检查,再进行类型擦除和编译。

  • 类型检查针对引用:泛型类型检查是针对引用的,使用泛型引用调用方法时会进行类型检测,与引用指向的对象无关。

    Java

    ``` ArrayList list1 = new ArrayList(); // 引用 list1 具有泛型类型检查能力 list1.add("1"); // 编译通过 list1.add(1); // 编译错误,list1 引用会进行类型检查

    ArrayList list2 = new ArrayList(); // 引用 list2 没有泛型类型检查能力 list2.add("1"); // 编译通过 list2.add(1); // 编译通过,list2 引用不会进行类型检查 ```

  • 参数化类型不考虑继承关系ArrayList<String> 不是 ArrayList<Object> 的子类,泛型类型之间不存在继承关系。这是为了避免潜在的 ClassCastException 风险,并保持泛型设计的初衷——解决类型转换问题。

11.泛型的多态与桥接方法

  • 类型擦除与多态冲突:类型擦除会导致子类重写父类泛型方法时,方法签名不一致,产生多态冲突。

  • 桥接方法解决冲突:JVM 通过生成桥接方法来解决类型擦除和多态的冲突。

  • 桥接方法原理:编译器为子类生成桥接方法,桥接方法参数类型为原始类型 Object,方法内部实际调用子类重写的指定泛型类型参数的方法。

    Java

    ``` class DateInter extends Pair { @Override public void setValue(Date value) { super.setValue(value); } @Override public Date getValue() { return super.getValue(); } }

    // 反编译 DateInter 类字节码后,会发现编译器生成了桥接方法: public void setValue(Object); // 桥接方法 public Object getValue(); // 桥接方法 ```

12.泛型的限制

  • 基本类型不能作为泛型类型:例如,不能使用 ArrayList<int>, 只能使用 ArrayList<Integer>。因为类型擦除后,原始类型变为 ObjectObject 无法存储基本类型值。Java 的自动装箱拆箱机制允许 list.add(1) 这种写法。
  • 泛型类型不能实例化:不能使用 new T() 创建泛型类型的实例。因为编译期无法确定泛型参数化类型,类型擦除后 T 变为 Objectnew T() 会变成 new Object(),失去泛型本意。可以通过反射 clazz.newInstance() 实例化泛型。
  • 泛型数组初始化限制
    • 不能采用具体的泛型类型初始化泛型数组,例如 new ArrayList<String>[10] 是非法的。
    • 可以使用通配符 new ArrayList<?>[10] 初始化泛型数组,但需要强制类型转换。
    • 推荐使用 List 集合替代泛型数组。
    • 可以使用反射 Array.newInstance(Class<T> componentType, int length) 创建泛型数组。
  • 泛型类中的静态成员限制:静态方法和静态变量不能使用泛型类声明的泛型类型参数,因为静态成员在类加载时初始化,此时泛型类的泛型参数类型尚未确定。
  • 异常中使用泛型限制
    • 不能 catch 泛型变量。
    • 泛型类不能 extends Throwable
    • 可以在 throws 声明中使用泛型变量。

13.获取泛型参数类型

  • 反射获取泛型类型信息:可以通过 java.lang.reflect.Type 接口及其子接口(ParameterizedType 等)在运行时获取泛型参数类型。
  • ParameterizedType 接口
    • getActualTypeArguments(): 返回确切的泛型参数类型数组。
    • getRawType(): 返回原始类型。
    • getOwnerType(): 返回所属类型。

14.实现泛型的方法

  1. 类型擦除 (Type Erasure)

    • 核心思想:在编译时进行类型检查,但在运行时丢弃(擦除)泛型类型信息。泛型类型在运行时被替换为其原始类型(通常是 Object 或类型边界)。
    • 优点
      • 兼容性好:易于向后兼容旧代码,特别是对于像 Java 这样需要保持与早期版本兼容的语言。
      • 运行时开销低:由于运行时不保留泛型类型信息,运行时开销相对较小。
      • 代码膨胀小:通常只生成一份泛型代码的副本,减少代码体积。
    • 缺点
      • 运行时类型信息丢失:运行时无法访问完整的泛型类型信息,限制了反射等功能。
      • 类型安全性受限:类型检查主要在编译时进行,运行时类型安全保障不如类型具化彻底。
      • 存在类型擦除的局限性:例如,无法创建泛型数组、无法进行 instanceof 泛型类型判断等。
    • 典型语言:Java
    • 类型具化 (Type Reification)

    • 核心思想:在运行时完整地保留泛型类型信息。编译时进行类型检查,并将泛型类型信息作为元数据保留在运行时环境中。

    • 优点
      • 运行时类型信息可用:运行时可以精确访问泛型类型参数,支持强大的反射能力。
      • 更强大的反射:可以实现更精细的类型判断、类型操作和动态创建泛型实例等。
      • 更彻底的类型安全:运行时类型信息有助于实现更严格的类型检查和类型转换。
      • 潜在的性能优势:在某些场景下,可以避免装箱拆箱和类型转换,提升性能。
    • 缺点
      • 运行时开销增加:存储类型信息和运行时类型操作会增加运行时开销。
      • 可能导致代码膨胀 (取决于实现方式,代码特化方式更明显)。
      • 兼容性挑战:对于已存在大量非泛型代码的语言,引入类型具化可能面临兼容性问题。
    • 典型语言:C# (.NET CLR), Swift, Kotlin (部分具化)
    • 代码特化 / 单态化 (Code Specialization / Monomorphization)

    • 核心思想:为每种不同的泛型类型参数组合生成独立的、特化的代码副本。

    • 优点
      • 性能高:由于代码是针对特定类型生成的,可以进行更激进的优化,通常能获得最佳性能。
      • 运行时类型信息保留:生成的代码本身就包含了具体的类型信息,相当于实现了类型具化。
    • 缺点
      • 代码膨胀:如果泛型代码被大量不同的类型参数实例化,会导致代码体积显著增加,编译时间也会变长。
    • 典型语言:C++ 模板, Rust 泛型 (部分情况下)
    • 部分类型具化 / 混合方法 (Partial Reification / Hybrid Approaches)

    • 核心思想:在类型擦除和完全类型具化之间取得平衡,运行时保留部分泛型类型信息或根据情况选择是否擦除。

    • 优点
      • 灵活性和性能的折衷:尝试在运行时类型信息可用性和性能开销之间找到平衡点。
      • 可以实现一些类型擦除下无法完成的任务:例如,创建泛型数组、进行模式匹配等 (如 Scala 的 ManifestTypeTag)。
    • 缺点
      • 复杂性增加:混合方法通常比纯粹的类型擦除或类型具化更复杂。
      • 仍然存在局限性:可能不是完全的类型具化,仍然存在一些运行时类型信息限制。
    • 典型技术:Scala Manifests/TypeTags, Kotlin Inline Classes (实验性)
    • 动态语言的泛型 (鸭子类型 / 结构类型)

    • 核心思想:不强制编译时类型检查,依赖运行时的 "鸭子类型" 或 "结构类型"。

    • 优点
      • 非常灵活:代码简洁,易于编写,无需显式类型声明。
      • 动态性强:更符合动态语言的特性。
    • 缺点
      • 类型安全风险:类型错误通常在运行时才会被发现,编译时无法提供类型安全保障。
      • 性能开销:动态类型检查可能带来运行时性能开销。
    • 典型语言:Python, JavaScript, TypeScript (结构类型为主)
实现方法 运行时类型信息 性能 代码膨胀 兼容性/灵活性 类型安全 典型语言/技术
类型擦除 (Type Erasure) 丢失 运行时开销低 兼容性好 编译时 Java
类型具化 (Type Reification) 保留 运行时开销高 (取决于实现) 类型安全 运行时+编译时 C#, Swift, Kotlin (部分)
代码特化 (Monomorphization) 保留 性能高 性能优先 运行时+编译时 C++ 模板, Rust 泛型 (部分)
部分类型具化/混合方法 部分保留 介于两者之间 介于两者之间 平衡 运行时+编译时 Scala Manifests/TypeTags, Kotlin Inline Classes (实验性)
鸭子类型/结构类型 运行时检查 运行时开销可能较高 非常灵活 运行时 Python, JavaScript, TypeScript (结构类型为主)