泛型协变与逆变:Kotlin 与 Java 的异同

        在编程中,泛型的协变(covariance)和逆变(contravariance)是处理类型兼容性和安全性的重要工具。Kotlin 提供 outin 关键字,而 Java 使用 ? extends? super 通配符实现类似功能。

       本文将从概念、规则、使用场景及示例等方面,详细分析 Kotlin 和 Java 的实现方式及异同点。

1. 基础概念

1.1 协变(Covariance)

协变允许使用子类型的泛型对象赋值给父类型的泛型变量,解决“生产者”场景的问题。

  • Kotlin:out
  • Java:? extends
1.2 逆变(Contravariance)

逆变允许使用父类型的泛型对象赋值给子类型的泛型变量,解决“消费者”场景的问题。

  • Kotlin:in
  • Java:? super
1.3 类型不变性(Invariance)

默认情况下,泛型是不可变的,这意味着 List 不能赋值给 List,即使 StringObject 的子类型。

2. 协变:out vs ? extends

2.1 特性对比
特性 Kotlin out Java ? extends
定义方向 子类型 -> 父类型 子类型 -> 父类型
适用场景 “生产者”模式(只输出) “生产者”模式(只输出)
限制 无法用于输入 无法用于输入
关键字位置 声明时(定义站点) 使用时(使用站点)
2.2 示例
Kotlin 示例:out
class Box(val value: T) // T 是协变的

fun demoOut() {
    val stringBox: Box = Box("Hello")
    val charSeqBox: Box = stringBox // 合法
    println(charSeqBox.value) // 输出:Hello
}
Java 示例:? extends
class Box {
    private T value;
    public Box(T value) {
        this.value = value;
    }
    public T getValue() {
        return value;
    }
}

void demoExtends() {
    Box charSeqBox = new Box<>("Hello"); // 合法
    CharSequence value = charSeqBox.getValue(); // 合法
    System.out.println(value); // 输出:Hello
}
2.3 限制

协变的核心限制是:泛型类型不能用于输入

Kotlin 示例:
class Box(private val value: T) {
    // fun setValue(newValue: T) {} // 错误,out 泛型不能用于输入
}

fun demoOutWrite() {
    val charSeqBox: Box = Box("Hello")
    // charSeqBox.setValue("World") // 错误
}

Java 示例:

void demoExtendsWrite() {
    List list = new ArrayList<>();
    // list.add("Hello"); // 错误:不能向上界通配符添加数据
}

 3. 逆变:in vs ? super

3.1 特性对比
特性 Kotlin in Java ? super
定义方向 父类型 -> 子类型 父类型 -> 子类型
适用场景 “消费者”模式(只输入) “消费者”模式(只输入)
限制 无法从泛型中读取具体类型 无法从泛型中读取具体类型
关键字位置 声明时(定义站点) 使用时(使用站点)
3.2 示例
Kotlin 示例:in
class Consumer {
    fun consume(item: T) {
        println("Consuming: $item")
    }
}

fun demoIn() {
    val charSeqConsumer: Consumer = Consumer() // 合法
    charSeqConsumer.consume("Hello") // 合法

    val stringConsumer: Consumer = Consumer() // 合法
    stringConsumer.consume("Hello") // 合法
}

Java 示例:? super

class Consumer {
    public void consume(T item) {
        System.out.println("Consuming: " + item);
    }
}

void demoSuper() {
    Consumer consumer = new Consumer(); // 合法
    consumer.consume("Hello"); // 合法
}
3.3 限制

逆变的核心限制是:泛型类型不能用作输出具体类型

Kotlin 示例:
class Consumer {
    // fun get(): T { return ... } // 错误,in 泛型不能用于输出
}

Java 示例:

void demoSuperRead() {
    List list = new ArrayList<>();
    Object value = list.get(0); // 合法,但只能读取为 Object
}

4. 类型不变性与灵活性

特性 Kotlin Java
默认行为 泛型是不可变的 泛型是不可变的
灵活性 inout 限制在声明时 ? extends? super 灵活调整
示例:默认类型不变性
Kotlin:
fun demoInvariance() {
    val stringList: List = listOf("Hello")
    // val charSeqList: List = stringList // 错误,类型不变性
}

Java:

void demoInvariance() {
    List stringList = new ArrayList<>();
    // List charSeqList = stringList; // 错误,类型不变性
}

5. 总结

5.1 相同点
  • Kotlin 的 inout 和 Java 的 ? super? extends 都用于解决泛型的协变与逆变问题。
  • 协变允许子类型泛型对象赋值给父类型泛型变量,逆变允许父类型泛型对象赋值给子类型泛型变量。
  • 都有明确的限制:协变不能用于输入,逆变不能用于输出。
5.2 不同点
特性 Kotlin Java
关键字位置 声明时(inout 使用时(? super? extends
灵活性 更简洁,声明时确定 更灵活,使用时调整边界
类型推断能力 类型推断更强 类型推断相对较弱
5.3 使用建议
  • Kotlin 的 inout:适合场景较为简单、类型边界在定义时可以确定的情况。
  • Java 的 ? super? extends:更灵活,适合需要在使用时动态调整类型边界的场景。

你可能感兴趣的:(java,kotlin,开发语言)