From Java To Kotlin 2:Kotlin 类型系统与泛型终于懂了

news2024/11/24 20:49:27

上期主要分享了 From Java To Kotlin 1 :空安全、扩展、函数、Lambda。

这是 From Java to Kotlin 第二期。
带来 表达式思维、子类型化、类型系统、泛型。

From Java to Kotlin 关键在于 思维的转变

表达式思维

Kotlin 中大部分语句是表达式
表达式思维是一种编程思维。 编程思维是一种非常抽象的概念,很多时候是只可意会不可言传的。
不过,从某种程度上看,学习编程思维,比学习编程语法更重要。因为编程思维决定着我们的代码整体的架构与风格,而具体的某个语法反而没那么大的影响力。当然,如果对 Kotlin 的语法没有一个全面的认识,编程思维也只会是空中楼阁。就像,我们学会了基础的汉字以后开始写作文:学了汉字以后,如果没掌握写作的技巧,是写不出好的文章的。同理,如果学了 Kotlin 语法,却没有掌握它的编程思维,也是写不出优雅的 Kotlin 代码的。

下面我们看一段 Kotlin 代码

//--- 1
var i = 0
if (data != null) {
    i = data
}

//--- 2 
var j = 0
if (data != null) {
    j = data
} else {
    j = getDefault()
    println(j)
}

//--- 3 
var k = 0
if (data != null) {
    k = data
} else {
    throw NullPointerException()
}

//--- 4 
var x = 0
when (data) {
    is Int -> x = data
    else -> x = 0
}

//--- 5
var y = 0
try {
    y = "Kotlin".toInt()
} catch (e: NumberFormatException) {
    println(e)
    y = 0
}

这些代码,如果我们用平时写 Java 时的思维来分析的话,是挑不出太多毛病的。但是站在 Kotlin 的角度,就完全不一样了。利用 Kotlin 的语法,我们完全可以将代码写得更加简洁,就像下面这样:

//--- 1
val i = data ?: 0

//--- 2 
val j = data ?: getDefault().also { println(it) }

//--- 3 
val k = data?: throw NullPointerException()


//--- 4 
val x = when (data) {
    is Int -> data
    else -> 0
}

//--- 5 
val y = try {
    "Kotlin".toInt()
} catch (e: NumberFormatException) {
    println(e)
    0
}

这段代码看起来就简洁了不少,所以从 Java 转到 Kotlin 要格外注意思维转变,培养表达式思维。

这里有个疑问:Kotlin 为什么就能用这样的方式写代码呢?其实这是因为:if、when、throw、try-catch 这些语法,在 Kotlin 当中都是表达式

那么,这个“表达式”到底是什么呢?其实,与表达式(Expression)对应的,还有另一个概念,我们叫做语句(Statement)。

  • 表达式(Expression),是一段可以产生值的代码;
  • 语句(Statement),则是一句不产生值的代码。

我们可以简单来概括一下:
表达式(Expression)有值,而语句(Statement)不总有。

用一个更详细的例子解释:


val a = 1    // statement
println(a)   // statement

// statement
var i = 0
if (data != null) {
    i = data
}

// 1 + 2 是一个表达式,但是对b的赋值行为是statement
val b = 1 + 2

// if else 整体是一个表达式
// a > b是一个表达式,  子表达式
// a - b是一个表达式,  子表达式
// b - a是一个表达式,  子表达式。
fun minus(a: Int, b: Int) = if (a > b) a - b else b - a

// throw NotImplementedError() 是一个表达式
fun calculate(): Int = throw NotImplementedError()

这段代码是描述了常见的 Kotlin 代码模式,从它的注释当中,我们其实可以总结出这样几个规律:

  • 赋值语句,就是典型的 statement;
  • if 语法,既可以作为语句,也可以作为表达式;
  • 语句与表达式,它们可能会出现在同一行代码中,比如 val b = 1 + 2;
  • 表达式还可能包含“子表达式”,就比如这里的 minus 方法;
  • throw 语句,也可以作为表达式。

看到这里,可能又有一个疑问,那就是:calculate() 这个函数难道不会引起编译器报错吗?


//       函数返回值类型是Int,实际上却抛出了异常,没有返回Int
//                ↓       ↓
fun calculate(): Int = throw NotImplementedError()

要想搞清楚这个疑问, 需要理解Kotlin的类型系统

小结

  • Koltin表达式思维是指时刻记住 Kotlin 大部分的语句都是表达式,它们可以产生返回值。利用这种思维,往往可以大大简化代码逻辑。

Kotlin 的类型系统

类、类型和子类型

  • 类(class)是指一种数据类型,类定义定义对象的属性和方法,可以用来创建对象实例,例如 class Person(val name: String),用于表示一个人的属性和行为。
  • 类型(type)是指一个_变量或表达式 **的 **数据类型_。类型可以用来描述变量或表达式的特征和限制取值范围可用的操作)。在Kotlin中,每个变量或表达式都有一个确定的类型,例如Int、String、Boolean等,类型可以是可空的或非空的,例如 String?String
  • 子类型(subtype)是指一个类型的子集,即一个类型的值可以赋值给另一个类型的变量或表达式。例如 class Student(name: String, val grade: Int) : Person(name) 中,StudentPerson 的子类型,StringString?的子类型 。

在 Kotlin 中,类和类型之间有一定的对应关系,但并不完全相同。一个类可以用于构造多个类型,
例如泛型类 List<T> 可以构造出 List<String>List<Int> 等不同的类型。一个类型也可以由多个类实现,例如接口类型 Runnable 可以由多个实现了 run() 方法的类实现。

子类型化

先看一段代码:

image.png
非可空类型的 strNotNull:String ,可以赋值给 可空类型的strNullable:String? ;
可空类型的strNullable:String? 不可以赋值给 非可空类型的 strNotNull:String。

可以看出每一个Kotlin都可以用于构造至少两种类型

根据子类型化的定义,String 是 String?的子类型。

看到这里可能有个疑问?没有继承关系,String 并没有 继承 String?,为啥String是 String? 的子类型。

其实我也有, 经常开发 Java 会有一个误区:认为只有继承关系类型之间才可以有父子类型关系。

因为在Java中,类与类型大部分情况下都是“等价”的(在Java泛型出现前)。事实上,“继承”和“子类型化”是两个完全不同的概念。子类型化的核心是一种类型的替代关系


子类型化, 以下内容引用自维基百科

在编程语言理论中,子类型(动名词,英语:subtyping(也有翻译为子类型化))是一种类型多态的形式。这种形式下,子类型(名词,英语:subtype)可以替换另一种相关的数据类型(超类型,英语:supertype)。也就是说,针对超类型元素进行操作的子程序、函数等程序元素,也可以操作相应的子类型。如果 S 是 T 的子类型,这种子类型关系通常写作 S <: T,意思是在任何需要使用 T 类型对象的_环境中,都可以安全地使用_ S 类型的对象。

由于子类型关系的存在,某个对象可能同时属于多种类型,因此,子类型(英语:subtyping)是一种类型多态的形式,也被称作子类型多态(英语:subtype polymorphism)或者包含多态(英语:inclusion polymorphism)。

子类型与面向对象语言中(类或对象)的继承是两个概念。子类型反映了类型(即面向对象中的接口)之间的_关系_;而继承反映了一类对象可以从另一类对象创造出来,是_语言特性 _的实现。因此,子类型也称接口继承;继承称作实现继承

子类型 - 维基百科,自由的百科全书


子类型化可表示为:

S <:T

以上S是T的子类,这意味着在需要T类型 的地方,S类型的 同样适用,可以用 S 类型的 替换。

所以在前面的例子中, 虽然String与String?看起来没有继承关系,然而在我们需要用String?类型值的地方,显然可以传入一个类型为String的值,这在编译上不会产生问题。反之却不然。 所以String?是String的父类型。

继承强调的是一种“实现上的复用”,而子类型化是一种类型语义的关系,与实现没关系。对于 Java 语言,由于一般在声明父子类型关系的同时也声明了继承的关系,所以造成了某种程度上的混淆。


类型系统

Kotlin 的类型还分为可空类型不可空类型。Any 是所有非空类型的根类型;而 Any? 是所有可空类型的根类型。
我们猜测 Kotlin 的类型体系可能是这样的:
image.png
那Any 与 Any? 之间是什么关系呢?

Any 、Any?与 Java 的 Object

Java 当中的 Object 类型,对应 Kotlin 的“Any?”类型。但两者并不完全等价,因为 Kotlin 的 Any 可以没有 wait()、notify() 之类的方法。因此,我们只能说 Kotlin 的“Any?”与 Java 的 Object 是大致对应的。

下面是Java 代码,它有三个方法,分别是可为空的 Object 类型、不可为空的 Object 类型,以及无注解的 Object 类型。

public class TestTypeJava {

    @Nullable  // 可空注解
    public Object test() { return null; }

    // 默认
    public Object test1() { return null; }

    @NotNull  // 不可空注解
    public Object test2() { return 1; }
}

上面的代码通过 Convert Java File to Kotlin File 转换成 Kotlin:

class TestTypeJava {
    // 可空注解
    fun test(): Any? {
        return null
    }
    
    fun test1(): Any? { //  可以看出默认情况下,   Java Object 对应 Kotlin Any?
        return null
    }

    // 不可空注解
    fun test2(): Any {
        return 1
    }
}

可以看出默认情况下,没有注解标记可空信息的时候, Java Object 对应 Kotlin Any?。

有些时候Java代码包含了可空性的信息,这些信息使用注解来表达。当代码中出现了这样的信息时,Kotlin就会使用它。因此Java中的@Nullable String被Kotlin当作String?,而@NotNull String就是String

image.png
如果没有是否可空注解, Java类型会变成 Kotlin 中的平台类型(后面会解释)

了解了 Any 和 Any?的关系,可以画出关系图
image.png

Unit 与 Void 与 void

先看一段 Java 代码

public class PrintHello {
    public void printHelloWorld() {
        System.out.println("Hello World!");
    }
}

转成 Kotlin

class PrintHello {
    fun printHelloWorld():Unit { // Redundant 'Unit' return type 
        println("Hello World!")
    }
}

Java 的 void 关键字在 Kotlin 里是没有的,取而代之的是一个叫做 Unit 的东西,

Unit 和 Java 的 void 真正的区别在于,void 是真的表示什么都不返回,而 Kotlin 的 Unit 却是一个真实存在的类型

public object Unit {
    override fun toString() = "kotlin.Unit"
}

它是一个 object,也就是 Kotlin 里的单例类型或者说单例对象。当一个函数的返回值类型是 Unit 的时候,它是需要返回一个 Unit 类型的对象的:

   fun printHelloWorld():Unit {
        println("Hello World!")
        return Unit  // return Unit 可以省略
    }

只不过因为它是个 object ,所以唯一能返回的值就是 Unit 本身。

这两个 Unit 是不一样的,上面的是 Unit这个类型,下面的是 Unit这个单例对象,它俩长得一样但是是不同的东西。注意了,这个并不是 Kotlin 给Unit 的特权,而是 object 本来就有的语法特性。如果有需要,也可以用同样的格式来使用别的单例对象,是不会报错的:

包括也可以这样写:

val unit: Unit = Unit

也是一样的道理,等号左边是类型,等号右边是对象——当然这么写没什么实际作用啊,单例可以直接用。

object Zhangsan

fun getZhangsan(): Zhangsan {  // 单例可以直接使用
  return Zhangsan
}

因此,在结构上,Unit 并没有任何特别之处,它只是 Kotlin 的 object。除了对于函数返回值类型和返回值的自动补充之外,它的特殊之处更多地在于语义和用途的角度。它是由官方规定的,用于表示**「什么也不返回」的场景的返回值类型**。但这只是它被规定的用法而已,本质上它是一个实实在在的类型。在 Kotlin 中,不存在真正没有返回值的函数,所有「没有返回值」的函数实质上的返回值类型都是 Unit,而返回值也都是 Unit 这个单例对象。这是 Unit 和 Java 的 void 在本质上的不同之处。

Unit 相比 void 带来什么不同

Unit 去除了无返回值函数的特殊性和有返回值函数之间的本质区别,从而使得很多事情变得更加简单,这种通用性为我们带来了便利。

例子: 函数类型的函数参数

虽然不能说Java中的所有函数调用都是表达式,但是可以说Kotlin中的所有函数调用都是表达式。

是因为存在特例void,在Java中如果声明的函数没有返回值,那么它就需要用void来修饰。如:

  public void printHelloWorld() {
        System.out.println("Hello World!");
  }

因为 void 不是类型,所以 函数printHelloWorld()无法匹配 () -> Unit 函数类型

class VoidTest {
    fun printHelloWorld1():Unit { // 作为参数时,就有函数类型  () -> Unit
        println("Hello World!")
    }

    fun runTask(task: () -> Any) {
        when (val result = task()) {
            Unit -> println("result is Unit")
            String -> println("result is a String: $result")
            else -> println("result is an unknown type")
        }
    }

    @Test
    fun main1() {
        val var1 = ::printHelloWorld1   //  () -> Unit
        runTask (var1) //  () -> Unit
        runTask {   "This is string" } //:() -> String
        runTask { 42 }  // () -> Int
    }
}

现在有了 Unit , fun printHelloWorld1():Unit 作为参数时,就有函数类型 () -> Unit 。

注意:在 Java 当中,Void 和 void 不是一回事(注意大小写),前者是一个 Java 的类,后者是一个用于修饰方法的关键字。如下所示:

public final class Void {


    @SuppressWarnings("unchecked")
    public static final Class<Void> TYPE = (Class<Void>) Class.getPrimitiveClass("void");

   
    private Void() {}
}

JAVA中Void类是一个不可实例化的占位符类,用来保存一个引用代表Java关键字void的Class对象。它的作用是在反射或泛型中表示void类型。
例如:Map接口的put方法需要两个类型参数,如果我们只需要存储键而不需要存储值,就可以使用Void类作为类型参数

Map<String, Void> map = new HashMap<>(); map.put("key", null);。

了解了 UnitUnit?的关系后,可以画出关系图
image.png

Nothing

Nothing 是 Kotlin 所有类型的子类型。 Noting 的概念与 Any? 恰好相反。

Nothing 也叫底类型(BottomType)。

Nothing的源码是这样的:

public class Nothing private constructor()

可以看到它本身虽然是 public 的,但它的构造函数是 private 的,这就导致我们没法创建它的实例;而且它不像 Unit 那样是个 object:

public object Unit {
  override fun toString() = "kotlin.Unit"
}

而是个普通的 class;并且在源码里 Kotlin 也没有帮我们创建它的实例
这些条件加起来,结果就是:Nothing 这个类既没有也不会有任何的实例对象
基于这样的前提,当我们写出这个函数声明的时候:

fun nothing(): Nothing {

}

我们可能无法找到一个合适的值来返回,但是在编写代码时,我们必须返回一个值。这种情况下,我们遇到了一个悖论,即必须返回一个值,但却永远找不到合适的返回值

Nothing的作用: 作为函数 永远不会返回结果 的提示

fun nothing() : Nothing {
  throw RuntimeException("Nothing!")
}

根据Nothing的特性, Nothing 专门用于抛异常。

public class NotImplementedError(message: String = "An operation is not implemented.") : Error(message)


@kotlin.internal.InlineOnly
public inline fun TODO(): Nothing = throw NotImplementedError()

从上面这段代码可以看出,Kotin 源码中 throw 表达式的返回值类型是 Nothing。

throw 这个表达式的返回值是 Nothing 类型。而既然 Nothing 是所有类型的子类型,那么它当然是可以赋值给任意其他类型的。
所以表达式思维中的问题就可以解答了

//       函数返回值类型是Int,实际上却抛出了异常,没有返回Int
//                ↓       ↓
fun calculate(): Int = throw NotImplementedError()

作用二

Nothing 类的构造函数是私有的,因此我们无法构造出它的实例。当 Nothing 类型作为函数参数时,一个有趣的现象就出现了:

// 这是一个无法调用的函数,因为找不到合适的参数
fun show(msg: Nothing) {}

show(null) // 报错
show(throw Exception()) // 虽然不报错,但方法仍然不会调用

在这里,我们定义了一个 show 函数,它的参数类型是 Nothing。由于 Nothing 的构造函数是私有的,我们将无法调用 show 函数,除非我们抛出异常,但这没有意义。
这个概念在泛型星投影的时候是有应用的,具体后面会解释。

作用三

而除此之外,Nothing 还有助于编译器进行代码流程的推断。比如说,当一个表达式的返回值是 Nothing 的时候,就往往意味着它后面的语句不再有机会被执行。如下图所示:
image.png

了解了 Nothing 和 Nothing?的关系后,可以画出关系图
image.png

平台类型

image.png

平台类型在Kotlin中表示为type!(如String!,Int!, CustomClass!)。
Kotlin平台类型本质上就是Kotlin不知道可空性信息的类型,即可以当作可空类型,也可以当作非空类型。平台类型只能来自Java,因为Java中所有的引用都可能为null,而Kotlin中对null有严格的检查和限制。
但是在Kotlin中是禁止声明平台类型的变量的。

image.png

具体的代码示例如下:

// Java 代码
public class Person {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

// Kotlin 代码
fun main() {
    val person = Person() // 
    val name = person.name // name 是 String! 类型
    println(name.length) // 可能抛出空指针异常
    person.name = null // 允许赋值为 null
}

在这个例子中, name 是平台类型,
image.png

因为它们来自于 Java 代码。Kotlin 编译器不会检查它们是否为 null,所以需要程序员自己负责。如果要避免空指针异常,可以使用安全调用运算符(?.)或非空断言运算符(!!)来处理平台类型。

println(name?.length) // 安全调用,如果 name 为 null 则返回 null
println(name!!.length) // 非空断言,如果 name 为 null 则抛出异常

平台类型是指 Kotlin 和 Java 的互操作性问题, 在混合项目中要多加注意。

小结

  • Any 是所有非空类型的根类型,而 Any? 才是所有类型的根类型。
  • Unit 与 Java 的 void 类型相似,代表一个函数不需要返回值;而 Unit? 这个类型则没有太多实际的意义。
  • 当 Nothing 作为函数返回值时,意味着这个函数永远不会返回结果,而且还会截断程序的后续流程。Kotlin 编译器也会根据这一点进行流程分析。
  • 当 Nothing 作为函数参数时,就意味着这个函数永远无法被正常调用。这在泛型星投影的时候是有一定应用的。
  • Nothing 可以看作是 Nothing? 的子类型,因此,Nothing 可以看作是 Kotlin 所有类型的底类型。
  • 正是因为 Kotlin 在类型系统中加入了 Unit、Nothing 这两个类型,才让大部分无法产生值的语句摇身一变,成为了表达式。这也是“Kotlin 大部分的语句都是表达式”的根本原因。

泛型:让类型更加安全

Kotlin 的泛型与 Java 一样,都是一种语法糖,即只在源代码中有泛型定义,到了class级别就被擦除了。 泛型(Generics)其实就是把类型参数化,真正的名字叫做类型参数,它的引入给强类型编程语言加入了更强的灵活性。

泛型的优点

  1. 类型安全:泛型可以在编译时检查类型,从而避免了在运行时出现类型不匹配的错误。这可以提高程序的可靠性和稳定性。
  2. 代码重用:泛型可以使代码更加通用和灵活,从而可以减少代码的重复和冗余。例如,我们可以编写一个通用的排序算法,可以用于任何实现了 Comparable 接口的类型。

在 Java 中,我们常见的泛型有:泛型类、泛型接口、泛型方法和泛型属性,Kotlin 泛型系统继承了 Java 泛型系统,同时添加了一些强化的地方。

泛型接口/类(泛型类型)

定义泛型类型,是在类型名之后、主构造函数之前用尖括号括起的大写字母类型参数指定:

声明泛型接口

Java:

//泛型接口
interface Drinks<T> {
    T taste();
    void price(T t);
}

Kotlin:

//泛型接口
interface Drinks<T> {
    fun taste(): T
    fun price(t: T)
}

声明泛型类

Java

abstract class Color<T> {
    T t;
    abstract void printColor();
}
class Blue {
    String color = "blue";
}
class BlueColor extends Color<Blue> {
    public BlueColor(Blue1 t) {
        this.t = t;
    }
    @Override
    public void printColor() {
        System.out.println("color:" + t.color);
    }
}

Kotlin

abstract class Color<T>(var t: T/*泛型字段*/) {
    abstract fun printColor()
}

class Blue {
    val color = "blue"
}

class BlueColor(t: Blue) : Color<Blue>(t) {
    override fun printColor() {
        println("color:${t.color}")
    }

}

泛型字段

定义泛型类型字段,可以完整地写明类型参数,如果编译器可以自动推定类型参数,也可以省略类型参数:

abstract class Color<T>(var t: T/*泛型字段*/) {
    abstract fun printColor()
}

声明泛型方法

Kotlin 泛型方法的声明与 Java 相同,类型参数要放在方法名的前面:

Java

public static <T> T fromJson(String json, Class<T> tClass) {
    T t = null;
    try {
        t = tClass.newInstance();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return t;
}

Kotlin

fun <T> fromJson(json: String, tClass: Class<T>): T? {
    /*获取T的实例*/
    val t: T? = tClass.newInstance()
    return t
}

泛型约束

Java 中可以通过有界类型参数来限制参数类型的边界,Kotlin中泛型约束也可以限制参数类型的上界:

Java

    public static <T extends Comparable<T>> T maxOf(T a, T b) {
        if (a.compareTo(b) > 0) return a;
        else return b;
    }

Kotlin

fun <T : Comparable<T>> maxOf(a: T, b: T): T {
    return if (a > b) a else b
}

image.png

where关键字: 多个上界用 where

Java 中多约束: &

public static <T extends CharSequence & Comparable<T>> List<T> test(List<T> list, T threshold) {
    
   return list.stream().filter(it -> it.compareTo(threshold) > 0).collect(Collectors.toList());
}

Kotin 中多约束:where

//多个上界的情况
fun <T> test(list: List<T>, threshold: T): List<T>
        where T : CharSequence,
              T : Comparable<T> {
    return list.filter { it > threshold }.map { it }
}

所传递的类型T必须同时满足 where 子句的所有条件,在上述示例中,类型 T 必须既实现了 CharSequence 也实现了 Comparable。

泛型形参&泛型实参

泛型类:
image.png

泛型函数:
image.png

泛型的型变

不变

先看一段 Java 代码,我们知道在Java中 ,List无法赋值给List

public class JavaGeneryc {
    public static void main(String[] args) {
        List<Apple> apples = new ArrayList<>();
        apples.add(new Apple());

        List<Fruit> fruits = apples; // 编译错误

        for (Fruit fruit : fruits) {
            System.out.println(fruit);
        }
    }
}

class Fruit {
    // 父类
}

class Apple extends Fruit {
    // 子类
}

image.png

但是到了Kotlin这里我们发现了一个奇怪的现象

fun main2(args: Array<String>) {
    val stringList:List<String> = ArrayList<String>()
    val anyList:List<Any> = stringList//编译成功
}

image.png

在Kotlin中竟然能将List赋值给List,不是说好的Kotlin和Java的泛型原理是一样的吗?怎么到了Kotlin中就变了?其实我们前面说的都没错,关键在于这两个List并不是同一种类型。我们分别来看一下两种List的定义:

image.png
虽然都叫List,也同样支持泛型,但是Kotlin的List定义的泛型参数前面多了一个 out关键词(加上out 发生协变 ),这个关键词就对这个List的特性起到了很大的作用。
普通方式定义的泛型是不变的,简单来说就是不管类型A和类型B是什么关系,Generic与Generic(其中Generic代表泛型类)都没有任何关系。比如,在Java中String是Oject的子类型,但List并不是List的子类型,在Kotlin中泛型的原理也是一样的。Kotin 使用 out 才发生了变化。

out 位置与 in 位置

image.png
函数参数的类型叫作in位置,而函数返回类型叫作out位置

协变 :保留子类型化关系

如果在定义的泛型类和泛型方法的泛型参数前面加上out关键词,说明这个泛型类及泛型方法是协变,简单来说类型A是类型B的子类型,那么Generic也是Generic的子类型,

image.png

协变点 (out 位置)

image.png
函数返回值类型为泛型参数。

协变的特征

只能消费,只能取

  • 子类型化会被保留(Producer是Producer的子类型)
  • T只能用在out位置

image.png

interface Book

interface EduBook : Book

class BookStore<out T : Book> {
    fun getBook(): T {
        TODO()
    }
}

fun covariant(){
//    教材书店
    val eduBookStore: BookStore<EduBook> = BookStore<EduBook>()
//     书店
    val bookStore: BookStore<Book> = eduBookStore // 协变,教辅书店是书店的子类型

    val book: Book = bookStore.getBook()
    val eduBook : EduBook = eduBookStore.getBook()
}

image.png

协变小结

•子类型 Derived 兼容父类型 Base
•生产者 Producer<Derived>兼容 Producer

逆变: 反转子类型化关系

如果在定义的泛型类和泛型方法的泛型参数前面加上in关键词,说明这个泛型类及泛型方法是逆变,简单来说类型A是类型B的子类型,那么Generic是Generic的子类型,类型父子关系反转。
image.png

逆变点 (in 位置)

image.png
函数参数类型为泛型参数。

逆变的特征

只能生产,只能放入

  • 子类型化会被反转(Consumer 是 Consumer的子类型)
  • T只能用在in位置

image.png

image.png
垃圾不能扔到干垃圾桶,但是可以扔到垃圾桶。
干垃圾可以扔到垃圾桶,也可以扔到垃圾桶。
由此可以看出垃圾桶可以替代干垃圾桶, 所以干垃圾桶是父类型。

open class Waste

// 干垃圾
class DryWaste : Waste()

// 垃圾桶
class Dustbin<in T : Waste> {
    fun put(t: T) {
        TODO()
    }
}

fun contravariant(){
    val dustbin: Dustbin<Waste> = Dustbin<Waste>()
    val dryWasteDustbin: Dustbin<DryWaste> = dustbin

    val waste = Waste()
    val dryWaste = DryWaste()

    dustbin.put(waste)
    dustbin.put(dryWaste)

//    dryWasteDustbin.put(waste)
    dryWasteDustbin.put(dryWaste)
}

声明为 in ,在 out 位置使用,是会报错的。

image.png

image.png

逆变小结
  • 子类型 Derived 兼容父类型 Base
  • 消费者 Consumer兼容 Consumer< Derived>
  • 记忆小技巧: in 表示逆变, in 倒序过来是 ni(逆)。

型变小结

协变逆变不变型
Producer ConsumerMutableList:
类的子类型化保留了:Producers是 Producer<Animal>的子类型子类型化反转了:Consumer 是 Consumer的子类型没有子类型化
T只能在out 位置T只能在 in 位置T可以在任何位置

泛型中的out与in与 Java 上下界通配符关系

在Kotlin中out代表协变,in代表逆变,为了加深理解我们可以将Kotlin的协变看成Java的上界通配符,将逆变看成Java的下界通配符:

//Kotlin使用处协变
fun sumOfList(list: List<out Number>)

//Java上界通配符
void sumOfList(List<? extends Number> list)

//Kotlin使用处逆变
fun addNumbers(list: List<in Int>)

//Java下界通配符
void addNumbers(List<? super Integer> list)

小结

Java 泛型Java 中代码示例Kotlin 中代码示例Kotlin 泛型
泛型类型class Boxclass Box泛型类型
泛型方法 T fromJson(String json, Class tClass)fun fromJson(json: String, tClass: Class): T?泛型函数
有界类型参数class Box<T extends Comparableclass Box<T : Comparable>泛型约束
上界通配符void sumOfList(List<? extends Number> list)fun sumOfList(list: List)使用处协变
下界通配符void addNumbers(List<? super Integer> list)fun addNumbers(list: List)使用处逆变

总的来说,Kotlin 泛型更加简洁安全,但是和 Java 一样都是有类型擦除的,都属于编译时泛型。


下期分享:

星投影

注解 @UnsafeVariance

内联特化(内联强化) reified


系列

From Java To Kotlin:空安全、扩展、函数、Lambda很详细,这次终于懂了

From Java To Kotlin 2:Kotlin 类型系统与泛型

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/614769.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue.js 中的数据请求是什么?如何进行数据请求?

Vue.js 中的数据请求是什么&#xff1f;如何进行数据请求&#xff1f; Vue.js 是一款流行的前端框架&#xff0c;它提供了许多方便的工具和 API&#xff0c;用于构建交互式的用户界面。其中&#xff0c;数据请求是 Vue.js 中重要的一部分&#xff0c;它可以让我们从服务器获取…

通过python封装商品ID采集1688商品详情数据,1688商品详情接口,1688API接口

1688是阿里巴巴集团旗下的B2B电商平台&#xff0c;提供海量的商品和服务。通过1688的API接口可以获取到商品的详细数据&#xff0c;并进行采集和分析。 1688的商品详情接口包括以下信息&#xff1a; 商品名称商品图片商品价格商品库存商品属性商品描述商品评价商品销量商品SK…

什么蓝牙耳机通话效果好,介绍几款不错的骨传导耳机

骨传导耳机是一种新型的耳机&#xff0c;相比于传统的耳机&#xff0c;骨传导耳机听歌时不需要将耳朵堵上&#xff0c;不会因为长时间佩戴而对听力造成损害。它不需要入耳也能听到声音&#xff0c;在户外运动时能够及时听到环境音&#xff0c;避免安全隐患。现在在骨传导市面上…

从零开始学习JavaScript:轻松掌握编程语言的核心技能⑤

从零开始学习JavaScript&#xff1a;轻松掌握编程语言的核心技能⑤ 1. JavaScript 函数定义2. JavaScript 函数参数2.1 函数显式参数(Parameters)与隐式参数(Arguments)2.1.1 显式参数&#xff08;Parameters&#xff09;2.1.2 隐式参数&#xff08;Arguments&#xff09; 2.2 …

HVV的艺术系列 之 上线的艺术

上线的艺术 很多时候&#xff0c;拿下的机器情况复杂多样。判断其出网性应该是首要工作。 01 到底该不该上线 承认的是&#xff0c;MSF和CS都是及其出色的后渗透工具。但是面对这种复杂多样的环境&#xff0c;上不上线是个我们要去认真考虑的问题&#xff0c;CS和MSF究竟能给我…

报表自动生成软件有哪些?热门报表自动生成软件推荐

随着数字化时代的到来&#xff0c;数据分析和处理变得越来越重要。在商业领域中&#xff0c;每个公司都需要制作各种类型的报表&#xff0c;以了解他们的运营情况、市场趋势和其他有关业务的信息。但是&#xff0c;手动创建这些报表是非常耗时且容易出错的。因此&#xff0c;报…

Vue3+Three.js+antvG2实战项目 智慧城市(五)

前言 在网上找了很久都没有找到使用Three.js开发智慧城市的免费文章或者免费视频,自己花了一点时间做了一个纯前端的智慧城市项目。 技术栈都是最新的:vue3vitetypeScriptThreeantv G2 源码分享 源码 模型,天空图盒子链接分享(不想下载源码可以只用下这个)提取码1234 20230424_…

报表开发工具Stimulsoft Report新增“用户参数”新功能,来看如何使用?

Stimulsoft Reports 是一款报告编写器&#xff0c;主要用于在桌面和Web上从头开始创建任何复杂的报告。可以在大多数平台上轻松实现部署&#xff0c;如ASP.NET, WinForms, .NET Core, JavaScript, WPF, Angular, Blazor, PHP, Java等&#xff0c;在你的应用程序中嵌入报告设计器…

PN/Modbus协议下,PLC与IO模块能否建立无线通讯?

在实际系统中&#xff0c;一个车间内PLC与多个IO信号点需要建立通讯&#xff0c;从而提高工作效率&#xff0c;通常距离在几十米到上百米不等。在有通讯需求的时候&#xff0c;如果布线的话&#xff0c;工程量较大且不美观&#xff0c;这种情况下比较适合采用无线通信方式。本方…

chatgpt赋能python:Python字符串截取指南:如何截取指定位置字符串

Python字符串截取指南&#xff1a;如何截取指定位置字符串 在Python中&#xff0c;字符串截取是一项非常常见的操作。当我们需要从一个字符串中提取特定位置的字符或子串时&#xff0c;我们可以使用Python内置的截取函数和切片操作来实现。在本文中&#xff0c;我们将介绍如何…

为什么上了ERP,效率反而更低?

业界一直有句老话&#xff1a;“不上ERP等死&#xff0c;上了ERP找死”&#xff0c;可把ERP的尴尬处境说透了。 有人把ERP奉为信仰&#xff1a;“那些说ERP不好用的根本是没用明白”&#xff0c;有人则认为ERP只是卖概念&#xff0c;冷嘲&#xff1a;“实施ERP的企业&#xff…

Measurement Studio 2019 f3 Crack

Measurement Studio是Microsoft Visual Studio的扩展软件&#xff0c;提供了用于创建测试和测量应用程序的.NET工具。 了解Measurement Studio的功能 Measurement Studio是​唯一​一​款.NET​工具​套​件&#xff0c;专为在Microsoft Visual Studio中构建工程应用&#xff0…

2023年,知识付费行业呈现哪些发展趋势?

艾媒咨询数据显示&#xff0c;2022年中国知识付费市场规模达1126.5亿元&#xff0c;较2015年增长约70倍&#xff0c;预计将在2025年超过2800亿元。随着疫情形势持续好转&#xff0c;知识付费的“居家红利”或将逐渐消退&#xff0c;但三年来用户的付费求知和在线学习习惯已经养…

C# WPF读取文本内容的7种方式

文章目录 前言一、界面展示二、使用步骤1.引入库2.界面代码3.后台代码&#xff08;1&#xff09;打开文件&#xff08;2&#xff09;第一种&#xff1a;基于FileStream&#xff0c;并结合它的Read方法读取指定的字节数组&#xff0c;最后转换成字符串进行显示。&#xff08;3&a…

迎战算力黄金时代,惠普Z系列工作站焕新升级

作者伍杏玲 随着今年 AIGC 浪潮席卷全球&#xff0c;AI应用的迅速增长对算力提出更多挑战。据《全球计算力指数评估报告》显示&#xff0c;未来5年&#xff0c;全球算力规模将以超过50%的速度飞速增长&#xff0c;预计到2024年&#xff0c;中国将生产出高达36ZB的数据量&#…

盲区死角「暗藏」风险隐患,哪些智能化方案或将前装「标配」

因车辆盲区死角而引发的交通事故&#xff0c;近年来呈现高发态势。上周一则《交警实测SUV视野盲区有多大&#xff0c;75个孩子藏在盲区&#xff0c;一点看不见》的视频火爆社交网络。 视频中&#xff0c;交警让幼儿园老师坐进一辆SUV警车的驾驶位并戴上眼罩&#xff0c;然后引导…

uniapp可视化操作-diygw

uniapp可视化操作:DIY可视化-拖拽设计1天搞定主流小程序环境安装 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 uniapp可视化操作:DIY可视化-拖拽设计1天搞定主流小程序环境安装前言一、DIY可视化桌面客户端安装…

Linux--ServerProgramming--(2)socket

1. 主机字节序和网络字节序 下面以32位机为前提&#xff1a;CPU累加器一次能装载至少 4 字节&#xff0c;即一个整数。字节序分为&#xff1a;1.大端字节序&#xff08;big endian&#xff09;指一个整数的高位字节&#xff08;23~32 bit &#xff09;存储在内存的低地址处&am…

Nat Biotechnol -- 生成式AI进军更高效价抗体

类似于ChatGPT的语言模型已被应用于改进针对COVID-19、埃博拉和其他病毒的抗体疗法。 代码看不懂&#xff1f;ChatGPT 帮你解释&#xff0c;详细到爆&#xff01; 单克隆抗体&#xff08;Y形&#xff09;与SARS-CoV-2病毒纤突蛋白&#xff08;红色&#xff09;上的结合位点&…

长光程气体吸收池的真空压力精密控制解决方案

摘要&#xff1a;目前用于气体吸收池真空压力控制的压力控制器存在有残留气体和无法进行高真空测量的问题&#xff0c;无法进行微量气体的光谱分析。为此&#xff0c;本文提出了动态平衡法的解决方案&#xff0c;即采用两个高速真空低漏率的电子针阀分别调节进气和出气流量&…