泛型是 Java 和 Kotlin 中用于实现 "参数化类型" 的核心机制,允许类、接口、方法在定义时不指定具体类型,而是在使用时动态指定,从而实现代码复用和类型安全。
基本定义
泛型类/接口
Java:通过 class 类名<T> 或 interface 接口名<T> 定义,类型参数放在类/接口名后。
java
// 泛型类
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// 泛型接口
public interface List<T> {
void add(T element);
T get(int index);
}
kotlin:语法类似,通过 class 类名<T> 或 interface 接口名<T> 定义,类型参数同样放在名称后。
kotlin
// 泛型类
class Box<T>(private var value: T) {
fun set(value: T) {
this.value = value
}
fun get(): T = value
}
// 泛型接口
interface List<T> {
fun add(element: T)
fun get(index: Int): T
}
泛型方法
Java:类型参数声明在方法返回值前,需显式声明。
java
public static <T> T getFirstElement(List<T> list) {
return list.get(0);
}
kotlin:类型参数声明在方法名前<T>,语法更紧凑,且编译器类型推断更智能。
kotlin
fun <T> getFirst(list: List<T>): T = list[0]
类型擦除
Java 和 Kotlin 的泛型都基于类型擦除实现:编译时检查类型安全,运行时泛型类型信息被擦除(仅保留原始类型)。
Java
java
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 运行时泛型信息被擦除,均为 ArrayList 类型
System.out.println(strList.getClass() == intList.getClass()); // 输出 true
Kotlin
kotlin
val strList: List<String> = listOf()
val intList: List<Int> = listOf()
// 同样擦除,运行时类型相同
println(strList.javaClass == intList.javaClass) // 输出 true
无法在运行时检查泛型具体类型(如 obj instanceof List<String> 编译错误)。
变异
变异描述泛型类型与子类型的关系,核心问题:若 A 是 B 的子类型,Generic<A> 与 Generic<B> 是什么关系?
Java 需在每次使用泛型时通过通配符控制变异(使用处处理),而 Kotlin 在定义泛型时一次性声明(声明处处理),更简洁且避免重复逻辑。
协变
Java:Generic<A> 是 Generic<B> 的子类型(A 是 B 的子类型),用 ? extends B 表示。
java
List<Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // 合法:Integer 是 Number 子类型,协变
Number n = numList.get(0); // 合法:能读(输出 Number)
numList.add(1); // 编译错误:无法确定具体类型,写操作不安全
泛型类型只能输出 B 及其子类型(读操作安全,写操作受限)。
Kotlin:用 out 标记类型参数,表示泛型类型是生产者,只能输出 T,即作为返回值。不能输入 T,会导致类型不安全。即若 A : B,则 Generic<A> : Generic<B>
kotlin
val intList: List<Int> = listOf(1, 2, 3)
val numList: List<Number> = intList // 合法:协变允许这种赋值
Kotlin 通过 out 关键字在泛型定义时声明协变,如下所示:
kotlin
// 协变接口:T 只能作为输出(返回值)
interface DataReader<out T> {
fun read(): T // 读取数据,返回 T 类型(输出操作,合法)
}
class IntReader : DataReader<Int> {
private var current = 1
override fun read(): Int {
return current++
}
}
val intReader: DataReader<Int> = IntReader()
// 利用协变特性,将 Int 读取器赋值给 Number 读取器变量,(因为 Int 是 Number 的子类型,协变允许)
val numberReader: DataReader<Number> = intReader // 合法
逆变
Java:Generic<B> 是 Generic<A> 的子类型(A 是 B 的子类型),用 ? super A 表示。泛型类型只能输入 A 及其子类型(写操作安全,读操作受限)。
java
List<Number> numList = new ArrayList<>();
List<? super Integer> intSuperList = numList; // 合法:Number 是 Integer 父类型,逆变
intSuperList.add(1); // 合法:能写(输入 Integer)
Number n = intSuperList.get(0); // 编译错误:只能读为 Object,类型不安全
kotlin:用 in 标记类型参数,表示泛型类型是消费者(只能输入 T,即作为参数)。即若 A : B,则 Generic<B> : Generic<A>。
kotlin
// 逆变接口:T 只能作为输入(方法参数)
interface DataProcessor<in T> {
fun process(data: T) // 处理数据,接收 T 类型参数(输入操作,合法)
}
// 处理所有 Number 类型(父类型处理器)
class NumberProcessor : DataProcessor<Number> {
override fun process(data: Number) {
println("NumberProcessor process: $data")
}
}
val numberProcessor: DataProcessor<Number> = NumberProcessor()
val intProcessor: DataProcessor<Int> = numberProcessor
不变
默认情况下,Generic<A> 与 Generic<B> 无父子关系(即使 A 是 B 的子类型)。对于 kotlin 来说,未用 out/in 标记时,泛型类型默认不变。
通配符与星投影
Java:无界通配符 ?,表示任意类型,读写均受限,可读为 Object,可写 null(无意义)
java
List<?> anyList = new ArrayList<String>();
Object obj = anyList.get(0); // 合法:读为 Object
anyList.add(null); // 合法:只能加 null
anyList.add("a"); // 编译错误:类型不确定
Kotlin:星投影 *,是 Kotlin 对未知类型参数的简化表示,行为与 Java 通配符类似,但规则更明确:
- 对于 Generic<out T : Upper>(协变且有上界):Generic<*> 等价于 Generic<out Upper>。
- 对于 Generic<in T>(逆变):Generic<*> 等价于 Generic<in Nothing>(Nothing 是 Kotlin 所有类型的子类型)。
- 对于 Generic<T : Upper>(不变且有上界):Generic<*> 等价于 Generic<Upper>。
Kotlin 标准库的 List 是协变的(List<out T>),其星投影 List<*> 等价于 List<out Any?>
kotlin
val anyList: List<*> = listOf("a", 1) // 等价于 List<out Any?>
val obj: Any? = anyList[0] // 合法:读为 Any?
// anyList.add("b") // 编译错误:星投影不可写(类似 Java ?)
泛型约束
泛型约束用于限制类型参数的范围(如只能是某类的子类)。
上界约束(限制为某类型的子类型)
Java:用 extends 声明,类型参数必须是指定类型的子类型。
java
// T 必须是 Number 的子类型(如 Integer、Double)
public class NumberBox<T extends Number> {
private T value;
}
NumberBox<Integer> intBox = new NumberBox<>();
Kotlin:用 : 声明,语法更简洁。
kotlin
// T 必须是 Number 的子类型
class NumberBox<T : Number>(private var value: T)
// 合法
val intBox = NumberBox(1) // 推断 T 为 Int
// 不合法
// val strBox = NumberBox("a")
多个上界约束
Java:用 & 连接,且类必须放在接口前(只能有一个类上界)。
java
// T 必须是 Number 子类且实现 Comparable<T>
public class ComparableNumberBox<T extends Number & Comparable<T>> {
}
Kotlin:用 where 子句,支持多个上界,顺序无限制。
kotlin
// T 必须是 Number 子类且实现 Comparable<T>
class ComparableNumberBox<T> where T : Number, T : Comparable<T>
Java 中「类型参数」不能声明下界,仅通配符支持 ? super A(见变异部分)。Kotlin 中「类型参数」也不直接支持下界,但可通过 in 逆变间接实现类似效果(本质是消费型参数)。
Java 和 kotlin 都不直接支持下界约束,但是,Java 可以通过通配符 ? super A,kotlin 可以通过 in 逆变实现类似的效果。
Kotlin 独有的泛型特性:reified
由于类型擦除,Java 无法在运行时获取泛型类型信息(如 T.class 不合法)。但 Kotlin 通过 inline 函数配合 reified 关键字,可在运行时保留类型信息。
kotlin
inline fun <reified T : Activity> startActivity(
context: Context,
extras: Bundle? = null
) {
val intent = Intent(context, T::class.java)
extras?.let { intent.putExtras(it) }
context.startActivity(intent)
}