Java/Kotlin 泛型

泛型是 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)
}
相关推荐
来来走走10 小时前
kotlin学习 基础知识一览
android·开发语言·kotlin
雨白15 小时前
StateFlow 与 SharedFlow:在协程中管理状态与事件
android·kotlin
tangweiguo0305198719 小时前
ProcessLifecycleOwner 完全指南:优雅监听应用前后台状态
android·kotlin
消失的旧时光-194320 小时前
Kotlin reified泛型 和 Java 泛型 区别
java·kotlin·数据
雨白2 天前
Flow 的异常处理与执行控制
android·kotlin
ClassOps2 天前
Gradle Groovy 和 Kotlin kts 语法对比
android·kotlin·gradle·groovy
用户69371750013842 天前
⚡Kotlin 五大神器完全解析:let、with、run、apply、also 一次搞懂,面试官都笑了!
android·kotlin
木易 士心2 天前
Spring Boot + Kotlin + Gradle 构建现代化后端应用
spring·kotlin
urkay-2 天前
Android 线程详解
android·java·kotlin·iphone·androidx