泛型
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>
类:key
和value
属性的类型由外部指定,展示了多元泛型。
-
泛型接口 (Generic Interfaces)
-
定义: 在接口名后使用
<T>
声明类型参数。 - 类型参数作用域: 接口中的方法可以使用类型参数
T
定义返回值类型。 - 实现: 实现泛型接口的类,可以选择继续使用泛型,或者指定具体的类型。
- 示例:
Info<T>
接口:定义了返回泛型类型T
的getVar()
方法。InfoImpl<T>
类:实现了Info<T>
接口,并继续使用泛型。
-
泛型方法 (Generic Methods)
-
定义: 在方法返回值类型之前使用
<T>
声明类型参数。 - 类型参数作用域: 方法的参数、返回值和方法体内部可以使用类型参数
T
。 - 调用: 调用泛型方法时,通常不需要显式指定类型,编译器会自动进行类型推断。 如果需要显式指定,可以使用
方法名.<类型参数>(参数)
的形式。 Class<T>
参数: 泛型方法中可以使用Class<T>
类型的参数,用于在运行时获取类型信息,并结合反射创建泛型类的对象。- 灵活性: 泛型方法比泛型类更灵活,因为可以在调用方法时才指定类型,而泛型类在实例化时就需要指定类型。
- 示例: 定义一个泛型方法,参数为
Class<T>
,可以创建不同类型的对象。
- 定义: 在类名后使用
4. 泛型的上下限 (Generics Bounds)
- 解决类型转换问题: 泛型中,
List<B>
不是List<A>
的子类型,即使B
是A
的子类。为了解决这种隐含的类型转换问题,引入了泛型的上下限。 - 上限 (
<? 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>
(因为Object
是String
的父类)。
- 含义: 表示类型参数可以是
- 无限制通配符 (
<?>
)- 含义: 表示类型参数可以是任何类型。
- 作用: 当不需要关心具体的类型,或者只需要进行一些不依赖于具体类型参数的操作时使用。
- 使用原则 (PECS - Producer Extends, Consumer Super)
- 生产者 (Producer): 如果参数化类型是用来生产 (读取)
T
类型的数据,使用<? extends T>
(上限)。 - 消费者 (Consumer): 如果参数化类型是用来消费 (写入)
T
类型的数据,使用<? super T>
(下限)。 - 既是生产者又是消费者: 如果既需要生产又需要消费,则不使用通配符,使用精确的参数类型。
- 生产者 (Producer): 如果参数化类型是用来生产 (读取)
- 多重限制 (
<T extends Type1 & Type2>
)- 使用
&
符号: 可以使用&
符号同时指定多个类型参数的上限,类型参数必须同时满足所有上限的约束。 - 示例:
<T extends Staff & Passenger>
表示类型T
必须同时是Staff
和Passenger
的子类型。
- 使用
5. 泛型数组 (Generic Arrays)
- 限制: Java 中不能直接创建泛型数组,例如
new ArrayList<String>[10]
是非法的。 - 原因: 类型擦除和数组的协变性导致泛型数组的创建存在类型安全问题。
- 常用规避方法:
- 讨巧的使用场景 (变长参数): 使用泛型方法接收可变参数
T... arg
,可以返回泛型数组T[]
,但这实际上是利用了编译器的一些特性,本质上仍然不是直接创建泛型数组。 - 合理的创建方式 (反射
Array.newInstance
): 通过反射Array.newInstance(type, size)
可以创建指定类型的数组,并进行强制类型转换(T[])
,但这会产生警告,需要开发者自行保证类型安全。
- 讨巧的使用场景 (变长参数): 使用泛型方法接收可变参数
6.伪泛型与类型擦除
- Java 泛型的本质是“伪泛型”:为了兼容旧版本,Java 泛型并非真正的泛型,而是一种编译时的概念。
- 类型擦除:在编译阶段,Java 会将代码中的泛型类型信息擦除,转换为其原始类型,就像没有泛型一样。运行时,JVM 中不存在泛型类型信息。
7.类型擦除原则:
- 删除泛型声明:移除
<>
及其内部的类型参数。 - 替换类型参数为原始类型:
- 无限定类型参数(
<T>
或<?>
)替换为Object
。 - 有限定上界的类型参数(
<T extends Number>
或<? extends Number>
)替换为最左边限定类型(即上界,如Number
)。 - 有限定下界的类型参数(
<? super Number>
)替换为上界(对于super
来说,上界是Object
)。
- 无限定类型参数(
- 插入类型转换代码:为了类型安全,在必要时插入强制类型转换代码。
- 生成桥接方法:为了保持泛型擦除后的多态性,自动生成“桥接方法”。
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
,其中T
被Object
替换。 - 有限定类型参数的原始类型:第一个边界的类型变量类。 例如,
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>
。因为类型擦除后,原始类型变为Object
,Object
无法存储基本类型值。Java 的自动装箱拆箱机制允许list.add(1)
这种写法。 - 泛型类型不能实例化:不能使用
new T()
创建泛型类型的实例。因为编译期无法确定泛型参数化类型,类型擦除后T
变为Object
,new 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.实现泛型的方法
-
类型擦除 (Type Erasure)
- 核心思想:在编译时进行类型检查,但在运行时丢弃(擦除)泛型类型信息。泛型类型在运行时被替换为其原始类型(通常是
Object
或类型边界)。 - 优点:
- 兼容性好:易于向后兼容旧代码,特别是对于像 Java 这样需要保持与早期版本兼容的语言。
- 运行时开销低:由于运行时不保留泛型类型信息,运行时开销相对较小。
- 代码膨胀小:通常只生成一份泛型代码的副本,减少代码体积。
- 缺点:
- 运行时类型信息丢失:运行时无法访问完整的泛型类型信息,限制了反射等功能。
- 类型安全性受限:类型检查主要在编译时进行,运行时类型安全保障不如类型具化彻底。
- 存在类型擦除的局限性:例如,无法创建泛型数组、无法进行
instanceof
泛型类型判断等。
- 典型语言:Java
-
类型具化 (Type Reification)
-
核心思想:在运行时完整地保留泛型类型信息。编译时进行类型检查,并将泛型类型信息作为元数据保留在运行时环境中。
- 优点:
- 运行时类型信息可用:运行时可以精确访问泛型类型参数,支持强大的反射能力。
- 更强大的反射:可以实现更精细的类型判断、类型操作和动态创建泛型实例等。
- 更彻底的类型安全:运行时类型信息有助于实现更严格的类型检查和类型转换。
- 潜在的性能优势:在某些场景下,可以避免装箱拆箱和类型转换,提升性能。
- 缺点:
- 运行时开销增加:存储类型信息和运行时类型操作会增加运行时开销。
- 可能导致代码膨胀 (取决于实现方式,代码特化方式更明显)。
- 兼容性挑战:对于已存在大量非泛型代码的语言,引入类型具化可能面临兼容性问题。
- 典型语言:C# (.NET CLR), Swift, Kotlin (部分具化)
-
代码特化 / 单态化 (Code Specialization / Monomorphization)
-
核心思想:为每种不同的泛型类型参数组合生成独立的、特化的代码副本。
- 优点:
- 性能高:由于代码是针对特定类型生成的,可以进行更激进的优化,通常能获得最佳性能。
- 运行时类型信息保留:生成的代码本身就包含了具体的类型信息,相当于实现了类型具化。
- 缺点:
- 代码膨胀:如果泛型代码被大量不同的类型参数实例化,会导致代码体积显著增加,编译时间也会变长。
- 典型语言:C++ 模板, Rust 泛型 (部分情况下)
-
部分类型具化 / 混合方法 (Partial Reification / Hybrid Approaches)
-
核心思想:在类型擦除和完全类型具化之间取得平衡,运行时保留部分泛型类型信息或根据情况选择是否擦除。
- 优点:
- 灵活性和性能的折衷:尝试在运行时类型信息可用性和性能开销之间找到平衡点。
- 可以实现一些类型擦除下无法完成的任务:例如,创建泛型数组、进行模式匹配等 (如 Scala 的
Manifest
和TypeTag
)。
- 缺点:
- 复杂性增加:混合方法通常比纯粹的类型擦除或类型具化更复杂。
- 仍然存在局限性:可能不是完全的类型具化,仍然存在一些运行时类型信息限制。
- 典型技术: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 (结构类型为主) |