var a = 1 // 变量
val b = 1 // 常量
类型大小(位)最小值最大值
Byte8-128127
Short16-3276832767
Int32-2,147,483,648 (-2^31)2,147,483,647(2^31-1)
Long64-9,223,372,036,854,775,808 (-2^63)9,223,372,036,854,775,807(2^63 - 1)
这里以上面的8位大小的Byte类型为例,在计算机底层存储数据时,只有8个bit位(一个bit位就可以表示一个0或1)来存储它,那么它能表示的最大值和最小值就是:

00000000 ~ 11111111 转换为十进制就是 0 ~ 255

不过为了能够表示负数,计算机一般使用补码进行表示,所以,上面的最小值和最大值就变成了-128 ~ 127了。

val a = 1_000_000_000 // 长数据分隔符
val a = 0xAF // 十六进制也行
val a = 0b1001 //0b前缀表示二进制数据,后面的1010对应着十进制的9
 
val b: UByte = 1u  // UByte类型, 由变量提供的类型
val s: UShort = 1u // UShort类型, 由变量提供的类型
val l: ULong = 1u  // ULong类型, 由变量提供的类型
 
val a1 = 42u    // UInt类型,根据数字大小自动推断得到
val a2 = 0xFFFF_FFFF_FFFFu // ULong类型,根据数字大小自动推断得到
  • UByte:一个无符号8位整数,范围从0到255
  • UShort:无符号16位整数,范围从0到65535
  • UInt:一个无符号32位整数,范围从0到2^32 - 1
  • ULong:一个无符号64位整数,范围从0到2^64 - 1
    uL和UL可以将文字直接标记为无符号Long类型:
val a = 1UL // ULong类型,直接使用后缀标记
val pi = 3.1415 // 默认推断为Double类型
val one: Double = 1 // 这种写法是错误的,因为1不是小数,无法编译通过
val one: Double = 1.0 // 但是这种写法就是对的,因为这样表示就是小数,即使小数位是0
val e: Float = 2.7182818284f // 这里表示为Float会导致精度折损,得到2.7182817

字符串除了和字符串拼接之外,也可以和其他类型进行拼接:

fun main() {
    val a = 10
    val text = "这是拼接的值" + a
    println(text)   //打印出来就是与其他类型的拼接结果
    // 但是我们需要注意字符串拼接的顺序,只能由字符串拼接其他类型,如果是其他类型拼接字符串,可能会出现问题:
}

但是现在我们就是希望其他类型的数据拼在最前面,这里应该怎么做呢?我们可以使用字符串模版来完成:

fun main() {
    val a = 10
    val text = "这是拼接的值$a"  //这里的$为模版表达式,可以直接将后面跟着的变量或表达式以字符串形式替换到这个位置
    println(text)
    val text = "$a 这是拼接的值" //注意这里$a之后必须空格,否则会把后面的整个字符串认为这个变量的名字
    println(text)
}

出现这种情况除了用空格去解决之外,我们也可以添加一个花括号:

val text = "${a}这是拼接的值"  //添加花括号就可以消除歧义了
val text = "${a > 0}这是拼接的值"  //花括号中也可以写成表达式

由于美元符用于模版表达式了,所以说如果我们希望在字符串中仅仅表示$这个字符,那么我们需要用到转义:

val text = "\$这是美元符"   //普通字符串直接使用\$表示
//原始字符串要套个娃
val str = """
  ${'$'}这是美元符    
	"""

流程控制

除了直接执行语句之外,我们也可以将if和else用作结果判断,比如:

fun main() {
    val score = 2
  	//这里判断socre是否大于60,是就得到Yes,否就得到No,并且可以直接赋值给变量
    val res = if (score > 60) "Yes" else "No"
    println(res) // No
}

对于多行代码块的情况,默认最后一行作为返回的结果:

fun main() {
    val score = 2
    val res = if (score > 60) {
        println("不错啊期末没挂科")
        "Yes"   //代码块默认最后一行作为返回结果
    } else {
        println("不会有人Java期末还要挂科吧")
        "No" // println 不能放在最后
    }
}

when

when定义具有多个分支的条件表达式。它类似于类似Java和C语言中的switch语句,它简单的形式看起来像这样:

fun main() {
    val c = 'A'
    when (c) {
        'A' -> println("去尖子班!准备冲刺985大学!")
        'B' -> println("去平行班!准备冲刺一本!")
        'C' -> println("去职高深造。")
    }

如果将when用作表达式,则else分支必须存在,除非编译器能推断出所有可能的情况都包含分支条件,比如下面的例子:

fun main() {
    val c = 'A'
    val numericValue = when (c) {
        'B' -> 0
        'A' -> 1
        else -> 2    //还有其他情况,这里必须添加else,不然其他情况岂不是没返回的东西?
    }
}
fun main() {
    val c = true
    val numericValue = when (c) {
        false -> 0
        true -> 1
        // 由于Boolean只具备真和假条件,这里的'else' 就不再强制要求
      	// 这同样适用于比如枚举类等
    }
}

在when_语句_中,遇到以下情况,携带else分支是必须的:

  • when分支中仅有一个Boolean类型、枚举 或 密封,以及用于判断的目标变量是可空的情况(后面会讲解)
  • when分支没有包括该判断目标的所有可能的值。

for循环

fun main() {
    for (i in 1..10 step 2) {
        println(i)
    }
    // 倒叙
    for (i in 10 downTo 1) {
        println(i)   //这里得到的就是10到1倒着排列的范围了
    }
}
fun main() {
    for (i in 1..3) {
        for (j in 1..3) {
            if (i == j) continue  //当i == j时加速循环
            println("$i, $j")
        }
    }
}

这里的continue加速的对象并不是外层的for,而是离它最近的内层for循环,break也是同样的规则:

fun main() {
    outer@ for (i in 1..3) {   //在循环语句前,添加 标签@ 来进行标记
        inner@ for (j in 1..3) {
            if (i == j) break@outer  //break后紧跟要结束的循环标记,当i == j时终止外层循环
            println("$i, $j")
        }
    }
}

函数定义

fun 函数名称([函数参数…]): 返回值类型 {
//函数体
}

//这个函数用于打印一段文本
fun hello(): Unit {  //本质上应该是返回Unit类型,这个类型表示空,类似于Java中的void,默认情况下可以省略
    println("PHP是世界上最好的语言.kt")
}

有些时候,我们也可以设计一些参数带有默认值的函数,如果在调用函数时不填入参数,那么就使用我们一开始设置好的默认值作为实际传入的参数:

fun main() {
    test()   //调用函数时,如果对应参数有默认值,可以不填
}
 
fun test(text: String = "我是默认值"){
    println(text)
}

在调用函数时,我们可以手动指定传入的参数对应的是哪一个形式参数:

fun main() {  
    test(b = 3)  // 9, 这里如果只想填写第二个参数b,我们可以直接指定吧实参给到哪一个形参  
    test(3)   // 13, 这种情况就是只填入第一个实参  
}  
  
fun test(a: Int = 6, b: Int = 10) {  
   print((a + b).toString())  
}

对于一些内容比较简单的函数,比如上面仅仅是计算两个参数的和,我们可以直接省略掉花括号,像这样编写:

fun test(a: Int = 6, b: Int = 10): Int = a + b   //函数的结果直接缩减为 = a + b 效果跟之前是一样的
fun test(a: Int = 6, b: Int = 10) = a + b  //返回类型可以自动推断,这里可以吧返回类型省掉

变量 getter and setter

修改和使用变量默认调用它的 getter and setter 方法,通过反编译可以看到。
我们也可以自定方法的逻辑

var a: Int = 5
get() = field *10
set(value){
	println("我被赋值了")
	field = value
}
 
var str: String = "尊嘟假嘟" 
	get() = field + field //使用filed代表当前这个变量(字段)的值,这里返回值拼接的结果
 
fun main(){
	println(a) // 50
	println(str) // 尊嘟假嘟尊嘟假嘟
 
	a = 90; // 我被赋值了 a的值变为90
}

late init

lateinit in Kotlin is a modifier used to declare a non-nullable variable that will be initialized at a later point in the program, rather than at the time of its declaration. This is particularly useful in scenarios where the initialization of a variable depends on external factors, such as dependency injection frameworks, Android lifecycle methods, or when setting up test environments.

lateinit can only be used with non-nullable var properties. It cannot be used with val (read-only) properties, nullable types, or primitive types (like Int, Boolean, Double) as these types have default initial values.

  • Deferred initialization:
    The lateinit keyword informs the compiler that the variable will be initialized before it is accessed. This allows you to avoid providing an initial value at the declaration point, which would otherwise be required for non-nullable properties.
  • Runtime check:
    If a lateinit property is accessed before it has been initialized, a UninitializedPropertyAccessException will be thrown at runtime. This ensures that you are aware of uninitialized properties and helps prevent potential NullPointerException issues.
class MyClass {
    lateinit var myString: String
 
    fun initializeString(value: String) {
        myString = value
    }
 
    fun printString() {
        println(myString) // Accessing myString after initialization
    }
}
 
fun main() {
    val obj = MyClass()
    obj.initializeString("Hello, lateinit!")
    obj.printString()
}

递归函数

fun main() {
    test(5)  //计算0-5的和
}
 
//这个函数实现了计算0-n的和的功能
fun test(n: Int): Int{
    if(n <= 0) return 0  //当n等于0的时候就不再向下,而是直接返回0
    return n + test(n - 1)  //n不为0就返回当前的n加上test参数n-1的和
}

这个函数最终调用起来就像这样:

test(5) = 5 + test(4) = 5 + 4 + test(3) = 5 + 4 + 3 + test(2) =
5 + 4 + 3 + 2 + test(1) = 5 + 4 + 3 + 2 + 1 + 0

lambda

函数变量类型

在kotlin中,函数可以当作变量存储,也就是变量的类型是函数。

// 这个函数(变量),表示一个接受String作为参数返回值为Int的函数类型
var func: (String) -> Int
  • 所有函数类型都有一个括号,并在括号中填写参数类型列表和一个返回类型,比如:(A, B) -> C 表示一个函数类型,该类型表示接受类型A和B的两个参数并返回类型C的值的函数。参数类型列表可为空的,比如() -> A,注意,即使是Unit返回类型也不能省略。
//典型的函数类型 (参数...) -> 类型  小括号中间是一个剪头一样的符号,然后最后是返回类型
var func0: (Int) -> Unit  //这里的 (Int) -> Unit 表示这个变量存储的是一个有一个int参数并且没有返回值的函数
var func1: (Double, Double) -> String   //同理,代表两个Double参数返回String类型的函数

同样的,作为函数的参数也可以像这样表示:

fun test(other: (Int) -> String){ }

函数类型的变量,我们可以将其当做一个普通的函数进行调用:

fun test(other: (Int) -> String){
    println(other(1))  //这里提供的函数接受一个Int参数返回string,那么我们可以像普通函数一样传入参数调用它
    // 相当于: 
    /*
    val result: String = other(1)
    println(result)
    */
}

由于函数可以接受函数作为参数,所以说你看到这样的套娃场景也不奇怪:

var func: (Int) -> ((String) -> Double)

函数变量起别名

不过这样写可能有些时候不太优雅,我们可以为类型起别名来缩短名称:

typealias HelloWorld = (String) -> Double
 
fun main() {
    var func: HelloWorld
}

使用变量类型函数

方法引用

那么,函数类型我们知道如何表示了,如何具体表示一个函数呢?我们前面都是通过fun来声明函数:

// 普通情况
fun test(str: String): Int {
    return 666
}

而现在我们的变量也可以直接表示这个函数:
类似于 java的方法引用

fun main() {
  	//这个变量表示的也是(String) -> Int这种类型的函数
    var func: (String) -> Int = ::test   //使用双冒号来引用一个现成的函数(包括我们后续会学习的成员函数、构造函数等)
}
 
//这个函数正好与上面的变量表示的函数类型一致
fun test(str: String): Int {
    return 666
}

匿名函数

除了引用现成的函数之外,我们也可以使用匿名函数,这是一种没有名称的函数:

fun main() {
    val func: (String) -> Int = fun(str: String): Int {  //这里写了fun关键字后,并没有编写函数名称,这种函数就是匿名函数,因为在这里也不需要什么名字,只需要参数列表函数体
        println("这是传入的内容$str")
        return 666
    }
}

匿名函数除了没名字之外,其他的用法跟函数是一样的。

Lambda表达式

最后,我们来看看今天的重量级嘉宾,不要小看了Kotlin的语法,我们也可以使用Lambda表达式来表示一个函数实例:

fun main() {
    var func: (String) -> Int = {  //一个Lambda表达式只需要直接在花括号中编写函数体即可
        println("这是传入的参数$it")   //默认情况下,如果函数只有一个参数,我们可以使用it代表传入的参数
        666   //跟之前的if表达式一样,默认最后一行为返回值
    }
  	func("HelloWorld!") // result: 这是传入的参数HelloWorld!
}

对于参数有多个的情况,我们也可以这样进行编写:

fun main() {
    val func: (String, String) -> Unit = { a, b ->   //我们需要手动添加两个参数这里的形参名称,不然没法用他两
        println("这是传入的参数$a, 第二个参数$b")   //直接使用上面的形参即可
    }
  	val func2: (String, String) -> Unit = { _, b ->
        println("这是传入的第二个参数$b")   //假如这里不使用第一个参数,也可以使用_下划线来表示不使用
    }
    func("Hello", "World") // 这是传入的参数Hello,第二个参数World
}

我们接着来看,如果我们现在想要调用一个高阶函数,最直接的方式就是下面这样:

fun main() {
    val func: (Int) -> String = { "收到的参数为$it" }
    test(func) // 使用的时候把函数类型的变量传进去
    // 收到的参数为
}
 
fun test(func: (Int) -> String) { // test是一个高阶函数,它的参数类型是函数类型
    println(func(66))
}

或者

fun main() {
    test3 { // 实现 function
	   println(it) // 把接受的值打印
	   "Hello" // 返回值
    }
    // 10
    // Hello
}
 
fun test3(function:(Int)-> String) {
    println(function(10))
}

在Lambda中没有办法直接使用return语句返回结果,而是需要用到之前我们学习流程控制时用到的标签:

fun main() {
    val func: (Int) -> String = test@{ // test@ 打标签
        //比如这里判断到it大于10就提前返回结果
        if(it > 10) return@test "我是提前返回的结果"
        println("我是正常情况")
        "收到的参数为$it"
    }
    test(func)
}
 
fun test(func: (Int) -> String) {
    println(func(66))
}

官方文档的解释

高阶函数是将函数用作参数或返回值的函数。

高阶函数的一个不错的示例是集合的函数式风格的 fold), 它接受一个初始累积值与一个接合函数,并通过将当前累积值与每个集合元素连续接合起来代入累积值来构建返回值:

fun <T, R> Collection<T>.fold(
    initial: R, 
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}

在上述代码中,参数 combine 具有函数类型 (R, T) -> R,因此 fold 接受一个函数作为参数, 该函数接受类型分别为 R 与 T 的两个参数并返回一个 R 类型的值。 在 for 循环内部调用该函数,然后将其返回值赋值给 accumulator。

为了调用 fold,需要传给它一个函数类型的实例作为参数, 而在高阶函数调用处,(下文详述的)lambda 表达 式广泛用于此目的。

fun main() {
    val items = listOf(1, 2, 3, 4, 5)
    
    // Lambdas 表达式是花括号括起来的代码块。
    items.fold(0) {
        // 如果一个 lambda 表达式有参数,前面是参数,后跟“->”
        acc: Int, i: Int -> // variable 'i' is each element in the list
        print("acc = $acc, i = $i, ")
        val result = acc + i
        println("result = $result")
        // lambda 表达式中的最后一个表达式是返回值:
        result
    }
    
    /*
    acc = 0, i = 1, result = 1
    acc = 1, i = 2, result = 3
    acc = 3, i = 3, result = 6
    acc = 6, i = 4, result = 10
    acc = 10, i = 5, result = 15
     */
}

for 循环遍历集合 list 的语法

for(element: Int in items) {
		print("$element")
}
print("\n")
// 12345

Accumulates value starting with initial value and applying operation from left to right to current accumulator value and each element.
Returns the specified initial value if the collection is empty.
Params: operation - function that takes current accumulator value and an element, and calculates the next accumulator value.

// 在新版 kt 标准库中简化了 fold 方法
public inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R {  
    var accumulator = initial  
    for (element in this) accumulator = operation(accumulator, element)  
    return accumulator  
}
fun main() {
    val items = listOf(1, 2, 3, 4, 5)
 
    // lambda 表达式的参数类型是可选的,如果能够推断出来的话:
    val joinedToString = items.fold("Elements:") {
        acc, i -> // 不加类型参数
        acc + " " + i
    }
    println(joinedToString) // Elements: 1 2 3 4 5
    /*
    accumulator = Elements:
    Elements + " " + 1
    Elements + " " + 2
    Elements + " " + 3
    Elements + " " + 4
    Elements + " " + 5
    return accumulator
     */
    // now accumulator is Elements: 1 2 3 4 5
}

it:单个参数的隐式名称

一个 lambda 表达式只有一个参数很常见。

If the compiler can parse the signature without any parameters, the parameter does not need to be declared and -> can be omitted. 该参数会隐式声明为 it:

val ints = listOf(-3, -2, -1, 0, 1, 2, 3)
 
// 传统写法:显式声明参数
val positives1 = ints.filter { n: Int -> n > 0 }
println("positives1 = $positives1")
 
// 简写写法:只有一个参数时,可以用 `it`
val positives2 = ints.filter { it > 0 }
println("positives2 = $positives2")

内联函数

inline function

使用高阶函数会可能会影响运行时的性能:每个函数都是一个对象,而且函数内可以访问一些局部变量,但是这可能会在内存分配(用于函数对象和类)和虚拟调用时造成额外开销。

为了优化性能,开销可以通过内联Lambda表达式来消除。使用inline关键字会影响函数本身和传递给它的lambdas,它能够让方法的调用在编译时,直接替换为方法的执行代码,什么意思呢?比如下面这段代码:

fun main() {
    test()
}
 
//添加inline表示内联函数
inline fun test(){
    println("这是一个内联函数")
  	println("这是一个内联函数")
  	println("这是一个内联函数")
}

由于test函数是内联函数,在编译之后,会原封不动地把代码搬过去:

fun main() {
    println("这是一个内联函数")   //这里是test函数第一行,直接搬过来
  	println("这是一个内联函数")
  	println("这是一个内联函数")
}
fun main() {
	 // 实现了 func表示把传入的数据(it) 打印
    test { println("打印:$it") } 
	 // 执行流程:
 
    // 这是一个内联函数
    // 打印:HelloWorld
}
 
//添加inline表示内联函数
inline fun test(func: (String) -> Unit){ // 方法参数:接受一个函数类型
    println("这是一个内联函数")
    func("HelloWorld")
}

由于test函数是内联的高阶函数,在编译之后,不仅会原封不动地把代码搬过去,还会自动将传入的函数参数贴到调用的位置:
高阶函数被调用后,使用内联会得到优化。

fun main() {
    println("这是一个内联函数")   //这里是test函数第一行
  	val it = "HelloWorld"  //这里是函数内传入的参数
    println("打印:$it")  //第二行是调用传入的函数,自动贴过来
}

再看一个例子:

inline fun doSomething(block: () -> Unit) { 
// `block` 是一个参数名,表示要传进来的这个参数叫什么名字
//() -> Unit 是函数类型:不接收任何参数,不返回任何值
    println("Before")
    block()
    println("After")
}
 
fun main() {
    doSomething {
        println("Hello")
    }
}

编译后大致等价于:

fun main() {
    println("Before")
    println("Hello")
    println("After")
}

注意!!!!!!!:在内联函数中,函数变量不能赋给 另一个变量。除非加入 noinline 关键字

fun main() {
	 // 实现了 func表示把传入的数据(it) 打印
    test { println("打印:$it") } 
	 // 执行流程:
 
    // 这是一个内联函数
    // 打印:HelloWorld
}
 
//添加inline表示内联函数
inline fun test(func: (String) -> Unit){ // 方法参数:接受一个函数类型
		val variable1 = func // 在内联函数中,函数变量不能赋给 另一个变量!!!!
    println("这是一个内联函数")
    func("HelloWorld")
}

默认情况下,所有函数参数都会被内联。但你可以用 noinline 排除部分参数:

inline fun test(block1: () -> Unit, noinline block2: () -> Unit) {
    block1()  // 被内联
    block2()  // 作为函数对象
}

什么是 noinlien? noinline 主要用于函数参数需要作为对象传递,以及不能内联的上下文(如存储到变量)。

inline fun doOps(
    inlineBlock: () -> Unit,
    noinline logging: () -> Unit  // 这里阻止了 logging 被内联
) {
    inlineBlock()   // 内联
    val func = logging  // 如果不加 noinline 会编译错误,因为不能传递被内联的 lambda
    func() // 调用函数变量 func()
}

由于内联,导致代码被直接搬运,所以Lambda中的return语句可以不带标签,这种情况会导致直接返回:

fun main() {
    test { return }  //内联高阶函数的Lambda参数可以直接写return不指定标签
    println("调用上面方法之后")
}
 
inline fun test(func: (String) -> Unit){
    func("HelloWorld")
    println("调用内联函数之后")
}

上述代码的运行结果就是,直接结束,两句println都不会打印,这种情况被称为非局部返回。

类

在 Kotlin 中的一个类有一个主构造函数并可能有一个或多个次构造函数。主构造函数在类头中声明,它跟在类名与可选的类型参数后。

class Person constructor(firstName: String) { /*……*/ }

如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字。

class Person(firstName: String) { /*……*/ }

The primary constructor initializes a class instance and its properties in the class header. The class header can’t contain any runnable code. If you want to run some code during object creation, use initializer blocks inside the class body. Initializer blocks are declared with the init keyword followed by curly braces. Write any code that you want to run within the curly braces.
在实例初始化期间,初始化块按照它们出现在类体中的顺序执行,与属性初始化器交织在一起:

//sampleStart
class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)
 
    //注意,多个初始化操作时,从上往下按顺序执行
    init {
        println("First initializer block that prints $name")
    }
 
    val secondProperty = "Second property: ${name.length}".also(::println)
 
    init {
        println("Second initializer block that prints ${name.length}")
    }
}
//sampleEnd
 
fun main() {
    InitOrderDemo("hello")
}

First property: hello
First initializer block that prints hello
Second property: 5
Second initializer block that prints 5

Kotlin没有写任何构造函数,Kotlin 会默认生成一个无参的主构造函数:

class Person {
    val name = "Default"
}

编译器生成的大致效果:

public class Person {
    private final String name = "Default";
    public Person() { } // ← 编译器帮你生成的无参构造函数
}

创建Person对象

val p = Person() // 可以直接 new,不传参

次要构造函数实际上需要先执行主构造函数,而在执行主构造函数时,会优先将之前我们讲解的初始化代码块执行,比如下面的代码:

class Student(var name: String, var age: Int) {
 
    init {
        println("我是初始化代码块")
    }
 
    constructor(name: String) : this(name, 18) { // 先执行次构造函数,次构造函数再调用主构造函数
        println("我是次要构造函数")
    }
    
	 fun main() {
		val student = Student("smith")
		// 我是初始化代码块
		// println("我是次要构造函数")
	 }
}

运算符重载函数

在Kotlin中,万物皆为对象

var a: Int
var b: Int
println(a + b) // Int类型的对象可以相加

但是如果是自定义的对象可以吗?不行。

val a = Student("小米", 18)
val b = Student("华为", 19)
val c = a + b // 报错

需要在类中使用operator关键字,构造运算符重载函数

class Student(var name: String, var age: Int) {
    //注意,二元运算符必须带一个形参,表示右侧的操作数,返回值为计算出来的结果
  	//形参和结果可以是任意类型,我们还可以提供多次编写同名的运算符重载函数来适配不同的类型
    operator fun plus(another: Student): Student{
        //比如这里我们希望两个学生对象相加,得到的结果为名字相加,年龄相加的一个新学生
        return Student(this.name + another.name,
         this.age + another.age) // 返回新对象
    }
}
val a = Student("小米", 18)
val b = Student("华为", 19)
val c = a + b // 相当于  val c = a.plus(b)

这样,我们就成功重载了加法运算符,可以直接上手使用:

fun main() {
    val a = Student("小米", 18)
    val b = Student("华为", 19)
    val c = a + b
    println("运算之后得到的新学生,名称:${c.name},年龄:${c.age}")
    // 名称:小米华为
    // 年龄:37
}

也可以试试看重载一些一元运算符,比如取反运算符:

class Student(var name: String, var age: Int) {
    //比如取反操作就是把当前学生的名字反过来
    operator fun not() : Student {
        this.name = this.name.reversed()
        //这里可以直接在当前对象上进行操作,然后返回当前对象
        return this // 返回当前对象
    }
}
fun main() {
  	//直接在这里使用!运算符
    val a = !Student("小米", 18)
    println("运算之后得到的新学生,名称:${a.name},年龄:${a.age}")
    // 米小
    // 18
}

最后,我们列出常见的一些运算符对应的函数名称,首先是一元运算符:

符号对应的函数名称
+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()
a—a.dec()+见下文
a++a.inc()+见下文
其中inc()和dec()函数比较特殊,它们必须返回一个值,该值将分配给使用++或--操作的变量,而不是改变执行inc或dec操作的对象,意思就是执行后应该得到一个新生成的对象,然后变量的值直接引用到这个新的对象,因为Int类型就是这样的,比如a++的操作步骤如下:
  • 将a的初始值存储到临时存储a0。
  • 将a0.inc()的结果分配给a。
  • 返回a0作为表达式的结果。

x++

编译成:

val tmp = x.inc()  // 调用你写的 operator fun inc()
x = tmp

同样的,++a的操作步骤如下:

  • 将a.inc()的结果分配给a。
  • 作为表达式的结果返回a的新值。

用 Student对象来举一个例子

/**
@ Author: ferry
@ Date 16/08/2025 2:27 am
 */
class Student(var name:String, var age:Int) {
	operator fun inc(): Student {
		return Student(this.name, age + 1)
	}
}
 
fun main(){
	var student1 = Student("John", 18)
	student1++
	println(student1.name + ": " + student1.age) // John: 19
}
class Student(var name:String, var age:Int) {
	operator fun inc(): Student {
		return Student(this.name, age ++) // 先返回新的Student对象,然后在之前的对象中自增1
	}
}
 
fun main(){
	var student1 = Student("John", 18)
	student1++
	println(student1.name + ": " + student1.age) // John: 18
}
class Student(var name:String, var age:Int) {
	operator fun inc(): Student {
		return Student(this.name, age ++) // 先在之前的对象中自增1,然后返回新的Student对象
	}
}
 
fun main(){
	var student1 = Student("John", 18)
	student1++
	println(student1.name + ": " + student1.age) // John: 19
}

中缀函数

实际上中缀函数在我们之前很多时候都有出现,比如位运算:

var num = 3
println(num shl 1) // “左移” 运算符(Shift Left)

二进制: 00000011 = 3(真值)
左移 1 位: 00000110 = 6 (真值)

/**
* Shifts this value left by the [bitCount] number of bits.
*
* Note that only the five lowest-order bits of the [bitCount] are used as the shift distance.
* The shift distance actually used is therefore always in the range `0..31`.
*/
@kotlin.internal.IntrinsicConstEvaluation
public infix fun shl(bitCount: Int): Int

用infix关键字标记的函数被称为中缀函数,在使用时,可以省略调用的点和括号进行调用,Infix函数必须满足以下要求:

  • 必须是成员函数。
  • 只能有一个参数。
  • 参数不能有默认值。

我们可以像下面这样编写:

class Student(var name: String, var age: Int) {
  	//这个中缀函数实现了将给定字符串与当前对象的名字拼接并返回
    infix fun customAppend(string: String) : String{
        return name + string
    }
}

我们在使用时,也非常方便,真的就像在使用一个运算符一样:

fun main() {
    val student = Student("小明", 18)
    println(student customAppend "我爱你")
    // 等价于
    println(student.customAppend("我爱你"))
    // 小明我爱你
}

如果需在类中使用中缀函数,必须明确函数的调用方(接收器)比如:

class MyStringCollection {
    infix fun add(str: String) { /*...*/ }
 
    fun build() {
        this add "abc"   // 正确
        add("abc")       // 正确
        //add "abc"      //  错误: 没有指定调用方或无法隐式表达
    }
}

空值和空类型

所有的变量除了引用一个具体的值之外,还有一种特殊的值可以使用,那就是null,它代表空值,也就是不引用任何对象。

在其他语言中,比如Java中null是一个非常常见的值,因为在某些情况下,引用类型的变量默认值就是null,这就经常会导致程序中出现一些空指针导致的异常,在Kotlin中,对空值处理是非常严格的,正常情况下,我们的变量是不能直接赋值为null的,否则会报错,无法编译通过:

fun main(){
	var str: String = null // 报错,编译不通过
}

这是因为所有的类型默认都是非空类型,非空类型的变量是不允许被赋值为null的,这直接在编译阶段就避免了其他语言中经常存在的空指针问题。

那么,如果我们希望某个变量在初始情况下使用null而不去引用某一个具体对象,该怎么做呢,此时我们需要将变量的类型修改为可空类型,只需在类型名称的后面添加一个?即可:

fun main() {
    var str: String? = null
}

既然现在是可空类型,那么很多问题就会出现了,比如当一个变量为null时,此时如果使用类中的一些成员方法或是获取成员属性时,会出现一些问题:

fun main() {
    var str: String? = null
    println(str.length) // 报错,str可以会是 null,不能引用其属性
}

此时,为了安全,我们就需要对变量进行判断,看看其是否为null然后才能去做一些正常情况下该做的事情:

fun main() {
    var str: String? = null
  	//这里直接通过if语句判断str变量是否为null,如果不是才执行
    if (str != null) { // java的处理方式
        println(str.length)  //现在就可以编译通过了
    }
}

可以看到,我们只要能确保某个空类型变量的值不为空,那么就可以正常执行操作。当然,实际上在这个if内部,因为已经判断不为null了,所以str被智能类型转换为非空类型,这也是Kotlin语言非常人性化的地方。
不过在有些情况下,我们可能已经非常清楚,这里的str一定不为null,即使它是一个可空类型变量,我们可以像这样做,来告诉编译器,我们这里一定是安全的,只管执行就好:

fun main() {
    var str: String? = null
  	//使用非空断言操作符!!.来明确不会出现null问题
    println(str!!.length) // 强制执行, NullPointerException
}

虽然使用非空断言操作符能够进行强制操作,但是这样实际上并不安全,它同样存在安全问题,也许我们有没考虑到的情况会导致这里为null呢,也说不定吧?对于一些我们拿不定具体会不会出现null的情况,有没有更好的解决办法呢?

Kotlin为我们提供了一种更安全的空类型操作,要安全地访问可能包含null值的对象的属性,请使用安全调用运算符?.,如果对象的属性为null则安全调用运算符返回null,像下面这样:

fun main() {
    var str: String? = null
    println(str?.length) // 输出 -> null
}

这里的调用结果存在两种情况:

  • 如果str为null,那么这里得到的结果就是null,并且不会正常执行后面的操作
  • 如果str不为null,那就正常返回这里本应该得到的结果

不过在有些时候,可能我们希望如果变量为null,在使用安全调用运算符时,返回一个我们自定义的结果,而不是null,这时该怎么做呢?我们可以使用Elvis (?:) 运算符:

fun main() {
    val str: String? = null
    
  	//Elvis运算符 ?: 左侧为空值检测目标,右侧为检测到null时返回的结果
    val len: Int = str?.length ?: 0
    // in this case: len = 0, because the String 'str' is null
}

Destructuring declarations

解构声明
Sometimes it is convenient to destructure an object into a number of variables, for example:

val (name, age) = person

This syntax is called a destructuring declaration. A destructuring declaration creates multiple variables at once. You have declared two new variables: name and age, and can use them independently:

println(name)
println(age)

A destructuring declaration is compiled down to the following code:

val name = person.component1()
val age = person.component2()

其中的 component1() 和 component2() 函数是在 Kotlin 中广泛使用的约定原则的另一个示例。 (参见像 + 和 *、for-循环作为示例)。 任何表达式都可以出现在解构声明的右侧,只要可以对它调用所需数量的 component 函数即可。 当然,可以有 component3() 和 component4() 等等。

class Student(var name: String, var age: Int) {
    operator fun component1() = name   //使用component1表示解构出来的第一个参数
    operator fun component2() = age    //使用component2表示解构出来的第二个参数
  	operator fun component3...  //以此类推
}
val (a, b) = student   //解构出来的参数按顺序就是componentN的结果了

如果我们只想要使用第二个参数,而第一个参数不需要,可以直接使用_来忽略掉:

val (_, b) = student
println("年龄: $b")

解构同样可以用在Lambda表达式中:

class Person(var name: String, var age: Int) {
    operator fun component1() = name
    operator fun component2() = age
}
 
fun main() {
    val person = Person("Smith", 18)
    val (name, age) = person // Destructuring declarations: 基本使用
    println("$name, $age") // Smith, 18
 
    // used in lambda
    val printStudentName: (Person) -> Unit = { (name, _ ) ->
        println("Student's name is: $name")
    }
    // invoke this method we just wrote
    printStudentName(person)
    // Student's name is: Smith
 
 
   // 两个参数 一个接受 Person类,一个是 Int
    val increaseAgeDestructured: (Person, Int) -> Unit = { ( _ ,age), increment  ->
        println("increased person's age : ${increment}")
        val newAge = age + increment
        println(newAge)
    }
 
    increaseAgeDestructured(person,3)
    // increased person's age : 3
    // 21
}

包和导入

package com.test // in the package `com.test`
 
var a = 20
 
fun message() {
    println("我是测试方法")
}
 
class User

现在kt文件存放在了一个明确的包中,如果我们要在这个包以外的其他地方使用就需要导入

import com.test.User   //使用import关键字进行导入,导入时需要输入 包名.类型/顶级函数名称 来完成
import com.test.message
import com.test.a
 
fun main() {
    val user = User()
    message()
}

这样,我们在导入之后就可以正常使用了,当然,如果一个包中定义的内容太多,我们需要大量使用,也可以使用一次性导入全部内容:

import com.test.*   //导入此包下的全部内容

名称冲突的情况:

import com.test.message
 
fun main() {
    message()   //这里调用的,到底是导入的message函数,还是当前kt文件定义的函数呢?
}
 
fun message(){
    println("Goodbye World!")
}

结果显而易见,这里会优先使用导入的函数,而不是在当前文件中定义的同名函数。那么该如何去解决这种冲突的情况呢?我们可以使用as关键字来为导入的内容起个新的名字:

import com.test.message as outer //将导入的message函数名字改为outer
 
fun main() {
    message()   //此时这里调用的就是下面的message函数了
}
 
fun message(){
    println("Goodbye World!")
}

访问权限控制

在类、对象、接口、构造函数和函数,以及属性上,可以为其添加 可见性修饰符 来控制其可见性,在Kotlin中有四个可见性修饰符,它们分别是:private、protected、internal和public,默认可见性是public,在使用顶级声明时,不同可见性的访问权限如下:

  • 如果不使用可见性修饰符,则默认使用public,这意味着这里声明的内容将在任何地方可访问。
  • 如果使用private修饰符,那么声明的内容只能在当前文件中可访问。
  • 如果使用internal修饰符,它将在同一模块(就是总的项目名称下的文件,不包括子模块)中可见(当前的项目中可以随意访问,与public没大差别,但是如果别人引用我们的项目,那么无法使用)
  • 顶级声明不支持使用protected修饰符。
class Student(private var name: String, //name属性无法被外部访问,因为是私有的
              internal var age: Int) {  //age可以被外部访问,但是无法再其他项目中访问到
    private constructor() : this("", 10)  //次构造函数:这个无参构造无法被外部访问,因为是私有的
}

在同一个模块中,你可以这样创建对象:

fun main() {
    val student = Student("Alice", 20)
    println(student.age) // 可以访问,因为 age 是 internal
    // println(student.name) // 错误,name 是 private
}

私有构造函数只能在类内部用,比如定义一个静态工厂方法:

class Student(private var name: String, internal var age: Int) {
    private constructor() : this("", 10)
 
    companion object {
        fun createDefaultStudent(): Student {
            return Student() // 调用私有构造函数
        }
    }
}
 
fun main() {
    val defaultStudent = Student.createDefaultStudent()
    println(defaultStudent.age) // 10
}

继承

在Kotlin中,我们可以使用继承操作来实现这样的结构,默认情况下,Kotlin类是“终态”的(不能被任何类继承)要使类可继承,请用open关键字标记需要被继承的类:

open class Student {  //被继承的类我们称为父类
  	val card = "学生证"
    fun hello() = println("我会打招呼")
}
open class Student {
    init {
         println("父类初始化")
    }
    val card = "学生证"
    fun hello() = println("我会打招呼")
}
 
class ArtStudent() : Student() {
    init {
        println("子类初始化")
    }
    fun draw() = println("我会画画")
}
 
fun main() {
    val artStudent = ArtStudent()
    artStudent.draw()
    artStudent.hello()
    println(artStudent.card)
}

执行流程

父类初始化
子类初始化
我会画画
我会打招呼
学生证

open class Person(var name: String, var age: Int) {
    init {
        println("Person init: name=$name, age=$age")
    }
    fun sayHello() = println("Hello...")
}
 
// extends Person
class Worker(name: String, age: Int) : Person(name, 17) {
    // 实例化Worker 时,自动调用父类 Person()的构造器,并把Worker的name变量传进入,age写死为17
    fun work() = println("work...")
}
 
fun main() {
    val worker = Worker("jack", 16)
    println(worker)
}

Person init: name=jack, age=17
com.ferry.obj.Worker@378fd1ac

当子类只存在辅助构造函数时,需要使用super关键字来匹配父类的构造函数:

open class Student {
    constructor(str: String)
    constructor(str: String, age: Int)
    fun hello() = println("我会打招呼")
}
 
//子类不写主构造函数时,可以直接在冒号后面添加父类类名
class ArtStudent : Student {
    constructor(str: String) : super(str)   //使用super来调用父类构造函数,super表示父类(超类)
    constructor(str: String, age: Int) : super(str, age)
    fun draw() = println("我会画画")
}

也可以去匹配子类中其他构造函数:

class ArtStudent : Student {
    constructor(str: String) : this(str, 18)   //也可以调用子类其他构造函数,但是其他构造函数依然要间接或直接调用父类构造函数
    constructor(str: String, age: Int) : super(str, age)
    fun draw() = println("我会画画")
}

如果子类既有主构造函数,也有辅助构造函数,那么其他辅助构造函数只能直接或间接调用主构造函数:

open class Student() {
    constructor(str: String) : this() // 调用Student主构造函数
    constructor(str: String, age: Int) : this()
}
 
class ArtStudent() : Student() {
    constructor(str: String) : this()  //正确,必须直接或间接调用主构造函数
    //constructor(str: String, age: Int) : super(str, age)   //报错,不能绕过主构造函数去匹配父类构造函数
    // 也就是次构造函数不能直接调用父类的构造函数
}

override

open class Student {
  	//注意,跟类一样,函数必须添加open关键字才能被子类覆盖
    open fun hello() = println("我会打招呼")
}
 
class ArtStudent : Student() {
    fun draw() = println("我会画画")
  	//在子类中编写一个同名函数,并添加override关键字,我们就可以在子类中进行覆盖了,然后编写自己的实现
    override fun hello() = println("おはよー")
}

变量也是可以进行覆盖的:

open class Student {
    open val test: String = "测试"
    fun hello() = println("我会打招呼: $test")  //这里拿到的test就会变成被覆盖掉的
}
 
class ArtStudent : Student() {
  	//对父类的变量进行覆盖,类型必须一样
    override val test: String = "干嘛"
    fun draw() = println("我会画画")
}

有些时候为了方便,比如在父类中的属性,我们可以直接在子类的主构造函数中直接覆盖:

open class Student {
    open val name: String  = "大明"
    fun hello() = println("我会打招呼,我叫: $name")
}
 
//在主构造函数中覆盖,也是可以的,这样会将构造时传入的值进行覆盖
class ArtStudent(override val name: String) : Student() {
    fun draw() = println("我会画画")
}
fun main() {
    val student = ArtStudent("小红")
    student.hello() // 我会打招呼,我叫:小红
}

不过,由于存在我们之前讲解的的初始化顺序,下面的这种情况需要特别注意:

open class Student {
    open val name: String = "小明"
    init { println("我的名字是: ${name.length}") }  //这里拿到的name实际上是还未初始化的子类name
}
 
class ArtStudent : Student() {
    override val name = "大明"
}
 
fun main() {
    ArtStudent()
    // NullPointerException 初始化ArtStudent()先调用Student()发现为null
    // 子类的属性要等父类构造执行完才初始化
}

创建 ArtStudent() 时,执行顺序是这样的:

  1. 先执行 父类构造函数(包括 init {} 块里的代码)。
  2. 然后才初始化子类的属性。

多态

跟java一样

open class Student {
    open fun hello() = println("大家好")
}
 
class ArtStudent : Student() {
    override fun hello() = println("我姓🐴我叫🐴牛逼")
}
fun main() {
    val student: ArtStudent = ArtStudent() // 一般情况
    val student: Student = ArtStudent() // 做其父类来进行使用
}

顶层Any类

/**
 * Kotlin类继承结构中的根类. 所有Kotlin中的类都会直接或间接将Any作为父类
 */
public open class Any {
    /**
     * 判断某个对象是否"等于"当前对象,这里同样是对运算符"=="的重载,而具体判断两个对象相等的操作需要由子类来定义
     * 在一些特定情况下,子类在重写此函数时应该保证以下要求:
     * * 可反身: 对于任意非空值 `x`, 表达式 `x.equals(x)` 应该返回true
     * * 可交换: 对于任意非空值 `x` 和 `y`, `x.equals(y)` 当且仅当 `y.equals(x)` 返回true时返回true
     * * 可传递: 对于任意非空值 `x`, `y`, 和 `z`, 如果 `x.equals(y)` 和 `y.equals(z)` 都返回true, 那么 `x.equals(z)` 也应该返回真
     * * 一致性: 对于任意非空值 `x` 和 `y`, 在多次调用 `x.equals(y)` 函数时,只要不修改在对象的“equals”比较中使用的信息,那么应当始终返回同样的结果
     * * 永不等于空: 对于任意非空值 `x`, `x.equals(null)` 应该始终返回false
     */
    public open operator fun equals(other: Any?): Boolean
 
    /**
     * 返回当前对象的哈希值,它具有以下约束:
     * 
     * * 对同一对象多次调用该函数时,只要不修改对象上的equals比较中使用的信息,那么此函数就必须始终返回相同的整数 
     * * 如果两个对象通过`equals`函数判断为true,那么这两个对象的哈希值也应该相同
     */
    public open fun hashCode(): Int
 
    /**
     * 将此对象转换为一个字符串,具体转换为什么样子的字符串由子类自己决定
     */
    public open fun toString(): String
}

equals函数,它实际上是对==这个运算符的重载,我们之前在使用一些基本类型的时候,就经常使用==来判断这些类型是否相同,比如Int类型的数据:

fun main() {
    val a = 10
    val b = 20
    println(a == b)
    println(a.equals(b))  //跟上面的写法完全一样
}

Kotlin中两种相等的判断方式:

  • 结果上 相等 (== 等价于 equals())
  • 引用上 相等 (=== 判断两个变量是否都是引用的同一个对象)
a == b

实际上会被翻译为:

a?.equals(b) ?: (b === null)  //a如果为null那就直接判断b是不是也为null,否则直接调用a的equals函数并让b作为参数
// a 存在就比较,调用 equals()
// a 不存在(null)就不调用,把 b 与 null 应用上相等判断

?:(Elvis 运算符):

  • 语法是 expr1 ?: expr2。
  • 如果 expr1 != null,结果就是 expr1。
  • 如果 expr1 == null,结果就是 expr2。

不等于判断呢?实际上是一样的:

fun main() {
    val a = "10"
    val b = "20"
    println(a != b)
    println(!a.equals(b))  //等价于上面的写法
}

我们也可以为我们自己编写的类型重写equals函数,比如我们希望Student类型当名字和年龄相等时,就可以使用==来判断为true,我们可以像这样编写:

class Student(val name: String, val age: Int) {
    override fun equals(other: Any?): Boolean {
        if(this === other) return true  //如果引用的是同一个对象,肯定是true不多逼逼
        if(other !is Student) return false //如果要判断的对象根本不是Student类型的,那也不用继续了
        if(name != other.name) return false  //判断名字是否相同
        if(age != other.age) return false  //判断年龄是否相同
        return true   //都没问题,那就是相等了
    }
}
fun main() {
    val a = Student("小明", 18)
    val b = Student("小红", 17)
    val c = Student("小明", 18)
    println(a == a)    //返回true因为就是自己
    println(a == b)    //返回false因为名字和年龄不一样
    println(a == c)    //返回true因为名字和年龄完全一样
}
 

默认情况下,如果我们不重写类的equals函数,那么会直接对等号两边的变量进行引用判断===判断是否为同一个对象。

抽象类

有些情况下,我们设计的类可能仅仅是作为给其他类继承使用的类,而其本身并不需要创建任何实例对象,比如:

// 模拟抽象类
open class Student protected constructor() {  //无法构造这个父类,要求使用子类
    open fun hello() = println("Hello World!")
}
class ArtStudent: Student() {
    override fun hello() = println("原神")  //两个子类都对hello进行了实现,采用各自的方式
}
class SportStudent: Student() {
    override fun hello() = println("启动")
}

可以看到,在上面这个例子中,Student类的hello函数在子类中都会被重写,所以说除非在子类中调用父类的默认实现,否则一般情况下,父类中定义的函数永远都不会被调用。

就像我们说一个学生会怎么考试一样,实际上学生怎么考试是一个抽象的概念,但是由于学生的种类繁多,美术生怎么考试和体育生怎么考试,才是具体的一个实现。所以说,我们可以将学生类进行进一步的抽象,让某些函数或字段完全由子类来实现,父类中不需要提供实现。我们可以使用abstract关键字来将一个类声明为抽象类:

//使用abstract表示这个是一个抽象类
abstract class Student {
    abstract val type: String  //抽象类中可以存在抽象成员属性
    abstract fun hello()   //抽象类中可以存在抽象函数
  	//注意抽象的属性不能为private,不然子类就没法重写了
}

当一个子类继承自抽象类时,必须要重写抽象类中定义的抽象属性和抽象函数:

class ArtStudent: Student() {
    override val type: String = "美术生"
    override fun hello() = println("原神,启动!")
}

当然,抽象类不仅可以具有抽象的属性,同时也具有普通类的性质,同样可以定义非抽象的属性或函数:

abstract class Student {
    abstract val type: String
    abstract fun hello()
    fun test() = println("不会有人玩到大三了才开始学Java吧")  //定义非抽像属性或函数,在子类中不强制要求重写
}

接口

interface A {
    val x: String  //接口中所有属性默认都是abstract的(可省略关键字)
    fun sleep()   //接口中所有函数默认都是abstract的(可省略关键字)
}
interface B {
    fun game()
}
class Student: A, B {   //接口的实现与类的继承一样,直接写到后面,多个接口用逗号隔开
    override val x: String = "测试"   //跟抽象类一样,接口中的内容是必须要实现的
    override fun sleep() = println("管他什么早八不早八的,睡舒服再说")
    override fun game() = println("读大学就该玩游戏玩到爽")
}

可以看到,接口相比于抽象类来说,更加的纯粹,它不像类那样可以具有什么确切的属性,一切内容都是抽象的,只能由子类来实现。

只不过,在接口中声明的属性可以是抽象的,也可以为Getter提供默认实现。在接口中声明的属性无法使用field后背字段,因此在接口中声明的Setter无法使用field进行赋值:

interface A {
    val x: String
        get() = "666"  
        // 只能重写getter,不能直接赋值,
        // 因为默认情况下getter是返回的field的值,但是接口里不让用
}

类的扩展

Kotlin提供了扩展类或接口的操作,而无需通过类继承或使用装饰器等设计模式,来为某个类添加一些额外的函数或是属性,我们只需要通过一个被称为扩展的特殊声明来完成。

例如,您可以从无法修改的第三方库中为类或接口编写新函数,这些函数可以像类中其他函数那样进行调用,就像它们是类中的函数一样,这种机制被称为扩展函数。还有扩展属性,允许您为现有类定义新属性。

比如我们想为String类型添加一个自定义的操作:

//为官方的String类添加一个新的test函数,使其返回自定义内容
fun String.test() = "666"
 
fun main() {
    val text = "Hello World"
    println(text.test())  //就好像String类中真的有这个函数一样
    // 666
}

注意,类的扩展是静态的,实际上并不会修改它们原本的类,也不会将新成员插入到类中,仅仅是将我们定义的功能变得可调用,使用起来就像真的有一样。同时,在编译时也会明确具体调用的扩展函数:

open class Shape {}  
class Rectangle : Shape() {}  
  
fun Shape.getName() = "Shape"  
fun Rectangle.getName() = "Rectangle"  //虽然这里同时扩展了父类和子类的getName函数
  
fun printClassName(shape: Shape){  //但由于这里指定的类型是Shape,因此编译时也只会使用Shape扩展的getName函数
    println(shape.getName())  
}  
  
fun main() {  
    printClassName(Rectangle())  // 实际调用的还是 Shape.getName()
    // 如果把 Shape.getName() 注释就报错
} 

扩展属性

class Student{}
 
// 直接定义报错
var Student.gender:String ="not specified"

可以看到直接扩展属性是不允许的,前面我们说过,扩展并不是真的往类中添加属性,因此,扩展属性本质上也不会真的插入一个成员字段到类的定义中,这就导致并没有变量去存储我们的数据,我们只能明确定义一个getter和setter来创建扩展属性,才能让它使用起来真的像是类的属性一样:

class Student // 这样写感觉没什么用
 
var Student.gender: String
    get() = "not specified"
    set(value) {}
 
fun main() {
    val student = Student()
    println(student.gender) // not specified
}

我们也可以在一个类中定义其他类的扩展,并且在定义时可以直接使用其他类提供的属性:

class A {
    fun hello() = "Hello World"
}
 
class B {
    fun A.test() {
        hello()   //直接在类A的扩展函数中调用A中定义的函数
    }
}

像这种扩展,由于是在类中定义,因此也仅限于类内部使用,比如:

class A {
    fun hello() = "Hello World"
}
 
class B (private val a: A){
    private fun A.test() = hello() + "!!!"
    fun world() = println(a.test())   //只能在类中通过A的实例使用扩展函数,只能在类B里面使用 扩展函数 test()
}
 
fun main() = B(A()).world() // Hello World!!!

在名称发生冲突时,需要特别处理:

class A {
    fun hello() = "Hello World"
}
 
class B (private val a: A){
    private fun A.test() {
        hello()   //直接使用优先匹配被扩展类中的方法
        this.hello()   //扩展函数中的this依然指的是被扩展的类对象
        this@B.hello()   //这里调用的才是下面的
    }
 
    fun hello() = "Bye World"
}

局部扩展也是可以的,我们可以在某个函数里面编写扩展,但作用域仅限于当前函数:

fun main() {
    fun String.test(): Unit {
        println("String test extension method!")
    }
    "".test() // String test extension method!
}

如果我们将一个扩展函数作为参数给到一个函数类型变量,那么同样需要再具体操作之前增加类型名称才可以:

fun main() {
    //因为是对String类型的扩展函数,需要String.前缀
    val customLength: String.() -> Int = {
        this.length   //跟上面一样,扩展函数中的this依然指的是被扩展的类对象
    }
 
	 // 4
    println("test".customLength())  //可以直接对符合类型的对象使用这个函数
    // 5
    println(customLength("Hello"))  //如果是直接调用,那么必须要传入对应类型的对象作为首个参数,此时this就指向我们传入的参数
}

总结一下,扩展属性更像是针对于原本类编写的外部工具函数,而绝不是对原有类的修改。

特殊类型介绍

数据类型

//在class前面添加data关键字表示为一个数据类
data class User(val name: String, val age: Int)
 
fun main() {
    val user1 = User("小明", 18)
    val user2 = User("小明", 18)
    println(user1)   //打印出来就是格式化的字符串 User(name=小明, age=18)
    println(user1 == user2)   //true,因为自动重写了equals函数
    val (name, age) = user1   //自动添加componentN函数,因此支持解构操作
    println("名称: $name, 年龄: $age")
 

数据类声明后,编译器会根据主构造函数中声明的所有属性自动为其生成以下函数:

  • .equals()/.hashCode()
  • .toString()生成的字符串格式类似于"User(name=John, age=42)"
  • .componentN()与按声明顺序自动生成用于解构的函数
  • .copy()用于对对象进行拷贝

当然,为了确保生成代码的一致性和有效性,数据类必须满足以下要求:

  • 主构造函数中至少有一个参数。
  • 主构造函数中的参数必须标记为val或var。
  • 数据类不能是抽象的、开放的、密封的或内部的。

此外,数据类的成员属性生成遵循有关成员继承的以下规则:
如果数据类主体中.equals() .hashCode()或.toString()等函数存在显式(手动)实现,或者在父类中有final实现,则不会自动生成这些函数,并使用现有的实现。

open class A {
    open fun f() {}
}
 
open class B : A() {
    override fun f() {}         // 默认就是 final,不能再 override
    // open override fun f() {} // 如果你想允许 C 再 override,需要写 open
}
 
class C : B() {
    override fun f() {} // ❌ 报错,因为 B 的 f() 已经是 final 了
}
data class User(val name: String, val age: Int) {
    val id: Int = 888
 
    final override fun equals(other: Any?): Boolean { // final 可以省略跟上面一样
        return true
    }
 
    //如果已经存在toString的情况下,不会自动生成
    override fun toString(): String = "我是自定义的toString"
}
 
fun main() {
    val user1 = User("小明", 18)
    println(user1)   //结果: 我是自定义的toString
    val user2 = User("Jack", 19)
    println(user2 == user1) // true,自动义 equals()永远返回 true
}

如果超类型具有open .componentN()函数并返回兼容类型,则为数据类生成相应的函数,并覆盖超类型的函数。如果由于一些关键字导致无法重父类对应的函数会导致直接报错。

// 正常用法
abstract class AbstractUser {
  	//此函数必须是open的,否则无法被数据类继承
    open operator fun component1() = "卢本伟牛逼"
}
 
class NormalUser : AbstractUser() // 定义子类实现 父类
 
// 子类覆盖 component1
class RealUser(val name: String) : AbstractUser() {
    override fun component1() = name
}
 
fun main() {
    val user = NormalUser()
    val (v) = user  // 解构
    println(v)      // 输出 "父类默认值"
 
	 val user = RealUser("Tom")
	 val (v) = user
	 println(v)      // 输出 "Tom"
}
abstract class AbstractUser {
  	//此函数必须是open的,否则无法被数据类继承
    open operator fun component1() = "卢本伟牛逼"
}
 
data class User(val name: String, val age: Int): AbstractUser()  //自动覆盖掉父类的component1函数
 
fun main() {
    val user = User("Tom", 20)
    val (n) = user
    println(n)  // 输出 "Tom" ,因为数据类生成了自己的 component1 覆盖了父类的
}
  • 并且不允许为.componentN()和.copy()函数提供显式实现。
    注意,编译器会且只会根据主构造函数中定义的属性生成对应函数,如果有些时候我们不希望某些属性被添加到自动生成的函数中,我们需要手动将其移出主构造函数:
data class Person(val name: String){
    var age: Int = 0 // age属性不会被处理
}
 
fun main() {
    val person1 = Person("John")
    val person2 = Person("John")
    person1.age = 10
    person2.age = 20
 
    println("person1 == person2: ${person1 == person2}")
    // person1 == person2: true
    println("person1 with age ${person1.age}: $person1")
    // person1 with age 10: Person(name=John)
    println("person2 with age ${person2.age}: $person2")
    // person2 with age 20: Person(name=John)
}

数据类自带一个拷贝对象的函数,使用.copy()函数复制对象,允许您更改其一些属性,而其余的保持不变。此函数对上述User类的实现如下:

data class Person(val name: String){
    var age: Int = 0 // age属性不会被处理
}
 
fun main() {
    val person1 = Person("John")
    person1.age = 10
 
    val person3 = person1.copy()
    println(person3 == person1) // true 
    println(person3 === person1) // false
    
    // 在copy函数还可以在拷贝过程中手动指定属性的值:
	 val personCopy = person1.copy(name = "I am a clone")  
    println(personCopy)
	 // Person(name=I am a clone)
}

枚举

//在类前面添加enum表示这是一个枚举类型
enum class LightState {
    GREEN, YELLOW, RED   //直接在枚举类内部写上所有枚举的名称,一般全部用大写字母命名
}
fun main() {
    val state: LightState = LightState.RED  //直接得到红灯
  	println(state.name)   //自带name属性,也就是我们编写的枚举名称,这里是 RED
}

同样的,枚举类也可以具有成员:

//同样可以定义成员变量,但是不能命名为 name,因为name拿来返回枚举名称了
enum class LightState(val color: String) {
    GREEN("绿灯"), YELLOW("黄灯"), RED("红灯");  
    //枚举在定义时也必须填写参数,如果后面还要编写成员函数之类的其他内容,还需在末尾添加分号结束
  
  	fun isGreen() = this == LightState.GREEN  //定义一个函数也是没问题的
}
 
fun main() {
    val state: LightState = LightState.RED 
    println("信号灯的颜色是: ${state.color}")  // 红灯
    println("信号灯是否可以通行: ${state.isGreen()}") // false
}

枚举类型可以用于when表达式进行判断,因为它的状态是有限的:

val state: LightState = LightState.RED
val message: String = when(state) {
    LightState.RED -> "禁止通行"
    LightState.YELLOW -> "减速通行"
    LightState.GREEN -> "正常通行"
}
println(message)   //结果为: 禁止通行

在枚举类中也可以编写抽象函数,抽象函数需要由枚举自行实现:

enum class LightState(val color: String) {
    GREEN("绿灯"){
        override fun test() = println("我是绿灯,表示可以通过")
    }, YELLOW("黄灯") {
        override fun test() = println("我是黄灯,是让你减速,不是让你踩油门加速过去")
    }, RED("红灯") {
        override fun test() = println("我是红灯,禁止通行")
    };
    abstract fun test()   //抽象函数
}
 
fun main() {
    val state: LightState = LightState.RED
    state.test()   //调用函数: 我是红灯,禁止通行
}
public enum LightState {
    GREEN("绿灯") {
        @Override
        public void test() {
            System.out.println("我是绿灯,表示可以通过");
        }
    },
    YELLOW("黄灯") {
        @Override
        public void test() {
            System.out.println("我是黄灯,是让你减速,不是让你踩油门加速过去");
        }
    },
    RED("红灯") {
        @Override
        public void test() {
            System.out.println("我是红灯,禁止通行");
        }
    };
 
    private final String color;
 
    // 枚举构造函数必须是 private
    private LightState(String color) {
        this.color = color;
    }
 
    public String getColor() {
        return color;
    }
 
    // 抽象方法,要求每个枚举常量都实现
    public abstract void test();
 
    public static void main(String[] args) {
        LightState state = LightState.RED;
        state.test();  // 调用方法: 我是红灯,禁止通行
    }
}

如果枚举类实现了某个接口,同样可以像这样去实现:

interface Message {
    fun sendMessage()
}
 
enum class LightState(val color: String): Message{
    GREEN("绿灯"){
        override fun sendMessage() = println("我是绿灯,表示可以通过")
    }, YELLOW("黄灯") {
        override fun sendMessage() = println("我是黄灯,是让你减速,不是让你踩油门加速过去")
    }, RED("红灯") {
        override fun sendMessage() = println("我是红灯,禁止通行")
    };
}
 
fun main() {
    val state = LightState.GREEN
    state.sendMessage() // 我是绿灯,表示可以通过
}
interface Message {
    fun sendMessage()
}
 
enum class Season(private val type: String): Message{  
    SPRING("spring"),SUMMER("summer"),WINTER("winter"), AUTUMN("autumn");  
    override fun sendMessage() = println(type)  
}
fun main() {
    val season = Season.SPRING
    season.sendMessage() // spring
}

枚举类提供的函数

interface Message {  
    fun sendMessage()  
}
 
enum class Season(private val type: String): Message{  
    SPRING("spring"),SUMMER("summer"),WINTER("winter"), AUTUMN("autumn");  
    override fun sendMessage() = println(type)  
}
 
fun main() {
    // 通过 字符串获取枚举类对象
    val winter: Season = Season.valueOf("WINTER")
    winter.sendMessage() // winter
     // 下面这个方法跟 valueOf()差不多
    enumValueOf<Season>("AUTUMN").sendMessage() // autumn
}

匿名类和伴生对象

val obj: Any = object{ //使用object关键字声明一个匿名类并创建其对象,可以直接使用变量接收得到的对象
    val name: String = "I am an anonymous class" // 属性
    override fun toString(): String { // 匿名类默认继承于Any,可以直接重写其toString
        return "custom toString function!"
    }
}
 
fun main() {
    println(obj) // custom toString function!
}
interface Person {
    fun chat()
}
 
fun main() {
    val obj: Person = object : Person {   //直接实现Person接口
        override fun chat() = println("牛逼啊")
    }
    // 牛逼啊
    obj.chat()  //当做Person的实现类使用
}
interface Person
open class Human(val name: String)
 
fun main() {
    val obj: Human = object : Human("小明"), Person {   //继承类时,同样需要调用其构造函数
        override fun toString() = "我叫$name"   //因为是子类,直接使用父类的属性也是没问题的
    }
    println(obj)
}

可以看到,平时我们无法直接实例化的接口或是抽象类,可以通过匿名类的形式得到一个实例化对象。
我们再来看下面这种情况:

interface KRunnable {
    fun invoke()   //此类型是一个接口且只包含一个函数
}
// 根据我们上面学习的用法,如果我们想要使用其匿名类,可以像这样编写:
fun main() {
    val runnable = object : KRunnable {   //使用匿名类的形式编写KRunnable的实现类对象
        override fun invoke() {
            println("我是函数invoke的实现")
        }    
    }
    runnable.invoke()
}

特别的,对于只存在一个抽象函数的接口称为函数式接口或单一抽象方法(SAM)Single Abstract Method 接口,函数式接口可以有N个非抽象成员,但是只能有一个抽象成员。对于函数式接口,可以使用我们前面介绍的Lambda表达式来使代码更简洁:

fun interface KRunnable {  //在接口声明前面添加fun关键字
    fun invoke()
}
 
fun main() {
    val runnable = KRunnable {   //支持使用Lambda替换
        println("我是函数invoke的实现")
    }
    runnable.invoke()
}

另一个例子

fun interface Printer{ 
// fun interface 来声明(表示它就是一个 [[SAM]] 接口)
 
    fun print()
}
 
fun printImpl(printer: Printer){
    printer.print()
}
fun main() {
    printImpl{
        println("我是 print 的实现")
    }
}

有了匿名类,所以有些时候我们通过函数得到的结果,可能并不是某个具体定义的类型,也有可能是直接采用匿名形式创建的匿名类对象:

open class Human(val name: String) // 定义一个Human类
 
fun createHuman() = object: Human("小明") {  //返回的一个匿名类对象
  	val age: Int = 10 // 匿名类定义的属性只能在匿名类内部使用
    override fun toString() = "我叫$name"
}
 
fun main() {
    println(createHuman().name) // 小明
    // println(createHuman().age)  //编译错误,因为返回的类型是Human,由于其匿名特性,只能当做Human使用
}

单例类

object关键字除了用于声明匿名类型,也可以用于声明单例类。单例类是什么意思呢?就像它的名字一样,在整个程序中只能存在一个对象,也就是单个实例,不可以创建其他的对象,始终使用的只能是那一个对象。

object Singleton {   //声明的一个单例类
    private var name = "你干嘛" // 属性
    override fun toString() = "我叫$name"
}
 
fun main() {
    val singleton = Singleton  //通过类名直接得到此单例类的对象
  	//不可以通过构造函数的形式创建对象
    println(singleton)
}
object Singleton {
    fun test() = println("原神,启动!")
}
 
fun main() {
    Singleton.test()   //单例定义的函数直接使用类名即可调用
}

用起来与Java中的静态属性挺像的,只不过性质完全不一样。单例类的这种性质在很多情况下都很方便,比如我们要编写某些工具操作,可以直接使用单例类的形式编写。

现在我们希望一个类既支持单例类那样的直接调用,又支持像一个普通class那样使用,这时该怎么办呢?

我们可以使用半生对象来完成,实际上就是将一个单例类写到某个类的内部:

class Student(val name: String, val age: Int) {
  	//使用companion关键字在内部编写一个伴生对象,它同样是单例的
    companion object Tools {
      	//伴生对象定义的函数可以直接通过外部类名调用
        fun create(name: String, age: Int) = Student(name, age)
    }
}
 
fun main() {
  	//现在Student不仅具有对象的函数,还可以通过类名直接调用其伴生对象通过的函数
    val student = Student.create("小明", 18)
  	println(student.toString()) // com.ferry.anonymous.Student@1ddc4ec2 未重写toString()
}

委托模式

Delegation

The Delegation pattern has proven to be a good alternative to implementation inheritance, and Kotlin supports it natively requiring zero boilerplate code.

A class Derived can implement an interface Base by delegating all of its public members to a specified object:

interface Base {
    fun print()
}
class BaseImpl(val num: Int) : Base {
    override fun print() = println(num)
}
class Derived(val base: Base): Base {   
//将一个Base的实现类作为属性保存到类中,同样实现Base接口
    override fun print() = base.print()   
    //真正去实现这个接口的,实际上并不是当前类,而是被拉进来的那个替身
}
fun main(){
	Derived(BaseImpl(3)).print()
	// 3
}

word: derive

Kotlin对于这种模式同样给予了原生支持:

interface Base {
    fun print()
}
 
class BaseImpl(val x: Int) : Base {
    override fun print() = println(x)
}
 
class Derived(val b: Base): Base by b  //使用by关键字将所有接口待实现操作委托给指定成员
 
fun main() {  
    val derived = Derived(BaseImpl(1))  
    derived.print()  // number is: 1
}

除了类可以委托给其他对象之外,类的成员属性也可以委托给其他对象:

import kotlin.reflect.KProperty
 
 
// 定义包含属性委托的类
class Example {
    var p: String by Delegate()
}
 
// 委托的类
class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, 这里委托了 ${property.name} 属性"
    }
 
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef 的 ${property.name} 属性赋值为 $value")
    }
}
fun main() {
    val e = Example()
    println(e.p)     // 访问该属性,调用 getValue() 函数
    // com.ferry.delegation.Example@6433a2, 这里委托了 p 属性
    
    e.p = "Runoob"   // 调用 setValue() 函数
    // com.ferry.delegation.Example@6433a2 的 p 属性赋值为 Runoob
    
    println(e.p)
    /*
      println(e.p) 这行代码 又一次调用 getValue() 函数 “e.p”
   */
}
}

泛型

class Score<T>(var name: String)
// 这里的T就是一个待定的类型,同样是这个类具有的,我们称为泛型参数
// 可以看到,它相比普通的类型,仅仅多了一个`<T>`表示类型参数,那么如何使用呢?
fun main() {
  	//在创建对象时,再来明确使用的是什么类型,同样使用尖括号填写
    val score = Score<Int>("数据结构与算法")
}

既然可以做到使用时明确,那现在我们应该怎么去设计这个类呢?

class Score<T>(var name: String, var id: String, var value: T)
//我们在定义类型参数后,T就是一个待定类型,我们可以直接将value属性的类型定义为T
 
fun main() {
    val score = Score<String>("数据结构与算法基础", "EP074512", "优秀")  
  	//在使用时,使用<String>来明确Score的值类型,此时value的类型也会变成String
    val value: String = score.value  //得到的直接就是String类型的结果
}

而泛型类型在类内部使用时,由于无法确定具体类型,也只能当做Any类去使用

有了泛型之后,我们再来使用一些类型就非常方便了,并且泛型并不是每个类只能存在一个,我们可以一次性定义多个类型参数:

class Test<K, V>(val key: K, val value: V)
fun <K : Comparable<V>, V> test() { }

<K : Comparable<V>, V>这是一个带两个类型参数的泛型函数:

  • K 必须是 Comparable<V> 的子类型,给 Comparable<V> 起了名 K
  • V 是一个普通的类型参数
    调用 test<K, V>() 时,必须保证 K 实现了 Comparable<V>
// 数据类型 Int 实现了 Comparable<Int>
public class Int private constructor() : Number(), Comparable<Int>
// 使用时
test<String, String>()   // ✅ String 实现了 Comparable<String>
test<Int, Int>()         // ✅ Int 实现了 Comparable<Int>
test<Double, Double>()   // ✅ Double 实现了 Comparable<Double>

子类在继承时,可以选择将父类的泛型参数给明确为某一类型,或是使用子类定义的泛型参数作为父类泛型参数的实参使用:

abstract class A<T> {
    abstract fun test(): T
}
 
class B: A<String>() {  //子类直接明确为String类型
    override fun test(): String = "Hello World" //明确后所有用到泛型的地方都要变成具体类型
}
 
abstract class C<D>: A<D>() {  //子类也有自己的泛型参数D
    abstract override fun test(): D
}
 
fun main() {
    val b = B()
    println(b.test()) // Hello World
}

除了在类上定义泛型之外,我们也可以在函数上定义:

// comupute: caculate sth
fun <T> compute(transform: (Int) -> T): T {
    val number = 42
    return transform(number)
}
 
fun main() {
    val doubled = compute { it * 2 }  
    println(doubled)  // 输出 84
 
    val asString = compute { "结果是 $it" }
    println(asString) // 输出 结果是 42
}

协变与逆变

假设Int类型是Number类型的子类,正常情况下只能子类转换为父类,泛型类型Test<T>存在以下几种形变:

  • 协变 (Covariance):因为Int是Number的子类,所以Test<Int>同样是Test<Number>的子类,可以直接转换
  • 逆变(Contravariance):跟上面相反,Test<Number>可以直接转换为Test<Int>,前置是后者的子类
  • 抗变 (Invariant):Test<Int>跟Test<Number>没半毛钱关系,无法互相转换

在Kotlin的泛型中,默认就是抗变的,即使两个类型存在父子关系,到编译器这里也不认账,但是实际上我们需要的可能是协变或是逆变,为了处理这种情况,Kotlin提供了两个关键字供我们使用:

  • out 关键字用于标记一个类型参数作为协变,可以实现子类到父类的转换。
  • in 关键字用于标记一个类型参数作为逆变,可以实现父类到子类的转换。
class Test<T>(var data: T)
 
fun main() {
    val test1: Test<Int> = Test(888)
  	//使用out关键字使得此类型协变,可以代表Number及其子类
    val test2: Test<out Number> = test1  //此时就可以正常接受子类Int了
}

虽然看上去非常难理解,但是简单来说,其实就是为类型添加一个可以转换子类的性质,out作用就是使类型支持协变,可以支持泛型从父类转换为子类,但是不能子类转父类,比如这里使用Any就没法成功接受。相反的,如果我们标记某个类型为in,那么这个类型就是逆变的,可以由父类向下转化:

class Test<T>(var data: T)
 
fun main() {
    val test1: Test<Any> = Test(888)
  	//使用in关键字使得此类型逆变,可以代表Number及其父类
    val test2: Test<in Number> = test1  //Any是Number的父类,逆变
}

在使用这种协变或逆变类型时,具体使用的类型就变得不确定了,导致不同的界限会有不同的效果,比如下面:

class Test<T>(var data: T)
 
fun main() {
  	//协变类型在使用时会变成上界,因为无论子类是什么,都是继承自上界类型的
    val test: Test<out Number> = Test(888) // 泛型必须是 Number 的子类,不确定是 Int, Double, Float...
    var data: Number = test.data // 所以 变量data的类型也是 Number
}
fun main() {
  	//逆变类型在使用时由于没有上界,具体使用哪个父类也不清楚,所以只能是Any?类型了
    val test: Test<in Number> = Test(888) // 泛型只能是 Number的父类
    var data: Any? = test.data // Number的父类是 Any,或者为null
}

也就是:如果一个泛型没有特别的上界限制,默认就是 Any?
在 Kotlin 里,如果你定义一个泛型类:

class Test<T>(val data: T)

这里的 T 没有限定上界,所以默认就是:

class Test<T : Any?>(val data: T) // 默认 T 的类型为 null,或 Any

在使用out和in之后,类型的使用就可以更加灵活,但是这样会存在一定的安全隐患,比如下面的代码:

// 了解
class Test<T>(val data: T)
open class A
class B: A()
class C: A()
 
fun main() {
    val test1: Test<B> = Test(B())  //这里存放的都是B类型的数据
    // 没问题
    val test2: Test<out A> = test1  //此时test2与test1是同一个对象,但是test2是out A
 
    // 本质上 对象test1的data类型是 B,在上面写好了
    //报错: test2.data = C()  //由于C是A的子类,按照正常情况来说可以直接用(但实际上这句会报错)
    
    val data: B = test1.data  //这下搞笑了,拿到的类型应该是C,结果接收的类型是B
}

为了解决这种情况,Kotlin对于out或in的类型进行了限制,比如设置了out的情况下:

Setter for ‘data’ is removed by type projection

属性的setter操作被限制,无法通过编译,因为这可能会导致不安全的操作发生,而in也是同理的:

class Test<T>(val data: T)
open class A
class B: A()
class C: A()
fun main() {
    val test1: Test<A> = Test(B())  //这里存的是B类型的对象
    val test2: Test<in C> = test1   //直接使用in C接收得到
    // val data: C = test2.data   //此时得到的结果应该也可以是C才对,那肯定是错的
    
    // 得进行强转
    val data: C = test2.data as C
}

因此,在使用in时,属性的getter操作被限制,会提示类型不匹配,得到的类型也是Any? 无法通过编译,同样是因为可能存在不安全的操作。不仅仅是属性,包括所有函数的参数、返回值,都会受到限制:

class Test<T>(val data: T){
	fun test(t: T){}
}
 
open class A
class B: A()
class C: A()
 
fun main() {
    val test1: Test<B> = Test(B())
    val test2: Test<out A> = test1
    // test2.test(C())  //报错,因为这里存在消费行为
}

因此,对于in和out来说,协变和逆变的属性将其限制为了生产者和消费者:

  • 使用out修饰的泛型不能用作函数的参数,对应类型的成员变量setter也会被限制,只能当做一个生产者使用。
  • 使用in修饰的泛型不能用作函数的返回值,对应类型的成员变量getter也会被限制,只能当做一个消费者使用。

在有些时候,我们可能并不在乎到底使用哪一个类型,我们希望一个变量可以接受任意类型的结果,而不是去定义某一个特定的上界或下界。在Kotlin泛型中,星号(*)代表了一种特殊的类型投影,可以代表任意类型:
相当于 Box<out Any?>

// 一个泛型类
class Box<T>(val value: T)
 
// 一个打印函数,可以接受 Box<*>,也就是未知类型的 Box
fun printBox(box: Box<*>) {
    // 这里取出来的值类型是 Any?,因为我们不知道 T 是什么
    val data: Any? = box.value
    println("Box contains: $data")
}
 
fun main() {
    val intBox: Box<Int> = Box(123)
    val stringBox: Box<String> = Box("Hello")
    val doubleBox: Box<Double> = Box(3.14)
 
    // 可以传入不同类型的 Box
    printBox(intBox)    // Box contains: 123
    printBox(stringBox) 
    printBox(doubleBox)
}

Calling java from kotlin

Functional Interface

  @NativeType("GLFWwindowsizefun")
public static @Nullable GLFWWindowSizeCallback glfwSetWindowSizeCallback(@NativeType("GLFWwindow *") long window, @NativeType("GLFWwindowsizefun") @Nullable GLFWWindowSizeCallbackI cbfun) {
	return GLFWWindowSizeCallback.createSafe(nglfwSetWindowSizeCallback(window, memAddressSafe(cbfun)));
}

how to use this static method? 传统方式

// Set up a window size callback to adjust the viewport
glfwSetWindowSizeCallback(window, new GLFWWindowSizeCallbackI() {
		@Override
		public void invoke(long window, int width, int height) {
				// Make sure the viewport matches the new window dimensions
				glViewport(0, 0, width, height);
		}
});

in kotlin,因为不用一个参数 long window 所以省略

glfwSetWindowSizeCallback(window) { _, width, height ->
	// 调整 OpenGL 视口,使渲染区域和窗口大小一致
	glViewport(0, 0, width, height)
}

lambda 作为函数最后一个参数时,可以写在括号外

fun repeatAction(times: Int, action: () -> Unit) {
    repeat(times) {
        action()
    } 
}
 
fun main() {
    repeatAction(5){
        println("I am the actual action")
    }
    // 'I am the actual action'  was executed 5x
}
// repeat
// 相当于 for 循环的写法
for (i in 1..times) {  
	action()  
}

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
fun main() {
  	//使用Java中的Runnable接口(省略后)
    val runnable: Runnable = Runnable {
        println("Hello World")
    }
 
	// java 方式
    val runnable2: Runnable = Runnable({
	    println("Hello World")
    }) 
 
	 Thread(runnable).start()
}

作用域函数

// 传统方式
data class Person(var name: String, var age: Int, var city: String) {
    fun moveTo(newCity: String) { city = newCity }
    fun incrementAge() { age++ }
}
 
fun main() {
//sampleStart
    val alice = Person("Alice", 20, "Amsterdam")
    println(alice)
    alice.moveTo("London")
    alice.incrementAge()
    println(alice)
//sampleEnd
}

返回 lambda 表达式结果

let

data class Person(var name: String, var age: Int, var city: String) {
    fun moveTo(newCity: String) {
        city = newCity
    }
    fun incrementAge(): Unit{
        age++
    }
}
 
fun main() {
    Person("Alice", 23, "Amsterdam").let {
        println(it) // Person(name=Alice, age=23, city=Amsterdam)
        it.incrementAge()
        it.moveTo("London")
        println(it) // Person(name=Alice, age=24, city=London)
    } // 使你的代码更加简洁易读
}
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this) // 可以看到返回一个 lambda,参数为调用者
}
val x = "Hello"
val len = x.let { str -> // 一个 lambda
    str.length   // str 就是 "Hello"
}
println(len) // 5

run

fun main() {
//sampleStart
    val numbers = mutableListOf("one", "two", "three")
    val countEndsWithE = numbers.run {
        add("four")
        add("five")
        // count,返回满足条件的元素个数:(需要传入lambda)
        count { it.endsWith("e") } // lambda的返回值为 int
    }
    println("There are $countEndsWithE elements that end with e.")
    // There are 3 elements that end with e.
    // one
    // three
    // five
//sampleEnd
}

返回上下文对象

also

also:用于执行一个lambda表达式并返回对象本身,跟apply功能一致像,但是采用的是it参数形式传递给Lambda当前对象。

//对当前对象进行操作,并返回当前对象本身
public inline fun <T> T.also(block: (T) -> Unit): T {
    ...
    block(this)   //调用我们传入的函数
    return this   //返回当前T类型对象本身
}
fun writeToLog(message: String) {
    println("INFO: $message") 
    // INFO: getRandomInt() generated value 67
}
 
fun main() {
    //sampleStart
    fun getRandomInt(): Int {
        return Random.nextInt(100).also {
            writeToLog("getRandomInt() generated value $it")
        }
    }
 
    val i = getRandomInt()
    println(i) // 67
//sampleEnd
}

INFO: getRandomInt() generated value 67
67

Random.nextInt(100)这个数字 int调用了 also调用了lambda,之后返回了自己(也就是)

fun writeToLogIntTest(value: Int): Unit {
    println("INFO: $value") // INFO: 3
}
 
fun main() {
    val number = 3
    number.also(::writeToLogIntTest)
}

apply

class Student(var name: String, var age: Int) {
    fun hello() = println("Hello, I am ${this.name}, I am ${this.age} years old")
}
 
// 接受: 可空类型 Student, 返回: 可空类型 Student
fun createStudent(student: Student?): Student? {
    student?.name = "Alice" // 不为空的情况 赋值
    student?.age = 18
    student?.hello()
    return student
}
 
fun main() {
    val newStudent = createStudent(Student("", 0))
    // Hello, I am Alice, I am 18 years old
    println(newStudent) // com.ferry.generic.Student@4d405ef7
}
// 优化
class Student(var name: String, var age: Int) {  
    fun hello() = println("Hello, I am ${this.name}, I am ${this.age} years old")  
}
 
fun createStudentApply(student: Student?): Student?{  
    student?.apply {  
        this.name = "John"  
        this.age = 17  
        this.hello()  
    }  
    return student  
}
 
// 接受: 可空类型 Student, 返回: 可空类型 Studentfun 
fun createStudent(student: Student?): Student? {  
    student?.name = "Alice" // 不为空的情况 赋值  
    student?.age = 18  
    student?.hello()  
    return student  
} 
  
fun main() {  
    val newStudent = createStudent(Student("", 10))  
    println(newStudent)  
  
    createStudentApply(newStudent)  
}
data class Person(var name: String, 
var age: Int = 0, 
var city: String = "")
 
fun main() {
//sampleStart
    val adam = Person("Adam").apply { 
        age = 20        // 同 this.age = 20
        city = "London"
    }
    println(adam)
//sampleEnd
}

介绍一下这些函数时如何使用的,这里以apply为例,这个函数功能是简化我们对某个对象的操作并在最后返回对象本身,在Standard.kt中是这样定义的:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    // ...
    block()   // 调用我们传入的函数
    return this   // 返回当前T类型对象本身
}
// 泛型 T,使用时代表任意类型
// T.apply()对任意类型的扩展函数
// 参数:接受一个任意类型的扩展函数
// 返回值:调用者本身

apply 及 also 的返回值是上下文对象本身。因此,它们可以作为辅助步骤包含在调用链中:可以继续在同一个对象上一个接一个地进行链式函数调用。

data class User(var name: String = "", var age: Int = 0)
 
fun main() {
    val user = User().apply {
        name = "Alice"   // 直接操作 User 对象
        age = 25
    }
 
    println(user) // 输出:User(name=Alice, age=25)
}

difference between also and apply

data class User(var name: String = "", var age: Int = 0)
 
fun main() {
    val wonder = User("default", 0)
    println(wonder) // User(name=default, age=0)
    val appliedWonder = wonder.apply {
        name = "Alice"   // 直接操作 User 对象
        age = 25
    }
    println(appliedWonder) // User(name=Alice, age=25)
}

如果不重写 toString(),wonder 和 appliedWonder为同一个对象,同一个hashcode

class User(var name: String = "", var age: Int = 0)
 
fun main() {
    val wonder = User("default", 0)
    println(wonder) // com.ferry.scopefun.User@72ea2f77
    val appliedWonder = wonder.apply {
        name = "Alice"   // 直接操作 User 对象
        age = 25
    }
    println(appliedWonder) // com.ferry.scopefun.User@72ea2f77
}

Kotlin 标准库里 with 的简化版

fun <T> withReceiver(receiver: T, block: T.() -> Unit) {
    receiver.block()
}
 
fun main() {
    withReceiver(StringBuilder()) {
        append("Hello, ")
        append("World!")
        println(toString())  // 输出 Hello, World!
    }
 
    withReceiver(mutableListOf(1, 2, 3)) {
        add(4)
        add(5)
        println(this)  // 输出 [1, 2, 3, 4, 5]
    }
    // 函数名 withReceiver 表达了“带着某个对象去做操作”
}

带接收者的函数类型:

val greet: String.() -> Unit = { 
    println("Hello, $this") // this 指向接收者 String
}
 
"Bob".greet()  // 调用时,"Bob" 就是接收者

takeIf 与 takeUnless

https://book.kotlincn.net/text/scope-functions.html

集合类

Collections

When programming, it is useful to be able to group data into structures for later processing. Kotlin provides collections for exactly this purpose.

Kotlin has the following collections for grouping items:

Collection typeDescription
ListsOrdered collections of items
SetsUnique unordered collections of items
MapsSets of key-value pairs where keys are unique and map verb to only one value
Each collection type can be mutable or read only.

  • List: 有序的集合,通过索引访问元素,可以包含重复元素,比如电话号码:它就是一组数字,并且顺序很重要,而且数字可以重复。
  • Set: 不包含重复元素的集合,它更像是数学中的集合,一般情况下不维护元素顺序,例如,彩票上的数字:都是独一无二的,并且它们的顺序不重要。
  • Map: 是一组键值对,其中每一个键不可重复存在,每个键都映射到恰好一个值(值可以重复存在)这跟数学上的映射关系很像。它经常用于存储(员工ID → 员工具体信息)这样的结构。
// 要创建一个List集合非常简单,就跟我们之前创建数组一样:
fun main() {
    val list: MutableList<String> = mutableListOf(1, 2, 3, 4)   //使用mutableListOf来创建一个可修改的List集合
    list[0] = 10   //修改某个位置上的元素
    println(list[1])   //获取某个位置上的元素
    println(list)    //打印出来的结果也是非常标准的: [10, 2, 3, 4]
}

List集合

List就像它的名字一样,就是一个列表,它存储一组有序的数据,比如我们看到的餐厅菜单,还有游戏的排行榜,每一道菜、每一个玩家都是按顺序向下排列的,并且根据情况,我们可以自由地在某个位置插入或删除一个新的元素,列表的长度也会动态发生变化,List集合就是为了这些功能而生的。

Lists store items in the order that they are added, and allow for duplicate items.

fun main() {
    val list: MutableList<Int> = mutableListOf(1, 2, 3, 4)   //使用mutableListOf来创建一个可修改的List集合
    println(list)//[1, 2, 3, 4]
 

遍历:

 fun main() {
    val list: MutableList<Int> = mutableListOf(1, 2, 3, 4)   //使用mutableListOf来创建一个可修改的List集合
    var counter = 0
    list.forEach {
        println("$counter:[$it]")
        counter++
    }
    //0:[1]
	 //1:[2]
	 //2:[3]
	 //3:[4]
}

Set集合

fun main() {
  	//使用mutableSetOf来创建一个Set集合
    val set: Set<String> = mutableSetOf("AAA", "BBB", "BBB", "CCC")
    println(set)   //由于不允许出现重复元素,得到 [AAA, BBB, CCC]
}

与列表一样,可以随意插入元素,元素默认在尾部插入,顺序为插入顺序:

val set: MutableSet<String> = mutableSetOf("AAA", "DDD", "CCC")
set.add("BBB")
println(set)//[AAA, DDD, CCC, BBB]

不过Set默认不支持在指定位置插入元素,只能尾插,同时我们也不能通过下标去访问Set中的元素,这是因为Set底层采用的并不是线性数据结构存储,而是用了哈希表或是树形结构(感兴趣的小伙伴可以看一下另一期数据结构与算法篇教程)而内部元素的顺序则是采用的其他形式进行维护的。
…

Map映射

定义一个键值对,官方为我们提供了一个非常方便的中缀函数:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

我们只需要指定:

val pair: Pair<Int, String> = 10001 to "小明"   //得到一个由学号指向学生名称的键值对

这样,我们就成功创建出了一个映射关系,但是这仅仅是单个映射,如果我们想要存储所有学生的学号映射关系,需要使用Map来实现,使用Map也非常简单:

val map: MutableMap<Int, Student> = mutableMapOf(
    10001 to Student("小明", 18),
    10002 to Student("小红", 17),
    10003 to Student("小刚", 19)
)
//使用mutableMapOf可以放入多个键值对,并生成一个Map对象

这样我们就成功地将所有的键值对存储在Map中了,我们接着来看看如何去访问,比如现在我们要查找指定学号的学生:

val student: Student? = map[10001]   //使用[]运算符通过Key查找Value

遍历一个Map

// 第一种方法
fun main() {
    
    val map: MutableMap<Int,Person> = mutableMapOf(
        1 to Person("Jack",10),
        2 to Person("Smith",14),
        3 to Person("John",16)
    )
 
    map.forEach { (k, v) -> println("键: $k, 值 $v") }
//键: 1, 值 com.ferry.collection.Person@66a29884
//键: 2, 值 com.ferry.collection.Person@4769b07b
//键: 3, 值 com.ferry.collection.Person@cc34f4d
}
 
class Person(val name: String, val age: Int)

Grouping

The Kotlin standard library provides extension functions for grouping collection elements. The basic function groupBy() takes a lambda function and returns a Map. In this map, each key is the lambda result, and the corresponding value is the List of elements on which this result is returned. This function can be used, for example, to group a list of Strings by their first letter.

You can also call groupBy() with a second lambda argument – a value transformation function. In the result map of groupBy() with two lambdas, the keys produced by keySelector function are mapped to the results of the value transformation function instead of the original elements.

This example illustrates using the groupBy() function to group the strings by their first letter, iterating through the groups on the resulting Map with the for operator, and then transforming the values to uppercase using the valueTransform function:

val numbers = listOf("one", "two", "three", "four", "five")
 
// Groups the strings by their first letter using groupBy()
val groupedByFirstLetter = numbers.groupBy { it.first().uppercase() }
println(groupedByFirstLetter)
// {O=[one], T=[two, three], F=[four, five]}
 
// Iterates through each group and prints the key and its associated values
for ((key, value) in groupedByFirstLetter) {
    println("Key: $key, Values: $value")
}
// Key: O, Values: [one]
// Key: T, Values: [two, three]
// Key: F, Values: [four, five]
 
// Groups the strings by their first letter and transforms the values to uppercase
val groupedAndTransformed = numbers.groupBy(keySelector = { it.first() }, valueTransform = { it.uppercase() })
println(groupedAndTransformed)
// {o=[ONE], t=[TWO, THREE], f=[FOUR, FIVE]}

Reflection

在这里提供一个测试用例,后续反射的使用讲解会根据用例操作:

fun main() {
    println(Person().javaClass) // Class
    println(Person()::class.java) // Class
    println(Person::class.java) // Class
 
    println(Person().javaClass.kotlin) // KClass
    println(Person::class) // KClass
}
class Person {
    var instanceField: String = "instanceFieldValue"
 
    constructor()
 
    constructor(instanceField: String) {
        this.instanceField = instanceField
    }
 
    fun invokeMethod() {
        println("invokeMethod")
    }
 
    fun invokeMethodWithParam(param: String) {
        println("invokeMethodWithParam, param = $param")
    }
 
    companion object {
        var companionField: String = "companionFieldValue"
 
        fun invokeCompanionMethod() {
            println("invokeCompanionMethod")
        }
 
        fun invokeCompanionMethodWithParam(param: String) {
            println("invokeCompanionMethodWithParam, param = $param")
        }
    }
}