XiaoLin's Blog

Xiao Lin

Java转kotlin开发知识补充笔记

77
2024-01-31

kotlin 介绍

Kotlin 是一种现代、简洁且强大的编程语言,由 JetBrains 于 2011 年推出。它旨在解决 Java 的一些缺点,例如冗长的代码和缺乏安全性。Kotlin 与 Java 兼容,这意味着它可以在 Java 虚拟机 (JVM) 上运行,并且可以使用 Java 库。

Kotlin 的主要特点包括:

  • 简洁: Kotlin 的语法简洁且易于学习,这使得它成为初学者和经验丰富的开发人员的理想选择。
  • 安全: Kotlin 是一种静态类型语言,这意味着它可以在编译时检测到许多错误。这有助于防止运行时错误并提高代码的可靠性。
  • 互操作性: Kotlin 与 Java 兼容,这意味着它可以与 Java 代码一起使用。这使得 Kotlin 成为现有 Java 项目的理想选择。
  • 支持函数式编程: Kotlin 支持函数式编程范式,这使得它非常适合编写并发代码和处理大数据。
  • 强大的工具支持: Kotlin 拥有强大的工具支持,包括集成开发环境 (IDE) 和编译器。这使得 Kotlin 非常适合大型项目和团队开发。

Kotlin 在许多领域都有着广泛的应用,包括:

  • Android 开发: Kotlin 是 Google 官方推荐的 Android 开发语言之一。它非常适合编写 Android 应用,因为它可以提高代码的可读性和安全性。
  • Web 开发: Kotlin 可以用于编写 Web 应用,例如 REST API 和 Web 服务。它与 Spring Boot 等流行的 Web 框架兼容。
  • 桌面应用开发: Kotlin 可以用于编写桌面应用,例如图形用户界面 (GUI) 应用和命令行工具。它与 Swing 和 JavaFX 等流行的桌面应用框架兼容。
  • 游戏开发: Kotlin 可以用于编写游戏,例如 2D 和 3D 游戏。它与 LibGDX 等流行的游戏开发框架兼容。

Kotlin 是一种非常有前途的编程语言,它拥有强大的功能和广泛的应用领域。随着 Kotlin 的不断发展,它将在越来越多的领域得到应用。

kotlin 的基础知识

kotlin的数据类型

在Kotlin中,数据类型可以大致分为两大类:基本类型引用类型

基本类型

基本类型(或称为原始类型)在Kotlin中有特殊的表示方式,但底层上它们是借用了Java的原始类型。Kotlin的基本类型包括:

  • 数字类型:包括IntLongShortByteDoubleFloat。这些类型分别对应于Java的intlongshortbytedoublefloat
  • 字符Char,用于表示单个字符。
  • 布尔Boolean,用于表示真(true)或假(false)。

引用类型

引用类型是指除了基本类型以外的所有类型,它们引用的是对象的内存地址。在Kotlin中,几乎一切都是对象,这包括了:

  • 字符串String,表示字符序列。
  • 数组Array,用来存储固定大小的同类型元素。
  • 集合类型:包括ListSetMap等,这些类型在Kotlin中是接口,具体实现依赖于标准库中的类,如ArrayListHashSetHashMap等。
  • 数字类型:Number, Number类型是一个表示数值的泛型类,它是所有内置数值类型的超类。这包括 Byte, Short, Int, Long, Float, 和 Double。这意味着你可以使用 Number 类型来引用任何数值类型的变量,而不需要具体指定它是什么类型。这在处理需要泛型或者不确定具体数值类型的情况下非常有用。
  • 自定义类:你可以定义自己的类,这些类的实例都是引用类型。

特殊类型

  • Any:Kotlin中所有类的超类,相当于Java中的Object。如果一个函数可以接受任何类型,那么它的参数类型可以用Any
  • Unit:表示函数没有返回值的类型,相当于Java的void
  • Nothing:表示永远不会返回的类型。通常用于函数,如果函数是抛出异常,那么它的返回类型就是Nothing。这对于类型推断非常有用。
  • Nullable类型:在类型名后面加?表示该类型的变量可以持有null值。例如,String?可以是一个字符串,也可以是null

Kotlin在设计上力求简洁和安全,其中类型系统的设计就体现了这一点,例如通过区分可空类型和非空类型,编译器能够在编译时期捕获大多数的空指针异常。

类定义

class MyClass(val name: String) {
   // 类体
}
  • class 关键字表示这是一个类定义。
  • MyClass 是类的名称。
  • (val name: String) 是类的主构造函数。主构造函数是在创建类实例时调用的函数。
  • val 关键字表示 name 是一个只读属性。
  • Stringname 的类型。
  • name: String 是参数列表。参数列表包含主构造函数的参数。
  • {} 是类体的开始和结束。类体包含类的成员。
  • 类成员可以是属性、函数、构造函数等。

属性

属性是类的成员,用于存储数据。属性可以是只读的或可写的。

  • 只读属性使用 val 关键字声明。只读属性只能在主构造函数中初始化。
  • 可写属性使用 var 关键字声明。可写属性可以在主构造函数中或类体中初始化。
class MyClass(val name: String) {
    var age: Int = 0
}
  • age 是一个可写属性。
  • Intage 的类型。
  • 0age 的初始值。

在 Kotlin 中,属性修饰符用于控制属性的可见性和可变性。这些修饰符可以应用于类、对象和接口中的属性。

以下是 Kotlin 中可用的属性修饰符:

  • public: 表示属性对任何代码都是可见的,包括在其他模块中。
  • protected: 表示属性对类及其子类是可见的,但不包括在其他模块中。
  • internal: 表示属性仅对同一个模块中的代码是可见的。
  • private: 表示属性仅对声明它的类是可见的。

除了这些可见性修饰符之外,Kotlin 还提供了以下可变性修饰符:

  • val: 表示属性是不可变的,这意味着它不能被重新赋值。
  • var: 表示属性是可变的,这意味着它可以被重新赋值。

属性修饰符可以组合使用,例如:

private val name: String = "John Doe"

这将创建一个名为 name 的私有不可变属性,并将其初始化为字符串 "John Doe"

属性修饰符的示例

以下是一些属性修饰符的示例:

  • public val name: String - 创建一个公共不可变属性名为 name,类型为 String
  • protected var age: Int - 创建一个受保护的可变属性名为 age,类型为 Int
  • internal val address: String - 创建一个内部不可变属性名为 address,类型为 String
  • private var balance: Double - 创建一个私有可变属性名为 balance,类型为 Double
最佳实践
  • 使用 val 来声明不可变属性,除非您需要显式地重新赋值。
  • 使用 var 来声明可变属性,但只在需要时才使用它。
  • 考虑使用 private 来声明属性,除非您需要在其他类或模块中访问它们。
  • 考虑使用 internal 来声明属性,如果您需要在同一个模块中的其他类中访问它们。
  • 考虑使用 protected 来声明属性,如果您需要在子类中访问它们。
  • 考虑使用 public 来声明属性,如果您需要在其他模块中访问它们。
属性的 get/set 方法定义

Kotlin 属性的 set/get 方法允许您控制对属性值的访问。

  • set() 方法用于设置属性值。
  • get() 方法用于获取属性值。

您可以通过在属性声明中使用 setget 关键字来定义这些方法。例如:

class A {  
    var a: Int = 0  
        set(value) {  
            if (value < 0) {  
                throw IllegalArgumentException("a must be positive, but was $value")  
            }            field = value  
        }  
        get() = field  
}

在上面的示例中,A 类有一个名为 a 的属性,它是一个 Int 类型整形。

a 属性的 set() 方法检查新值是否为负数,如果是则抛出异常。否则,它将新值存储在 field 字段中。

a 属性的 get() 方法返回 field 字段的值。

属性重写

属性重载是 Kotlin 中的一项功能,它允许您为同一属性定义多个不同的实现。这在您需要在不同情况下使用不同值的属性时非常有用。

要重载属性,您需要使用 override 关键字。例如,以下代码演示了如何重载一个名为 name 的属性:

class Person(val name: String) {
    override var name: String = ""
}

在上面的代码中,Person 类有一个名为 name 的属性,它被初始化为一个空字符串。Person 类还重载了 name 属性,这样就可以在对象创建后更改该属性的值。

要使用重载的属性,您需要使用 super 关键字。例如,以下代码演示了如何使用重载的 name 属性:

val person = Person("Alice")
person.name = "Bob"

在上面的代码中,person 对象的 name 属性最初被设置为 “Alice”。但是,使用 person.name = "Bob" 语句将该属性的值更改为 “Bob”。

属性重载是一个强大的功能,它可以用于各种情况。例如,您可以使用属性重载来:

  • 在不同的环境中使用不同的配置值。
  • 在不同的设备上使用不同的资源。
  • 为不同的用户提供不同的体验。

属性重载是一种非常灵活的功能,它可以帮助您编写更灵活和可重用的代码。

函数

函数是类的成员,用于执行代码。函数可以有参数,也可以没有参数。

class MyClass(val name: String) {
    fun greet() {
        println("Hello, $name!")
    }
}
  • greet 是一个函数。
  • () 是参数列表。参数列表包含函数的参数。
  • {} 是函数体的开始和结束。函数体包含函数的代码。

指向性传参

fun sum(a: Int, c: (Int) -> Unit, b: Int = 2) {  
    c(a + b)  
}  
  
sum(a = 1,b = 4, c = {  
    println(it)  
})
  • 函数修饰符
  1. inline:将函数内联到调用它的位置,而不是创建一个单独的函数调用。这可以提高性能,尤其是在函数很小且经常被调用的时候。
  2. tailrec:表示函数是尾递归的,即函数的最后一步是递归调用自身。这可以防止栈溢出,因为尾递归函数不需要为每次递归调用创建一个新的栈帧。
  3. suspend:表示函数是一个挂起函数,即它可以在协程中被挂起和恢复。挂起函数可以使用 await 关键字来等待其他挂起函数的结果。
  4. noinline:表示函数不能被内联。这通常用于防止函数被内联到一个不能访问其自由变量的上下文中。
  5. crossinline:表示函数可以被内联,但它不能访问其自由变量。这通常用于防止函数被内联到一个会改变其自由变量值的上文中。
  6. varargs:表示函数可以接受可变数量的参数。可变参数必须是函数的最后一个参数。
  7. operator:表示函数可以被用作运算符。例如,你可以定义一个 + 运算符函数来重载加法运算。
  8. infix:表示函数可以被用作中缀运算符。例如,你可以定义一个 + 中缀运算符函数来重载加法运算。
  9. external:表示函数是一个外部函数,即它不是由 Kotlin 编译器生成的。外部函数通常用于调用原生库中的函数。
  10. expect:表示函数是一个期望函数,即它将在另一个模块中实现。期望函数通常用于创建库,以便其他模块可以引用库中的函数,而无需知道库的实现细节。

构造函数

构造函数是类的成员,用于创建类实例。构造函数可以在类名后面使用 () 调用。

val myClass = MyClass("John")
  • MyClass("John") 是一个构造函数调用。
  • myClass 是一个 MyClass 类的实例。

kotlin 的构造函数

Kotlin的构造函数可以分为两种类型:主构造函数和次构造函数。

  1. 主构造函数:
    主构造函数是类头的一部分,可以在类名后面声明。主构造函数没有任何代码体,它定义了类的属性。
    例如:
class Person(val name: String, val age: Int) {
    // 类的属性在主构造函数中定义
}

在上面的例子中,Person类有两个属性nameage,它们被声明在主构造函数中。

  1. 次构造函数:
    次构造函数是可选的,用于提供额外的初始化逻辑。一个类可以有多个次构造函数,它们需要使用constructor关键字来声明。
    例如:
class Person(val name: String, val age: Int) {
    constructor(name: String) : this(name, 0) {
        // 次构造函数中调用了主构造函数
    }
}

在上面的例子中,Person类有一个主构造函数和一个次构造函数。次构造函数接受一个名为name的参数,并调用了主构造函数来初始化剩余的属性。

除了主、次构造函数外,Kotlin还支持初始化块(初始化代码块)和委托给其他构造方法。这些功能提供了更灵活和强大的初始化选项。

继承

继承允许一个类从另一个类继承属性和函数。继承使用 : 关键字表示。

class MyClass(val name: String) {
    fun greet() {
        println("Hello, $name!")
    }
}

class Subclass(name: String) : MyClass(name) {
    fun goodbye() {
        println("Goodbye, $name!")
    }
}
  • SubclassMyClass 继承属性和函数。
  • Subclass 有一个自己的构造函数。
  • Subclass 有一个自己的函数 goodbye

接口

接口是一个定义方法签名的类。接口不能有属性或函数的实现。接口使用 interface 关键字声明。

interface MyInterface {
    fun greet()
}

class MyClass : MyInterface {
    override fun greet() {
        println("Hello!")
    }
}
  • MyInterface 是一个接口。
  • MyClass 实现 MyInterface 接口。
  • MyClass 必须实现 greet 方法。

初始化

Kotlin 中类的初始化分成了三块,他们的执行顺序也是依次执行的:

  1. 主构造方法
  2. 次构造方法
  3. 初始化块(initializer blocks),即 init 块

这边常用的 init 块的两种场景是:

  1. 多个构造方法存在的情况下,可以将统一处理的逻辑加到 init 块中
  2. 在只存在主构造方法的情况下,相关初始化属性的逻辑处理可以放到 init 块中
class CountDownView : LinearLayout {

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    init {
        orientation = HORIZONTAL
        gravity = Gravity.CENTER_VERTICAL + Gravity.START
    }
}

抽象类

抽象类是一个不能被实例化的类。抽象类必须至少有一个抽象方法。抽象方法没有实现。抽象类使用 abstract 关键字声明。

abstract class MyClass {
    abstract fun greet()
}

class Subclass : MyClass() {
    override fun greet() {
        println("Hello!")
    }
}
  • MyClass 是一个抽象类。
  • SubclassMyClass 继承。
  • Subclass 必须实现 greet 方法。

枚举类

枚举类是一个包含一组常量的类。枚举类使用 enum class 关键字声明。

enum class MyEnum {
    A, B, C
}

val myEnum = MyEnum.A
  • MyEnum 是一个枚举类。
  • A, B, C 是枚举类的常量。
  • myEnum 是一个 MyEnum 类的实例。

内部类

Kotlin 中的内部类与 Java 中的内部类非常相似,它们都可以访问外部类的成员。Kotlin 中的内部类可以分为两种:嵌套类和局部类。

嵌套类

嵌套类是声明在另一个类中的类。嵌套类可以访问外部类的成员,包括私有成员。嵌套类通常用于将相关类组织在一起,或者将实现细节隐藏在外部类中。

class Outer {
    private val name = "Outer"

    class Nested {
        fun accessOuter() {
            println("Name: $name")
        }
    }
}

fun main() {
    val outer = Outer()
    val nested = Outer.Nested()
    nested.accessOuter()
}

局部类

局部类是声明在函数或其他局部作用域中的类。局部类只能访问其所在作用域内的成员,包括私有成员。局部类通常用于将实现细节隐藏在函数或其他局部作用域中。

fun main() {
    fun localClassExample() {
        class Local {
            fun accessLocal() {
                println("Local class")
            }
        }

        val local = Local()
        local.accessLocal()
    }

    localClassExample()
}

内部类的访问控制

内部类的访问控制与 Java 中的内部类非常相似。内部类可以具有以下访问控制修饰符:

  • public:内部类可以从任何地方访问。
  • protected:内部类只能从其所在的包及其子包中访问。
  • internal:内部类只能从其所在的模块中访问。
  • private:内部类只能从其所在的类中访问。

内部类的使用场景

内部类可以用于以下场景:

  • 将相关类组织在一起。
  • 将实现细节隐藏在外部类中。
  • 在函数或其他局部作用域中创建临时类。
  • 创建匿名类。

匿名内部类

匿名内部类是没有任何名称的内部类。匿名内部类通常用于实现接口或抽象类。

fun main() {
    val runnable = object : Runnable {
        override fun run() {
            println("Running...")
        }
    }

    val thread = Thread(runnable)
    thread.start()
}

总结

Kotlin 中的内部类与 Java 中的内部类非常相似,它们都可以访问外部类的成员。Kotlin 中的内部类可以分为两种:嵌套类和局部类。嵌套类是声明在另一个类中的类,局部类是声明在函数或其他局部作用域中的类。内部类可以用于将相关类组织在一起,将实现细节隐藏在外部类中,在函数或其他局部作用域中创建临时类,创建匿名类。

密封类

Kotlin 密封类是一种限制子类只能在同一文件中定义的类。这与 Java 中的枚举类相似,但密封类更灵活,因为它允许子类具有不同的构造函数和属性。

密封类的语法如下:

sealed class Shape {
    class Circle(val radius: Double) : Shape()
    class Square(val sideLength: Double) : Shape()
    class Rectangle(val width: Double, val height: Double) : Shape()
}

在上面的示例中,Shape 是一个密封类,它有三个子类:CircleSquareRectangle。这些子类只能在同一文件中定义,并且不能在其他文件中使用。

密封类可以用于以下场景:

  • 当您需要限制子类只能在同一文件中定义时。
  • 当您需要确保所有子类都具有相同的基类时。
  • 当您需要在子类之间进行模式匹配时。

密封类的主要优点是它可以提高代码的可读性和可维护性。通过将所有子类都放在同一文件中,您可以更容易地看到它们之间的关系并理解它们是如何工作的。此外,密封类还可以帮助您避免在子类之间出现不一致的情况。

密封类的主要缺点是它可能会限制您的灵活性。如果您以后需要添加一个新的子类,则必须修改密封类并重新编译所有相关的代码。

总体而言,密封类是一种非常强大的工具,可以帮助您编写更清晰、更可维护的代码。如果您需要限制子类只能在同一文件中定义,那么密封类是一个很好的选择。

以下是密封类的几个示例:

  • 枚举类:枚举类是一种特殊的密封类,它只能有一个实例。枚举类通常用于表示一组有限的可能值。
  • 数据类:数据类是一种密封类,它具有自动生成的构造函数、getter 和 setter 方法,以及 toString()equals() 方法。数据类通常用于表示数据对象。
  • 接口:接口是一种密封类,它只能包含抽象方法。接口通常用于定义一组方法,这些方法必须由实现该接口的类实现。

数据类

数据类(data class)是 Kotlin 中定义数据的一种方式,它提供了简洁的语法和许多有用的功能。数据类与普通类相似,但具有以下特点:

  • 数据类的主构造函数必须包含至少一个参数。
  • 数据类的主构造函数的参数必须都是 val 或 var 类型的。
  • 数据类的主构造函数的参数必须都是不可变的类型。
  • 数据类的主构造函数的参数不能有默认值。
  • 数据类的主构造函数的参数不能是可变参数。
  • 数据类的主构造函数的参数不能是 Lambda 表达式。

数据类提供了以下功能:

  • 自动生成的 equals() 和 hashCode() 方法。
  • 自动生成的 toString() 方法。
  • 自动生成的 copy() 方法。
  • 自动生成的 componentN() 方法。

数据类非常适合用于表示具有多个属性的对象,例如 Person、Address、Car 等。

以下是一个数据类的例子:

data class Person(val name: String, val age: Int)

这个数据类表示一个人,它具有两个属性:name 和 age。

我们可以使用数据类来创建对象:

val person = Person("John", 30)

我们可以使用数据类的属性来访问对象的数据:

println(person.name) // John
println(person.age) // 30

我们可以使用数据类的 copy() 方法来创建新的对象:

val newPerson = person.copy(age = 31)

这个新的对象与 person 对象相同,但 age 属性的值为 31。

我们可以使用数据类的 componentN() 方法来访问对象的数据:

val (name, age) = person

这与以下代码相同:

val name = person.name
val age = person.age

数据类是 Kotlin 中定义数据的一种非常方便的方式。它们提供了简洁的语法和许多有用的功能。

伴生对象 companion object

伴生对象(companion object)是 Kotlin 中的一个特殊类,它与类本身紧密相关,并且可以访问类本身的私有成员。伴生对象通常用于定义静态方法和属性,这些方法和属性与类本身相关,但并不属于任何特定实例。

要声明一个伴生对象,您可以在类中使用 companion object 关键字,如下所示:

class MyClass {
    companion object {
        // 伴生对象的内容
    }
}

伴生对象可以访问类本身的私有成员,包括私有构造函数。这使得伴生对象非常适合用于初始化类本身的私有状态,或者提供对类本身私有成员的访问。

伴生对象还可以定义静态方法和属性,这些方法和属性与类本身相关,但并不属于任何特定实例。例如,您可以使用伴生对象来定义一个工厂方法,该方法可以创建该类的实例,如下所示:

class MyClass {
    companion object {
        fun create(): MyClass {
            // 创建 MyClass 的实例并返回
        }
    }
}

val instance = MyClass.create()

伴生对象还可以定义静态属性,这些属性与类本身相关,但并不属于任何特定实例。例如,您可以使用伴生对象来定义一个常量,该常量包含类的版本号,如下所示:

class MyClass {
    companion object {
        const val VERSION = "1.0.0"
    }
}

println(MyClass.VERSION) // 输出: 1.0.0

最佳实践:

  • 使用伴生对象来定义静态方法和属性,这些方法和属性与类本身相关,但并不属于任何特定实例。
  • 使用伴生对象来初始化类本身的私有状态,或者提供对类本身私有成员的访问。
  • 不要在伴生对象中定义与特定实例相关的方法或属性。

泛型

kotlin的泛型介绍

Kotlin 的泛型设计让你的代码更加灵活和复用性更高。它允许你定义一个工作于多种类型上的类、接口或方法。这里是一些关键概念和它们的应用方式:

1. 泛型类和接口

在 Kotlin 中,你可以创建泛型类或接口,这样它们就可以用不同的数据类型实例化。这是通过在类或接口名后添加尖括号(<>)和类型参数来实现的。比如:

class Box<T>(t: T) {
    var value = t
}

这里,T是一个类型参数,可以在创建Box类的实例时替换成任何类型。

2. 泛型函数

函数也可以是泛型的,方法类似。类型参数位于函数名之前:

fun <T> singletonList(item: T): List<T> {
    return listOf(item)
}

这个例子中的singletonList函数可以用任何类型的item调用,并返回包含该项的List

3. 泛型约束

Kotlin 允许你限制泛型类型参数必须继承自特定的父类型。这是通过where关键字实现的:

fun <T> append(item: T, list: List<T>) where T : CharSequence, T : Appendable {
    // ...
}

这个例子中,T必须同时是CharSequenceAppendable的子类型。

4. 类型擦除和型变

  • 类型擦除:Kotlin 的泛型在运行时类型信息被擦除。这意味着在运行时你不能直接查询泛型的具体类型。
  • 型变:Kotlin 通过使用in(逆变)和out(协变)关键字处理泛型的型变。out使得一个泛型类可以安全地作为其类型参数的超类型使用,而in则相反。

5. 星号投影(Star Projection)

星号投影是一种特殊的类型参数,用于表示你不关心具体的类型参数。例如,如果你有一个List<*>,这表示你对列表中元素的具体类型不感兴趣。

通过这些机制,Kotlin 的泛型提供了代码复用和类型安全性的强大工具,同时保持了灵活性和表达力。

基础概念

泛型类型参数:在定义类、接口或函数时,你可以声明一个或多个类型参数,使其能够处理不同的数据类型。这些类型参数在使用时会被实际的类型替换。

使用示例

class Box<T>(t: T) {
    var value = t
}

val box: Box<Int> = Box(1) // 指定T为Int类型

泛型约束

你可以限制泛型类型参数必须继承自特定的父类型,这称为泛型约束。Kotlin 使用 : 来指定约束。

约束示例

fun <T: Number> List<T>.sum(): T {
    // ...
}

这个函数限制了泛型类型 T 必须是 Number 或其子类型。

类型擦除

Kotlin 的泛型在运行时会进行类型擦除,这意味着泛型信息不会在运行时保留。因此,你不能直接查询一个泛型的运行时类型,但是可以通过其他方式(例如传递类引用)来间接获取这些信息。

星号投影(Star Projection)

有时候,你可能不关心泛型的具体类型参数,只是想表达某种泛型无论其类型参数如何都可以接受。这时,可以使用星号投影。

示例

fun printList(list: List<*>) {
    list.forEach { println(it) }
}

变型:in、out

Kotlin 中的泛型变型通过关键字 inout 来控制泛型类型的协变和逆变。

  • 协变(out):如果类的泛型类型参数只作为输出,可以使用 out 关键字。这意味着子类型关系将被保留。
  • 逆变(in):如果类的泛型类型参数只作为输入,可以使用 in 关键字。逆变操作反转了子类型关系。

变型示例

class Producer<out T> { /*...*/ }
class Consumer<in T> { /*...*/ }

Kotlin 的泛型系统既强大又灵活,通过合理使用,可以编写出既类型安全又易于维护的代码。

定义变量和常量

kotlin 定义变量和常量

变量

变量用于存储可以更改的值。要定义变量,请使用 var 关键字,后跟变量的名称和类型。例如:

var name = "John Doe"

这将创建一个名为 name 的变量,并将其初始化为字符串 "John Doe"

不可变变量

不可变变量是使用val关键字来定义的。一旦给这样的变量分配了一个值,就不能再更改这个值。换句话说,这种类型的变量是只读的。

这里有一个简单的例子来演示如何声明和使用不可变变量:

fun main() {
    val name = "John" // 声明一个不可变变量并初始化为"John"
    println(name) // 输出: John

    // 下面这行将会引发编译时错误,因为我们试图修改一个不可变的变量
    // name = "Doe"
}

在上面的例子中,我们创建了一个名为name的不可变变量,并将其初始化为字符串"John"。尝试更改name的值将会导致编译错误,因为val关键字表示该变量是只读的。

使用不可变性是函数式编程范式中的一个重要概念。它有助于创建更安全、更易于维护和并发友好的代码。通过限制对数据结构内容修改的能力,可以降低程序中出现意外和难以跟踪错误的风险。此外,在多线程环境下,由于数据本身不会改变,所以读取操作无需额外同步措施,从而简化了并发程序设计。

在Kotlin中,默认推荐先考虑使用不可变对象(通过val定义),除非有特定需求需要修改对象状态才使用可变对象(通过var定义)。

常量

常量用于存储不能更改的值。在kotlin中要定义常量,需要在类的顶级或者伴生对象中才可定义

class Test {  
    companion object {  
        const val HELLO = "hello"  
    }  
}

这将创建一个名为 PI 的常量,并将其初始化为数字 3.141592653589793

类型推断

Kotlin 支持类型推断,这意味着您不必显式指定变量或常量的类型。编译器将从变量或常量的值推断出类型。例如:

var name = "John Doe"
val PI = 3.141592653589793

编译器将推断 name 的类型为 StringPI 的类型为 Double

可变性和不可变性

变量是可变的,这意味着它们的值可以更改。常量是不可变的,这意味着它们的值不能更改。

范围

变量和常量的范围是指它们可以在程序中使用的位置。局部变量只能在定义它们的函数或代码块中使用。全局变量可以在程序的任何地方使用。

初始化

变量和常量必须在使用前初始化。变量可以通过赋值运算符(=)初始化。常量可以通过 val 关键字或 const 关键字初始化。

const 关键字用于初始化编译时常量。编译时常量在编译时计算,并且不能在运行时更改。

使用

变量和常量可以使用其名称访问。例如:

println(name)
println(PI)

这将输出:

John Doe
3.141592653589793

对象定义

kotlin 的对象定义通过 var 关键字指定格式为 var [对象名称]:[对象显示类型指定] = [对象值]

// 类定义
class MyClass {
   // 类成员变量
   var name: String = "John Doe"
   var age: Int = 25

   // 类成员函数
   fun sayHello() {
       println("Hello, my name is $name and I am $age years old.")
   }
}

// 对象定义
val myObject = MyClass()

// 访问类成员变量
println(myObject.name) // John Doe

// 访问类成员函数
myObject.sayHello() // Hello, my name is John Doe and I am 25 years old.

kotlin 的多线程

多线程创建

Kotlin 是一种支持多线程的现代编程语言。多线程允许程序同时执行多个任务,从而提高程序的效率和性能。Kotlin 提供了多种方式来创建和管理线程,包括:

  • 协程 (Coroutine):协程是 Kotlin 中的一种轻量级线程,它可以暂停和恢复执行。协程可以轻松地创建和管理,并且它们不会阻塞线程。
  • 线程 (Thread):线程是操作系统中的一个执行单元,它可以独立于其他线程运行。线程可以创建和管理,但它们比协程更重量级,并且它们可能会阻塞线程。
  • 并发 (Concurrency):并发是指多个任务同时执行。Kotlin 提供了多种并发库,可以帮助您编写并发程序。

以下是一些使用 Kotlin 进行多线程编程的示例:

// 创建一个协程
val coroutine = launch {
    // 执行协程中的代码
}

// 创建一个线程
val thread = Thread {
    // 执行线程中的代码
}

// 使用并发库编写并发程序
val executorService = Executors.newFixedThreadPool(4)
executorService.submit {
    // 执行并发任务
}

Kotlin 的多线程特性可以帮助您编写高效、高性能的程序。如果您需要在程序中同时执行多个任务,那么可以使用 Kotlin 的多线程特性来实现。

结构化并发

协程

协程是一种轻量级的并发原语,它允许您在单个线程中暂停和恢复多个任务。这意味着您可以编写并发代码,而无需担心线程管理和同步。

异步编程

异步编程是一种编程范式,它允许您在不阻塞当前线程的情况下执行任务。这意味着您可以同时执行多个任务,而无需等待每个任务完成。

通道

通道是一种通信机制,它允许协程之间交换数据。通道可以是单向或双向的,并且可以缓冲数据。

选择器

选择器是一种机制,它允许协程等待多个通道上的事件。当某个通道上有事件发生时,选择器会通知协程,以便协程可以处理该事件。

并发库

Kotlin 提供了丰富的并发库,包括协程、异步编程、通道和选择器。这些库可以帮助您编写高效且可扩展的并发代码。

示例

以下是一个使用协程和通道实现的简单并发程序:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val channel = Channel<Int>()

    launch {
        for (i in 1..10) {
            channel.send(i)
        }
        channel.close()
    }

    for (value in channel) {
        println(value)
    }
}

这个程序使用协程来同时执行两个任务:发送数据和接收数据。协程 launch 用于发送数据,协程 for 用于接收数据。通道 channel 用于在两个协程之间交换数据。

优点

Kotlin 的结构性并发具有以下优点:

  • 易于使用:Kotlin 的并发库易于使用,即使您是并发编程的新手,也可以轻松上手。
  • 高效:Kotlin 的并发库非常高效,可以帮助您编写高性能的并发程序。
  • 可扩展:Kotlin 的并发库非常可扩展,可以帮助您编写可扩展到大量并发任务的程序。

缺点

Kotlin 的结构性并发也有一些缺点:

  • 学习曲线陡峭:Kotlin 的并发库学习曲线陡峭,需要花费一些时间才能掌握。
  • 可能难以调试:并发程序可能难以调试,尤其是当您不熟悉并发编程时。

结论

Kotlin 的结构性并发非常强大,可以帮助您编写高效且可扩展的并发程序。但是,Kotlin 的并发库学习曲线陡峭,可能难以调试。如果您是并发编程的新手,建议您先学习一些并发编程的基本知识,然后再使用 Kotlin 的并发库。

launch 和 async 的区别

launchasync 都是 Kotlin 协程中的函数,用于在协程中执行代码块。它们的主要区别是 async 返回一个 Deferred<T> 对象,而 launch 不返回任何值。

launch 函数用于在协程中执行代码块,但不关心代码块的返回值。例如,以下代码使用 launch 函数在协程中打印一条消息:

GlobalScope.launch {
    println("Hello, world!")
}

async 函数用于在协程中执行代码块,并返回代码块的返回值。例如,以下代码使用 async 函数在协程中计算一个数的平方,并返回结果:

val result = GlobalScope.async {
    5 * 5
}

launchasync 函数都可以使用 await() 函数来等待协程执行完成。例如,以下代码使用 await() 函数来等待 async 函数执行完成,并打印结果:

val result = GlobalScope.async {
    5 * 5
}

println(result.await())

launchasync 函数的主要区别如下:

  • launch 函数不返回任何值,而 async 函数返回一个 Deferred<T> 对象。
  • launch 函数不能使用 await() 函数来等待协程执行完成,而 async 函数可以使用 await() 函数来等待协程执行完成。

launchasync 函数都可以用于在协程中执行代码块。launch 函数适用于不关心代码块返回值的情况,而 async 函数适用于需要使用代码块返回值的情况。

suspend 关键字

suspend 关键字是 Kotlin 协程中使用的一种关键字,它允许协程在执行过程中暂停,并在稍后恢复执行。

协程是一种轻量级的并发原语,它允许在一个线程中执行多个任务,而无需创建和管理多个线程。协程可以使用 suspend 关键字来暂停执行,直到某个条件满足,例如等待网络请求的返回或数据库查询的结果。

以下是一些使用 suspend 关键字的示例:

// 等待网络请求的返回
suspend fun fetchUserData(userId: Int): User {
    val response = HttpClient.get("https://example.com/users/$userId")
    return response.body()
}

// 等待数据库查询的结果
suspend fun findUserByName(name: String): User {
    val query = "SELECT * FROM users WHERE name = ?"
    val statement = db.compileStatement(query)
    statement.bindString(1, name)
    val resultSet = statement.executeQuery()
    return resultSet.firstOrNull()?.toUser()
}

在上面的示例中,fetchUserDatafindUserByName 函数都被标记为 suspend 函数,这意味着它们可以在协程中调用。当这些函数在协程中调用时,协程会暂停执行,直到函数返回结果。

suspend 关键字还可以用于挂起函数,即在函数执行过程中暂停执行,直到某个条件满足。例如,以下代码使用 suspend 关键字来挂起函数,直到通道 channel 中有数据可用:

suspend fun receiveData(): Int {
    return channel.receive()
}

receiveData 函数在协程中调用时,协程会暂停执行,直到 channel 中有数据可用。当数据可用时,协程会恢复执行并返回数据。

总结:

  • suspend 关键字允许协程在执行过程中暂停,并在稍后恢复执行。
  • suspend 函数可以在协程中调用,当 suspend 函数在协程中调用时,协程会暂停执行,直到函数返回结果。
  • suspend 关键字还可以用于挂起函数,即在函数执行过程中暂停执行,直到某个条件满足。

runBlocking 的作用

runBlocking 是 Kotlin 协程库中一个重要的函数,它允许您在协程作用域之外执行协程。

协程是一种并发编程的工具,它允许您将一个任务分解成多个子任务,并在多个线程上同时执行这些子任务。协程与线程不同之处在于,协程是轻量级的,并且可以在同一个线程上切换执行,这使得协程的开销非常小。

runBlocking 函数的作用是创建一个协程作用域,并在该作用域内执行指定的协程。在协程作用域之外,您不能直接执行协程,因为协程需要在协程作用域内才能运行。

runBlocking 函数的语法如下:

fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> Unit) {
    val newContext = context + CoroutineName("runBlocking")
    val coroutine = newCoroutine(newContext, block)
    coroutine.join()
}

其中,context 是协程作用域的上下文,block 是要在协程作用域内执行的协程。

runBlocking 函数的用法非常简单,您只需要在要执行协程的地方调用 runBlocking 函数,并在函数体内指定要执行的协程即可。

例如,以下代码演示了如何使用 runBlocking 函数执行一个协程:

runBlocking {
    // 在协程作用域内执行的代码
    val result = async {
        // 在子协程中执行的代码
        delay(1000)
        return@async "Hello, world!"
    }
    val message = result.await()
    println(message)
}

这段代码首先调用 runBlocking 函数创建了一个协程作用域,然后在协程作用域内执行了一个协程。协程首先调用 async 函数创建一个子协程,然后在子协程中执行 delay 函数让子协程暂停 1 秒,然后返回 “Hello, world!”。最后,协程调用 result.await() 函数等待子协程完成,然后将子协程的返回值打印到控制台。

runBlocking 函数是一个非常有用的函数,它允许您在协程作用域之外执行协程。这使得您可以在任何地方使用协程,而无需担心协程作用域的问题。

并发作用域

Kotlin 中的并发作用域是一种组织和管理协程的机制。它允许你在一个作用域内启动和管理一组协程,并为这些协程提供一个公共的上下文。

协程作用域可以让你在代码中显式地定义协程的执行范围,并控制协程的启动、取消和异常处理。这可以使你的代码更易于阅读、理解和维护。

Kotlin 提供了多种内置的并发作用域,包括:

  • GlobalScope: 全局作用域,可以在任何地方启动协程。
  • CoroutineScope: 由协程构建器函数(如 launchasync) 创建的作用域,用于在协程内部启动新的协程。
  • SupervisorJob: 用于创建子协程不会影响父协程的作用域。
  • plus: 用于组合多个作用域。

你还可以使用 CoroutineScope 接口创建自定义的并发作用域。

以下是一些使用并发作用域的示例:

// 使用 GlobalScope 启动一个新的协程
GlobalScope.launch {
    // ...
}

// 使用 CoroutineScope 启动一个新的协程
fun myCoroutineScope(): CoroutineScope {
    return CoroutineScope(SupervisorJob())
}

fun main() {
    val scope = myCoroutineScope()
    scope.launch {
        // ...
    }
    // 使用 coroutineScope() {} 创建
	coroutineScope {
			
					}
}

并发作用域还可以用于控制协程的取消和异常处理。例如,你可以使用 SupervisorJob 创建一个作用域,使得子协程不会影响父协程。如果子协程抛出异常,父协程将继续执行。

以下是一个使用 SupervisorJob 的示例:

val scope = CoroutineScope(SupervisorJob())
scope.launch {
    try {
        // ...
    } catch (e: Exception) {
        // 处理异常
    }
}

scope.launch {
    // ...
}

在上面的示例中,即使第一个协程抛出异常,第二个协程也会继续执行。

总结:

Kotlin 中的并发作用域是一种组织和管理协程的机制。它允许你在代码中显式地定义协程的执行范围,并控制协程的启动、取消和异常处理。这可以使你的代码更易于阅读、理解和维护。

kotlin 的空判断

1. 使用 ==!= 运算符

最简单的方法是使用 ==!= 运算符来判断变量是否为 null

if (variable == null) {
    // variable is null
} else {
    // variable is not null
}

2. 使用 ?: 运算符

?: 运算符可以用来将一个变量的值赋给另一个变量,如果第一个变量为 null,则将第二个变量的值赋给第一个变量。

val value = variable ?: "default value"

3. 使用 ?. 运算符

?. 运算符可以用来安全地访问对象的属性或调用对象的方法。如果对象为 null,则不会访问属性或调用方法,而是返回 null

val length = variable?.length

4. 使用 Elvis 运算符

Elvis 运算符 (?:) 可以用来将一个变量的值赋给另一个变量,如果第一个变量为 null,则将第二个变量的值赋给第一个变量。

val value = variable ?: "default value"

5. 使用 when 表达式

when 表达式可以用来匹配变量的值,并执行不同的代码。

when (variable) {
    null -> {
        // variable is null
    }
    else -> {
        // variable is not null
    }
}

6. 使用 if 表达式

if 表达式可以用来判断变量是否为 null,并执行不同的代码。

val value = if (variable != null) {
    variable
} else {
    "default value"
}

kotlin 的! 和 !!

  • !(非空断言运算符):! 用来表示一个变量或表达式一定是非空的,并且不会抛出 NullPointerException。如果变量或表达式为 null,那么 ! 会抛出 NullPointerException。例如:
val name: String? = "Kotlin"
val length = name!!.length // 6
  • !!(非空断言运算符):!!! 类似,但它更具安全性。!! 会在运行时检查变量或表达式是否为 null,如果为 null,则会抛出 IllegalStateException。例如:
val name: String? = null
val length = name!!?.length // null

!! 通常用于那些你确信不会为 null 的变量或表达式,例如在构造函数中初始化的属性。

!!! 的主要区别在于:

  • ! 会在编译时检查变量或表达式是否为 null,!! 会在运行时检查。
  • ! 会抛出 NullPointerException!! 会抛出 IllegalStateException

!!! 都可以用于非空断言,但 !! 更具安全性。

kotlin 的 when

when 表达式是 kotlin 中用于多路选择语句的替代。它类似于 Java 中的 switch 语句,但更灵活,因为它可以处理任意数量的条件,并且可以使用范围和模式匹配。

when 表达式的基本语法如下:

when (expression) {
    condition1 -> result1
    condition2 -> result2
    ...
    else -> resultN
}

其中,expression 是要评估的表达式,condition 是要检查的条件,result 是要执行的代码。

when 表达式支持以下类型的条件:

  • 常量值: 可以使用常量值作为条件,例如:
when (x) {
    1 -> println("x is 1")
    2 -> println("x is 2")
    3 -> println("x is 3")
    else -> println("x is not 1, 2, or 3")
}
  • 范围: 可以使用范围作为条件,例如:
when (x) {
    in 1..10 -> println("x is between 1 and 10")
    in 11..20 -> println("x is between 11 and 20")
    else -> println("x is not between 1 and 20")
}
  • 模式匹配: 可以使用模式匹配作为条件,例如:
when (x) {
    is String -> println("x is a string")
    is Int -> println("x is an integer")
    is Double -> println("x is a double")
    else -> println("x is not a string, integer, or double")
}

when 表达式还支持以下类型的结果:

  • 代码块: 可以使用代码块作为结果,例如:
when (x) {
    1 -> {
        println("x is 1")
        y++
    }
    2 -> {
        println("x is 2")
        z--
    }
    else -> {
        println("x is not 1 or 2")
    }
}
  • 表达式: 可以使用表达式作为结果,例如:
when (x) {
    1 -> y++
    2 -> z--
    else -> 0
}

when 表达式还支持以下类型的操作符:

  • ==: 等于号操作符用于比较两个值是否相等,例如:
when (x) {
    1 == y -> println("x is equal to y")
    else -> println("x is not equal to y")
}
  • !=: 不等于号操作符用于比较两个值是否不相等,例如:
when (x) {
    1 != y -> println("x is not equal to y")
    else -> println("x is equal to y")
}
  • <: 小于号操作符用于比较两个值是否小于,例如:
when (x) {
    x < y -> println("x is less than y")
    else -> println("x is not less than y")
}
  • <=: 小于等于号操作符用于比较两个值是否小于等于,例如:
when (x) {
    x <= y -> println("x is less than or equal to y")
    else -> println("x is not less than or equal to y")
}
  • >: 大于号操作符用于比较两个值是否大于,例如:
when (x) {
    x > y -> println("x is greater than y")
    else -> println("x is not greater than y")
}
  • >=: 大于等于号操作符用于比较两个值是否大于等于,例如:
when (x) {
    x >= y -> println("x is greater than or equal to y")
    else -> println("x is not greater than or equal to y")
}

when 表达式是一个非常灵活和强大的工具,可以用于处理各种各样的条件和结果。它比 Java 中的 switch 语句更灵活,因为它可以处理任意数量的条件,并且可以使用范围和模式匹配。

kotlin 的进阶特性

集合与 vararg 的转换

集合与 vararg 的转换可以通过使用 toTypedArray()toList() 函数来完成。

如果有一个集合,想要将其转换为 vararg 参数,可以使用 toTypedArray() 函数。这个函数会返回一个数组,其中包含集合的所有元素。例如:

val list = listOf("a", "b", "c")
val array = list.toTypedArray()

如果有一个 vararg 参数,想要将其转换为集合,可以使用 toList() 函数。这个函数会返回一个包含 vararg 参数的新列表。例如:

fun printList(list: List<String>) {
    println(list)
}

printList(*array)

在上面的代码中,* 运算符用于将数组展开为 vararg 参数。

总结起来,集合与 vararg 的转换可以通过以下方式完成:

  • 将集合转换为 vararg 参数:使用 toTypedArray() 函数。
  • 将 vararg 参数转换为集合:使用 toList() 函数,并使用 * 运算符展开数组。

kotlin 中通过参数名称传递参数

在 kotlin 中,我们有一个如下的函数

fun test(a: Int = 1, vararg list: String) {  
    println(a)  
    list.forEach {  
        println(it)  
    }  
}

当我们在调用的时候,我们对默认值的传参并不关心,当我只需要为 b 传参时,就会出现以下问题
image.png

可以发现,我们在当第一位参数有默认值,且我们并不关心他的传参的时候,会出现我们的参数传递不到指定的参数上的问题,这个时候就需要使用到 kotlin 的特性,根据属性名传递参数的特性了

在 Kotlin 中,可以使用扩展函数来实现根据属性名传递参数的功能。以下是如何实现的步骤:

  1. 创建一个扩展函数,该函数接受一个对象和一个参数名作为参数,并返回该对象中具有该参数名的属性值。
fun <T> T.getProperty(propertyName: String): Any? {
    val property = this::class.java.getDeclaredField(propertyName)
    property.isAccessible = true
    return property.get(this)
}
  1. 使用扩展函数来获取对象中具有指定参数名的属性值。
val person = Person("John", 30)
val name = person.getProperty("name")
val age = person.getProperty("age")
  1. 将扩展函数与参数名一起传递给另一个函数。
fun printPersonInfo(person: Person, propertyName: String) {
    val propertyValue = person.getProperty(propertyName)
    println("$propertyName: $propertyValue")
}

printPersonInfo(person, "name")
printPersonInfo(person, "age")

这种方法的好处是,它可以使代码更加简洁和可读。此外,它还可以提高代码的可维护性,因为当需要更改参数名时,只需要更改扩展函数中的代码即可。

所以我们这里的具体调用方式就变成了

var params = arrayOf("1", "2", "3", "4")  
test(list = params)

运算符重载

在 Kotlin 中可以为类型提供预定义的一组操作符的自定义实现。这些操作符具有预定义的符号表示(如 + 或 *)与优先级。为了实现这样的操作符,需要为相应的类型提供一个指定名称的成员函数扩展函数。这个类型会成为二元操作符左侧的类型及一元操作符的参数类型。

要重载运算符,请使用 OPERATOR 修饰符标记相应的函数:

interface IndexedContainer {
    operator fun get(index: Int)
}

中缀函数

在Kotlin中,中缀函数是一种特殊的函数调用语法,可以使函数调用更加简洁和可读。中缀函数需要满足以下条件:

  1. 函数必须是成员函数或扩展函数。
  2. 函数必须只有一个参数。
  3. 函数的参数必须是无参数的,即不能有默认值。

定义中缀函数需要在函数前面加上infix关键字。例如:

infix fun String.isEqualTo(other: String) = this == other

该代码定义了一个名为isEqualTo的中缀函数,它接受一个字符串参数,并返回两个字符串是否相等。

使用中缀函数时,可以省略点号和括号,直接将它放在两个对象之间。例如:

val result = "hello" isEqualTo "world"

这样就能够判断两个字符串是否相等了。

需要注意的是,虽然中缀函数可以使代码更加简洁和可读,但滥用它可能会降低代码的可读性。因此,在使用中缀函数时应谨慎考虑是否真正需要使用它们。中缀is须是

以下是一些其他中缀函数的示例:

  • String.plus():将两个字符串连接在一起。
  • List.plus():将两个列表连接在一起。
  • Int.compareTo():比较两个整数的大小。
  • Double.times():将一个数字乘以另一个数字。

您可以使用中缀表示法来调用这些中缀函数。例如,以下代码将字符串 “Hello” 和 “World” 连接在一起:

val result = "Hello" + "World"

println(result) // 输出: HelloWorld

中缀表示法是一种非常方便的语法,可以使您的代码更简洁和更易读。

内联函数

Kotlin 的内联函数(inline functions)是一种在编译时期将函数体代码“内联”到调用处的特性,这意味着编译器会将内联函数的代码直接替换到每一个调用点,而不是正常的函数调用(即通过栈进行调用)。这个特性对于减少运行时开销尤其有用,特别是在使用高阶函数(如函数作为参数或返回值的函数)时。

优点:

  • 减少运行时开销:由于不需要通过栈进行函数调用,可以减少额外的开销。
  • 优化性能:对于小的函数,内联可以避免函数调用的开销,有助于提高性能。
  • 更好的Lambda表达式支持:内联函数允许lambda表达式在编译时被内联,这对于Kotlin中的高阶函数非常有用,因为它可以避免创建匿名类实例并减少内存使用。

缺点:

  • 增加编译后的代码量:如果一个内联函数在多处被调用,那么每个调用点都会被替换成函数体的代码,这可能会增加最终的代码量。
  • 滥用可能导致性能问题:如果过度使用内联功能,尤其是在大型函数上,可能会导致编译后的代码膨胀,反而降低性能。

如何使用:
在 Kotlin 中,你可以通过在函数声明前添加 inline 关键字来创建内联函数。例如:

inline fun <T> measureTime(block: () -> T): T {
    val start = System.nanoTime()
    val result = block()
    val end = System.nanoTime()
    println("Time taken: ${(end - start) / 1_000_000} ms")
    return result
}

上述代码反编译的结果:

public final class InlineKt {
   public static final Object measureTime(@NotNull Function0 block) {
      int $i$f$measureTime = 0;
      Intrinsics.checkNotNullParameter(block, "block");
      long start = System.nanoTime();
      Object result = block.invoke();
      long end = System.nanoTime();
      String var7 = "Time taken: " + (end - start) / (long)1000000 + " ms";
      System.out.println(var7);
      return result;
   }

   public static final void main() {
      Function0 result = (Function0)null.INSTANCE;
      int $i$f$measureTime = false;
      // 可以看到,measureTime方法的方法体都被内敛到了调用出
      long start$iv = System.nanoTime();
      Object result$iv = result.invoke();
      long end$iv = System.nanoTime();
      String var7 = "Time taken: " + (end$iv - start$iv) / (long)1000000 + " ms";
      System.out.println(var7);
      System.out.println(result$iv);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

在上面的例子中,measureTime 函数接受一个 lambda 表达式作为参数,并计算执行该 lambda 所需的时间。因为 measureTime 函数被标记为内联,所以它的调用会在编译时被替换成具体的计时逻辑和 lambda 表达式的代码,从而避免了函数调用的开销。

  • 对于小而频繁调用的函数,尤其是那些作为参数接受 lambda 表达式的函数,考虑使用内联。
  • 避免将大型函数标记为内联,因为这可能会导致编译后的代码膨胀。
  • 内联不仅限于函数,Kotlin 的 inline 关键字也可用于内联属性和类。

传入的参数不想被内联时

一个高阶函数一旦被标记为内联,它的方法体和所有 Lambda 参数都会被内联。

inline fun test(block1: () -> Unit, block2: () -> Unit) {
    block1()
    println("xxx")
    block2()
}

test() 函数被标记为了 inline  ,所以它的函数体以及两个 Lambda 参数都会被内联。但是由于我要传入的 block1  代码块巨长(或者其他原因),我并不想将其内联,这时候就要使用 noinline  。

inline fun test(noinline block1: () -> Unit, block2: () -> Unit) {
    block1()
    println("xxx")
    block2()
}

这样, block1 就不会被内联了。篇幅原因,这里就不展示 Java 代码了,相信你也能很容易理解 noinline

扩展函数

Kotlin 的扩展函数是一种强大的特性,允许你向一个已经存在的类添加新的方法,而不需要修改其源代码或者使用继承。这是一种在不直接修改类定义的情况下,扩展类功能的优雅方式。扩展函数可以在任何类上定义,包括库中的类或者你自己定义的类。

定义扩展函数

要定义一个扩展函数,你需要使用以下语法:

fun ClassName.extensionFunctionName(parameters): ReturnType {
    // 函数体
}

其中 ClassName 是你想要扩展的类的名称,extensionFunctionName 是你给这个扩展函数起的名字,parameters 是函数的参数(如果有的话),ReturnType 是函数返回值的类型。

示例

假设你有一个 String 类型的数据,你想要添加一个扩展函数来判断字符串是否是回文字符串(正读和反读都相同)。

fun String.isPalindrome(): Boolean {
    return this == this.reversed()
}

现在,你可以在任何 String 对象上调用 .isPalindrome() 方法来检查它是否是回文字符串。

val example = "madam"
println(example.isPalindrome()) // 输出:true

扩展函数的作用域

扩展函数只在它被定义的作用域内可见。通常,你会在顶层定义扩展函数,这样它就在整个包内可见。如果你只想在某个特定的文件或类内使用这个扩展函数,你可以将其定义在文件或类的内部。

注意点

  • 扩展函数并不真正修改它扩展的类。它们通过静态解析的方式在调用点被解析。
  • 在扩展函数内部,你可以通过 this 关键字来访问接收者对象(即调用该扩展函数的对象)。
  • 扩展函数不能访问接收者类的私有或者受保护的成员。

示例

fun String.lastChar(): Char {  
    return this[this.length - 1]  
}  
  
fun main() {  
    println("Kotlin".lastChar())  
}

反编译的结果:

public final class ExtensionKt {
   public static final char lastChar(@NotNull String $this$lastChar) {
      Intrinsics.checkNotNullParameter($this$lastChar, "$this$lastChar");
      return $this$lastChar.charAt($this$lastChar.length() - 1);
   }

   public static final void main() {
      char var0 = lastChar("Kotlin");
      System.out.println(var0);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

可以看到,

一元操作

一元前缀操作符

表达式 翻译为
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

这个表是说,当编译器处理例如表达式 +a 时,它执行以下步骤:

  • 确定 a 的类型,令其为 T
  • 为接收者 T 查找一个带有 operator 修饰符的无参函数 unaryPlus(),即成员函数或扩展函数。
  • 如果函数不存在或不明确,则导致编译错误。
  • 如果函数存在且其返回类型为 R,那就表达式 +a 具有类型 R

这些操作以及所有其他操作都针对基本类型做了优化,不会为它们引入函数调用的开销。

以下是如何重载一元减运算符的示例:

data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus() = Point(-x, -y)

val point = Point(10, 20)

fun main() {
   println(-point)  // 输出“Point(x=-10, y=-20)”
}

递增与递减

表达式 翻译为
a++ a.inc() + 见下文
a-- a.dec() + 见下文

inc() 和 dec() 函数必须返回一个值,它用于赋值给使用 ++ 或 -- 操作的变量。它们不应该改变在其上调用 inc() 或 dec() 的对象。

编译器执行以下步骤来解析_后缀_形式的操作符,例如 a++

  • 确定 a 的类型,令其为 T
  • 查找一个适用于类型为 T 的接收者的、带有 operator 修饰符的无参数函数 inc()
  • 检测函数的返回类型是 T 的子类型。

计算表达式的步骤是:

  • 把 a 的初始值存储到临时存储 a0 中。
  • 把 a0.inc() 结果赋值给 a
  • 把 a0 作为该表达式的结果返回。

对于 a--,步骤是完全类似的。

对于_前缀_形式 ++a 和 --a 以相同方式解析,其步骤是:

  • 把 a.inc() 结果赋值给 a
  • 把 a 的新值作为该表达式结果返回。

二元操作

算术运算符

表达式 翻译为
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a..b a.rangeTo(b)
a..<b a.rangeUntil(b)

对于此表中的操作,编译器只是解析成_翻译为_列中的表达式。

下面是一个从给定值起始的 Counter 类的示例,它可以使用重载的 + 运算符来增加计数:

data class Counter(val dayIndex: Int) {
    operator fun plus(increment: Int): Counter {
        return Counter(dayIndex + increment)
    }
}

in 操作符

表达式 翻译为
a in b b.contains(a)
a !in b !b.contains(a)

对于 in 和 !in,过程是相同的,但是参数的顺序是相反的。

索引访问操作符

表达式 翻译为
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ……, i_n] a.get(i_1, ……, i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ……, i_n] = b a.set(i_1, ……, i_n, b)

方括号转换为调用带有适当数量参数的 get 和 set

invoke 操作符

表达式 翻译为
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ……, i_n) a.invoke(i_1, ……, i_n)

圆括号转换为调用带有适当数量参数的 invoke

广义赋值

表达式 翻译为
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)

对于赋值操作,例如 a += b,编译器执行以下步骤:

  • 如果右列的函数可用:
    • 如果相应的二元函数(即 plusAssign() 对应于 plus())也可用, a is a mutable variable, and the return type of plus is a subtype of the type of a, 那么报告错误(无法区分)。
    • 确保其返回类型是 Unit,否则报告错误。
    • 生成 a.plusAssign(b) 的代码。
  • 否则试着生成 a = a + b 的代码(这里包含类型检测:a + b 的类型必须是 a 的子类型)。

赋值在 Kotlin 中_不是_表达式。

相等与不等操作符

表达式 翻译为
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

这些操作符只使用函数 equals(other: Any?): Boolean, 可以覆盖它来提供自定义的相等性检测实现。不会调用任何其他同名函数(如 equals(other: Foo))。

=== 和 !==(同一性检测)不可重载,因此不存在对他们的约定。

这个 == 操作符有些特殊:它被翻译成一个复杂的表达式,用于筛选 null 值。 null == null 总是 true,对于非空的 xx == null 总是 false 而不会调用 x.equals()

比较操作符

表达式 翻译为
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

所有的比较都转换为对 compareTo 的调用,这个函数需要返回 Int 值

属性委托操作符

provideDelegate、 getValue 以及 setValue 操作符函数已在委托属性中描述。

具名函数的中缀调用

可以通过中缀函数的调用 来模拟自定义中缀操作符。

为类添加拓展函数

// 在String类中添加一个名为"toTitleCase"的拓展函数
fun String.toTitleCase(): String {
   // 将字符串中的每个单词的首字母大写
   return this.split(" ").joinToString(" ") { it.capitalize() }
}

// 使用拓展函数
val str = "hello world"
val titleCaseStr = str.toTitleCase()

println(titleCaseStr) // 输出:Hello World

在这个例子中,我们定义了一个名为 toTitleCase() 的拓展函数,它将字符串中的每个单词的首字母大写。我们使用 split() 函数将字符串拆分为单词,然后使用 joinToString() 函数将单词重新连接起来,每个单词的首字母大写。最后,我们使用 capitalize() 函数将每个单词的首字母大写。

拓展函数可以帮助我们为现有的类添加新的功能,而无需修改类的源代码。这使得我们可以轻松地扩展现有库的功能,或者为我们的代码添加自定义功能。。

kotlin 的 lambda

Lambda 表达式(也称为匿名函数)允许您在不创建命名方法的情况下定义函数。Lambda 表达式通常用于作为函数参数传递,或者在集合上执行操作。

1. Lambda 表达式的语法

Lambda 表达式的语法如下:

{ parameters -> expression }

其中:

  • parameters 是 lambda 表达式的参数列表。
  • expression 是 lambda 表达式的函数体。

例如,以下 lambda 表达式计算两个数字的和:

{ a: Int, b: Int -> a + b }

2. 使用 Lambda 表达式

您可以将 lambda 表达式作为函数参数传递,或者在集合上执行操作。

例如,以下代码使用 lambda 表达式将集合中的每个元素加 1:

val numbers = listOf(1, 2, 3, 4, 5)
val incrementedNumbers = numbers.map { it + 1 }

println(incrementedNumbers) // 输出: [2, 3, 4, 5, 6]

3. Lambda 表达式的类型推断

Kotlin 编译器可以自动推断 lambda 表达式的类型。这意味着您不必显式地指定 lambda 表达式的类型。

例如,以下 lambda 表达式计算两个数字的和:

{ a, b -> a + b }

Kotlin 编译器会自动推断出这个 lambda 表达式的类型是 (Int, Int) -> Int

4. Lambda 表达式的最佳实践

  • 使用 lambda 表达式来简化代码。
  • 避免在 lambda 表达式中使用复杂的逻辑。
  • 使用 lambda 表达式来传递函数作为参数。
  • 使用 lambda 表达式来在集合上执行操作。

lambda 指定入参名称

val lambdaTest2: (a:Long) -> Unit = {a ->   
println(a)  
}

lambda 传入拓展函数

val lambdaTest: (init: Body.() -> Long) -> Unit = { init ->  
    var body = Body()  // 创建接收者对象
    var long = body.init()  // 将该接收者对象传给该 lambda
}

println(lambdaTest {   // 带接收者的 lambda 由此开始
    body()  // 调用该接收者对象的一个方法
})

lambda 为作用域起别名

val lambdaTest: (init: Body.() -> Long) -> Long = lambdaTest@{ init ->  // 通过name@的语法来为整个lambda函数代码块取一个别名
    var body = Body()  
    var long = body.init()  
    return@lambdaTest long  // 通过别名将返回值返回,默认的return返回的是调用方也就是main()
}

lambda 表达式在函数入参中的函数简写

入参只有一个 lambda 表达式的情况
// 当lambda只有一个入参且是lambda表达式的时候,可以直接省略()使用{}直接传入一个函数实现
val lambdaTest3: (() -> Unit) -> Unit = {  
    it()  
}  
  
lambdaTest3{  
    println("lambdaTest3")  
}
多个入参, 但是 lambda 表达式在最后一个参数的情况
val lambdaTest4: (a:Int,() -> Unit) -> Unit = {a, b ->  
    println(a)  
    b()  
}  

// 这种情况可以将lambda不写在()内,直接通过{}的方式实现函数传入
lambdaTest4(1) {  
    println("lambdaTest4")  
}

闭包

Kotlin 闭包(closures)是函数嵌套在另一个函数内部,并能够访问外部函数作用域中的变量。闭包在 Kotlin 中有广泛的应用,例如实现事件处理、创建私有函数、以及模拟对象。

以下是一个简单的 Kotlin 闭包示例:

fun main() {
    val outerVariable = 10

    val closure = {
        println("Outer variable: $outerVariable")
    }

    closure()  // 输出: Outer variable: 10
}

在这个示例中,outerVariable 是一个定义在主函数(main)作用域中的变量。closure 是一个嵌套函数,它可以访问外部函数的作用域,包括 outerVariable 变量。当调用 closure() 时,它将打印出 outerVariable 的值。

闭包的优点在于,它可以访问外部函数作用域中的变量,即使外部函数已经执行完毕。这使得闭包非常适合实现事件处理、创建私有函数、以及模拟对象。

闭包的应用

  • 事件处理: 闭包可以被用来实现事件处理。当一个事件发生时,闭包可以被调用来处理该事件。例如,一个按钮点击事件的处理函数可以是一个闭包。
  • 创建私有函数: 闭包可以被用来创建私有函数。私有函数只能在定义它们的函数内部被访问。这使得闭包非常适合实现一些只在特定函数中使用的辅助函数。
  • 模拟对象: 闭包可以被用来模拟对象。对象是一种封装了数据和行为的实体。闭包可以被用来创建具有类似行为的实体,而不需要创建一个真正的对象。

闭包的注意事项

  • 闭包会捕获外部函数作用域中的变量。 这意味着,如果外部函数作用域中的变量被修改,那么闭包也会受到影响。
  • 闭包可能会导致内存泄漏。 如果闭包捕获了对外部函数作用域中对象的引用,那么该对象可能无法被垃圾回收器回收。这可能会导致内存泄漏。
// accumulate是一个无参数的,返回"函数类型"的,函数
// "accumulate()"表示:是一个无参数的,名为accumulate的函数,记作函数A
// "() -> Unit"表示;	这是一个无参数的,返回空值Unit的,函数类型,暂记作函数B
// 将这个函数B,"() -> Unit",作为返回值,即返回一个是“函数类型”的返回值
// 在函数accumulate()中,即返回这个东东,"{ println(count++) }",这个代码块,block
// 但是注意,此时,这个block,是不会运行里面的内容的
// 因为它仍表示为一个函数类型的对象,只有在这个函数类型对象后加一个"()"
// 这个block,才会被激活
fun accumulate():() -> Unit{
    var sum = 0
    return {
        println(sum++)
    }
}

fun main(args: Array<String>) {
    // 这里定义counting = accumulate(),则counting变为函数类型
    // 函数类型后面加一个"()",即可“被激活”
    // 多次调用counting,则表示,多次调用"同一个accumulate()"
    // 所以同一个accumulate()里的sum值保留,故能够累加
    // 但是,没有被申明的accumulate(),则分别视作单独的函数
    // 不具备累加的能力
    val counting = accumulate()
    counting()  		// 输出结果:0
    counting()  		// 输出结果:1
    counting()  		// 输出结果:2
    accumulate()()		// 输出结果:0
    accumulate()()		// 输出结果:0
    accumulate()()		// 输出结果:0

    println("counting: "+counting)			
    //                 输出结果: counting: Function0<kotlin.Unit>

    println("counting(): "+counting())		
    // 先输出: 3;	   后输出:    counting(): kotlin.Unit

    println("accumulate(): "+accumulate())	
    //                 输出结果: accumulate(): Function0<kotlin.Unit>
}

优雅的 Stream API

TODO…

Scope函数

在Kotlin中,作用域函数(Scope functions)是一种非常有用的语法特性,允许你对对象执行代码块,而不需要显式地引用对象本身。Kotlin提供了几种作用域函数:let, run, with, apply, 和 also。每个函数都提供了一种不同的方式来处理对象上的操作和表达式,使代码更加简洁、易读。

  1. let函数: 使用它来执行一个或多个操作在一个对象的非空实例上。let通常用于处理可空对象及进行链式调用转换。在let的代码块内部,对象通过it引用,除非你声明了另一个名字。
val str: String? = "Hello"
str?.let {
    // 在这里,it代表str
    print(it.length)
}
  1. run函数: runlet相似,但它在对象的上下文中执行代码块,并且返回代码块的最后一个表达式的结果。对于非空对象的调用,使用run可以让你执行一个更复杂的操作序列,并且返回一个结果。
val result = "Hello".run {
    // 这里可以直接访问对象的属性和函数
    length
}
  1. with函数: with不是扩展函数,它作为一个函数参数传递对象,并执行给定的代码块。它非常适合对一个对象执行多个操作时使用。
val message = StringBuilder()
with(message) {
    append("Hello, ")
    append("world!")
    toString()
}
  1. apply函数: apply几乎与run相同,但它返回的是对象本身而不是最后的表达式结果。这非常适合进行对象的配置。
val person = Person().apply {
    name = "John"
    age = 25
}
  1. also函数: alsolet相似,但区别在于also返回的是对象本身,而不是代码块的结果。它主要用于对象的额外操作,而不改变对象本身。
val numbers = mutableListOf("One", "Two", "Three")
numbers.also {
    print("The list elements before adding new one: $it")
}.add("Four")
  • scope的使用时机图

每个作用域函数都有其特定的用例,选择哪一个取决于你想要的操作类型以及返回值。

常用的复杂对象

字符串

Kotlin 的字符串是不可变的 Unicode 字符序列。它们使用双引号 (“) 或三重引号 (”“”") 来表示。

val str1 = "Hello, world!"
val str2 = """
    This is a multiline
    string.
"""

字符串可以连接起来形成新的字符串。

val str3 = str1 + str2

字符串也可以使用索引来访问其字符。

val firstChar = str1[0] // 'H'
val lastChar = str1[str1.length - 1] // '!'

字符串还支持各种操作,例如大小写转换、子字符串提取和正则表达式匹配。

val upperStr = str1.toUpperCase() // "HELLO, WORLD!"
val subStr = str1.substring(7, 12) // "world"
val matches = str1.matches(Regex("Hello, \\w+!")) // true

字符串模板可以用于轻松地将变量插入字符串中。

val name = "Alice"
val age = 20

val greeting = "Hello, $name! You are $age years old."

字符串模板还可以用于格式化数字。

val price = 10.99

val formattedPrice = "The price is $%.2f."

字符串是 Kotlin 中一种非常强大的类型。它们可以用于各种目的,例如文本处理、格式化和模板化。

集合相关

创建数组
// 创建一个整型数组
val myIntArray = intArrayOf(1, 2, 3, 4, 5)

// 创建一个字符串数组
val myStringArray = arrayOf("a", "b", "c", "d", "e")

// 创建一个混合类型数组
val myMixedArray = arrayOf(1, "a", 3.14, true, 'c')

// 访问数组元素
println(myIntArray[0]) // 输出:1
println(myStringArray[2]) // 输出:c
println(myMixedArray[3]) // 输出:true

// 遍历数组
for (element in myIntArray) {
   println(element)
}

// 使用下标遍历数组
for (i in myStringArray.indices) {
   println(myStringArray[i])
}

// 使用 forEach() 遍历数组
myMixedArray.forEach { element ->
   println(element)
}

// 创建二维数组
val my2DArray = arrayOf(
   intArrayOf(1, 2, 3),
   intArrayOf(4, 5, 6),
   intArrayOf(7, 8, 9)
)

// 访问二维数组元素
println(my2DArray[0][1]) // 输出:2
println(my2DArray[2][2]) // 输出:9

// 遍历二维数组
for (row in my2DArray) {
   for (element in row) {
       println(element)
   }
}
创建各种集合

kotlin 创建可变集合

  1. 使用 mutableListOf() 函数创建一个可变列表:
val mutableList = mutableListOf(1, 2, 3)
  1. 使用 mutableSetOf() 函数创建一个可变集合:
val mutableSet = mutableSetOf(1, 2, 3)
  1. 使用 mutableMapOf() 函数创建一个可变映射:
val mutableMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3)
  1. 使用 Array() 函数创建一个可变数组:
val mutableArray = Array(5) { 0 }
  1. 使用 mutableIterable() 函数创建一个可变迭代器:
val mutableIterable = mutableListOf(1, 2, 3).asIterable()

kotlin 创建不可变集合

要创建不可变集合,可以使用 Kotlin 的 listOf()setOf()mapOf() 函数。这些函数返回一个不可变的集合,其中元素不能被添加、删除或修改。

以下是如何使用这些函数创建不可变集合的示例:

// 创建一个不可变的列表
val myList = listOf(1, 2, 3)

// 创建一个不可变的集合
val mySet = setOf(1, 2, 3)

// 创建一个不可变的映射
val myMap = mapOf(1 to "one", 2 to "two", 3 to "three")

需要注意的是,这些函数返回的集合是不可变的,这意味着它们不能被修改。如果您尝试修改这些集合,将会得到一个编译错误。

如果您需要一个可变的集合,可以使用 Kotlin 的 mutableListOf()mutableSetOf()mutableMapOf() 函数。这些函数返回一个可变的集合,其中元素可以被添加、删除或修改。

kotlin 创建各种空集合

// 创建一个空的 List
val emptyList = emptyList<Int>()

// 创建一个空的 Set
val emptySet = emptySet<String>()

// 创建一个空的 Map
val emptyMap = emptyMap<Int, String>()

// 创建一个空的 Array
val emptyArray = arrayOf<Int>()

// 创建一个空的 ByteArray
val emptyByteArray = ByteArray(0)

// 创建一个空的 ShortArray
val emptyShortArray = ShortArray(0)

// 创建一个空的 IntArray
val emptyIntArray = IntArray(0)

// 创建一个空的 LongArray
val emptyLongArray = LongArray(0)

// 创建一个空的 FloatArray
val emptyFloatArray = FloatArray(0)

// 创建一个空的 DoubleArray
val emptyDoubleArray = DoubleArray(0)

// 创建一个空的 BooleanArray
val emptyBooleanArray = BooleanArray(0)

// 创建一个空的 CharArray
val emptyCharArray = CharArray(0)
创建一个 range
//    创建一个range  
var intRange = 1..10  
  
//    创建一个rangeTo  
var intRangeTo = 1.rangeTo(10)

kotlin 的二元组和三元组

二元组 (Pair)

二元组是一种数据结构,它包含两个元素。它可以用 Pair<A, B> 表示,其中 AB 是两个类型。例如,以下代码创建一个二元组,其中第一个元素是字符串,第二个元素是整数:

val pair = Pair("Hello", 1)

你可以使用 firstsecond 属性来访问二元组中的元素:

val firstElement = pair.first
val secondElement = pair.second

你也可以使用解构声明来访问二元组中的元素:

val (firstElement, secondElement) = pair

三元组 (Triple)

三元组是一种数据结构,它包含三个元素。它可以用 Triple<A, B, C> 表示,其中 ABC 是三个类型。例如,以下代码创建一个三元组,其中第一个元素是字符串,第二个元素是整数,第三个元素是布尔值:

val triple = Triple("Hello", 1, true)

你可以使用 firstsecondthird 属性来访问三元组中的元素:

val firstElement = triple.first
val secondElement = triple.second
val thirdElement = triple.third

你也可以使用解构声明来访问三元组中的元素:

val (firstElement, secondElement, thirdElement) = triple

二元组和三元组都是不变的数据结构,这意味着它们一旦被创建就不能被修改。

使用二元组和三元组

二元组和三元组可以用于各种场景。例如,你可以使用它们来存储一个键和一个值,或者来存储一个对象的两个或三个属性。

以下是一些使用二元组和三元组的示例:

  • 使用二元组来存储一个键和一个值:
val map = mutableMapOf(Pair("key1", "value1"), Pair("key2", "value2"))
  • 使用三元组来存储一个对象的三个属性:
data class Person(val name: String, val age: Int, val gender: String)

val person = Person("John", 30, "male")

二元组和三元组是 Kotlin 中非常有用的数据结构。它们可以帮助你组织和存储数据,并使你的代码更易于阅读和理解。