上期主要分享了 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() 方法的类实现。

子类型化

先看一段代码:

非可空类型的 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 的类型体系可能是这样的:那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

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

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

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?的关系后,可以画出关系图

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 的时候,就往往意味着它后面的语句不再有机会被执行。如下图所示:

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

平台类型

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 是平台类型,

因为它们来自于 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。

泛型形参&泛型实参

泛型类:

泛型函数:

泛型的型变

不变

先看一段 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的定义:

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

**

out 位置与 in 位置

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

协变 :保留子类型化关系


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

**

image.png

协变点 (out 位置)

函数返回值类型为泛型参数。

协变的特征

只能消费,只能取

  • 子类型化会被保留(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****的子类型,类型父子关系反转。

**

逆变点 (in 位置)

函数参数类型为泛型参数。

逆变的特征

只能生产,只能放入

  • 子类型化会被反转(Consumer是 Consumer的子类型)

  • T只能用在in位置

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

逆变小结
  • 子类型 Derived 兼容父类型 Base

  • 消费者 Consumer兼容 Consumer< Derived>

  • 记忆小技巧: in 表示逆变, in  倒序过来是 ni(逆)。

型变小结

协变 逆变 不变型
Producer Consumer MutableList:
类的子类型化保留了: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 Box class Box 泛型类型
泛型方法 T fromJson(String json, ClasstClass) funfromJson(json: String, tClass: Class): T? 泛型函数
有界类型参数 class Box<T extends Comparable class 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 2:Kotlin 类型系统与泛型的更多相关文章

  1. Kotlin 在kotlin内使用Java的一些注意(长篇)

    首先呢,大部分的java在kotlin内是可以使用的. 但是有些java的关键字和kotlin的一样,而意义不一样就需要转义.(单引号括起来的)这一点需要注意. 这是一个长篇 我会不断更新.毕竟我也在 ...

  2. gradle 混合编程java、scala、kotlin、groovy

    众所周知,java是基于JVM的开发语言,但能够在JVM上运行的开发语言不仅仅有java,目前还有很热门的kotlin(kotlin不仅仅只能开发Android).scala.groovy等等.目前国 ...

  3. 【Try Kotlin】Kotlin Koans 代码笔记

    Kotlin Koans 心印 Introduction 1.Hello, world! Simple Functions Take a look at function syntax and mak ...

  4. kotlin学习(8)泛型

    泛型的声明 与Java不同的是,Kotlin要求类型实参要么被显示的说明,要么能被编译器推导出来.因为Java在1.5版本才引入泛型,所以为了兼容过去的版本,可以不直接声明List类型的变量,而不说明 ...

  5. java从基础知识(八)泛型

    1.什么是泛型? 泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型类.泛型接口.泛型方法 ...

  6. JAVA之旅(二十一)——泛型的概述以及使用,泛型类,泛型方法,静态泛型方法,泛型接口,泛型限定,通配符

    JAVA之旅(二十一)--泛型的概述以及使用,泛型类,泛型方法,静态泛型方法,泛型接口,泛型限定,通配符 不知不觉JAVA之旅已经写到21篇了,不得不感叹当初自己坚持要重学一遍JAVA的信念,中途也算 ...

  7. java为什么要用类型擦除实现泛型?--c++,java,c# 的泛型是如何实现的

    所以总结一下c++,java,c#的泛型.c++的泛型在编译时完全展开,类型精度高,共享代码差.java的泛型使用类型擦出,仅在编译时做类型检查,在运行时擦出,共享代码好,但是类型精度不行.c#的泛型 ...

  8. 在已有的Java项目中使用Kotlin

    需要配置两个地方首先在项目点gradle文件中引入Kotlin插件 然后在module的build.gradle文件中应用插件

  9. Android java项目中引用kotlin,混合开发工程配置

    https://www.jianshu.com/p/9220227cdfb3 buildscript { ext.kotlin_version = '1.2.71' repositories { go ...

  10. 【kotlin】kotlin中List中添加List怎么操作

    如题,List集合添加一个List集合怎么操作 如上,现在有了List<A>,A类中有个字段List<B>, 新创建一个List<B>,想把LIst<A> ...

随机推荐

  1. 提供一款局域网聊天小工具,对接了chatGPT3.5

    账号.密码随便填,只要自己能记住就可以,网络地址如效果图(本机IP地址+8080端口),效果图如下 服务地址用本机IP,端口可以随便用一个,如8888,填完记得点击按钮开启: 通讯地址如果你有其他人的 ...

  2. 基于开源的 ChatGPT Web UI 项目,快速构建属于自己的 ChatGPT 站点

    作为一个技术博主,了不起比较喜欢各种折腾,之前给大家介绍过 ChatGPT 接入微信,钉钉和知识星球(如果没看过的可以翻翻前面的文章),最近再看开源项目的时候,发现了一个 ChatGPT Web UI ...

  3. Bean的自动装配(Autowired)

    Bean的自动装配(Autowired) 自动装配是Spring满足bean依赖的一种方式 Spring会在上下文中自动寻找,并自动给bean装配属性 在Spring中有三种自动装配的方式 在xml中 ...

  4. Unity学习笔记01 —— 编辑器

    场景Scene 基本操作 按下鼠标滚轮拖动场景,滑动滚轮缩放场景. 鼠标右键旋转场景,点击""后,通过左键移动场景. 点击右键同时按下W/S/A/D/Q/E键可实现场景漫游. 在S ...

  5. super 与 this 关键字

    super与this用法相似: 1.普通的直接引用 2.形参与成员名字重名,用 this 来指代类本身,super指代父类 public class Students extends Person { ...

  6. 组织树查询-Jvava实现(递归)

    1.首先查询出组织机构 就是一个简单的查询 List<Dept> deptList = mapper.getDeptList(); Map<Long, OrgNode> nod ...

  7. Spring的Factories机制介绍

    Java 的 SPI 机制 Java SpringBoot 加载 yml 配置文件中字典项 Spring的Factories就是Spring版本的Java Spi. Spring Factories的 ...

  8. python-pygal

    准备写大作业的时候发现了一个绝绝子的python库. 原文:https://blog.damavis.com/en/creating-vector-graphics-with-python/ 官网:h ...

  9. C# 笔迹擦除8边形

    擦除区域与橡皮大小不一致 测试反馈,擦除区域与真实的橡皮大小不一致: 上图中,橡皮显示是圆形的,但擦除效果是一个"8边形"区域. 找了一台8K屏,确实是能复现的: 看到这个诡异的8 ...

  10. Centos7.x 安装Chrome + Chrome driver

    一.安装Chrome 1.执行下面命令进行安装操作 yum install https://dl.google.com/linux/direct/google-chrome-stable_curren ...