重学编程语言 - Kotlin

简介

KotlinJetBrains 公司开源的一门现代化静态类型编程语言。Kotlin 支持多平台操作,可应用于 Android 开发、JavaScript 开发、服务端开发以及其他任何运行 JVM 的环境,Kotlin 的多目标平台实现原理在于其支持编译为 Java 字节码、JavaScript 字节码等目标平台的字节码。

Kotlin 的三大特性是:简洁安全互操作

  • 简洁(Concise):Kotlin 编写的代码非常简洁,其提供的诸如data classobject关键字、Lambda 表达式等特性可以显著减少样板代码。
  • 安全(Safe):Kotlin 将所有的类型都拆分成两种:「非空类型」和「可空类型」,这可以有效避免程序运行过程中出现NullPointerException,并且 Kotlin 对可空类型也提供了多种便利操作方法,另外,Kotlin 还提供了智能类型转换,一次检查通过即自动转换为检查类型,减少了后续冗余转换。
  • 互操作(Interoperable):Kotlin 100% 兼容 Java 语言,Kotlin 代码可调用 Java,Java 代码可调用 Kotlin。

:Kotlin 语言开发的最初目标就是用于替换 Java 语言,因此本文主要基于 JVM 平台进行讲解(即将 Kotlin 代码编译为 Java 字节码)。

开发工具

支持 Kotlin 语言编写的开发工具主要有:

  • IntelliJ IDEA: IntelliJ IDEA 是 JetBrains 公司开发的,是 Kotlin 官方推荐的开发工具。
  • Android Studio:Android Studio 是谷歌公司基于 IntelliJ IDEA 修改的一个开发工具,主要用于 Android 开发。
  • Complier:Complier 是一个命令行编译工具。

本文采用的开发工具为 IntelliJ IDEA,具体安装步骤网上查询即可,此处略过不表。

:IntelliJ IDEA 中提供了字节码查看功能:Tools -> Kotlin -> Show Kotlin Bytecode,此时就会弹出窗体显示当前文件字节码,在当前窗体点击 Decompile 选项,就可将字节码反编译为 Java 代码,这样就可以知道 Kotlin 代码最终编译等效的 Java 代码,非常有用。

编程范式

Kotlin 语言支持的编程范式有:面向对象函数式

类型系统

Kotlin 在编译期间进行类型检查,且不支持类型间隐式转换,因此 Kotlin 是一门 强静态类型语言

一个值得提及的点是,Kotlin 对其类型系统进行了类型拆分操作,将 Kotlin 中的所有类型都拆分为:非空类型可空类型。其中,非空类型变量不允许设置为null,相对于 Java 来说,这在很大程度上规避了程序在运行中可能出现的NullPointerException,解决了 Java 中被称为可能导致数十亿美元损失的令人讨厌的空指针异常。从这点上来说,Kotlin 实现的类型系统相比于 Java 更加安全。

Hello World

最简入门示例如下所示:

fun main(args: Array<String>) {
    println("Hello World")
}

其中,main函数就是 Java 语言中的public static void main(String[] args){...}

:上述例程还可简化为:fun main() = println("Hello World")

:Kotlin 中每条语句后面无需使用;,但是如果同一行中存在多条语句,则必须使用;进行分隔:

fun main() {
    print("Hello ");println("World") // => Hello World
}

数据类型

在 Kotlin 中,所有的类型都是引用类型,因此,变量永远指向一个对象引用。
:基本数据类型通常为值类型数据,操作效率会更高效。Kotlin 虽然不存在基本数据类型,但在编译期间会进行优化,将一些数值类型、字符和布尔值等自动拆箱为基本数据类型,提高程序运行效率。

下面介绍 Kotlin 中常见的一些内置类型:

  • 数值类型:Kotlin 中数值类型使用Number表示,数值类型包含 整型浮点型
    Number是整型(比如Int)和浮点型(FlaotDouble)的基类。

    • 整型:Kotlin 中的整型类型如下表所示:

      Type Size(bits) Min value Max value
      Byte 8 -128 127
      Short 16 -32768 32767
      Int 32 -2,147,483,648 (-2^{31}) 2,147,483,647 (2^{31} - 1)
      Long 64 -9,223,372,036,854,775,808 (-2^{63}) 9,223,372,036,854,775,807 (2^{63} - 1)

      举个例子:

      val one = 1                   // 默认为 Int 类型
      val threeBillion = 3000000000 // 超过 Int.MAX_VALUE,则为 Long 类型
      val oneLong = 1L              // 使用后缀`L`显示声明为 Long 类型
      val oneByte: Byte = 1         // Byte
      
    • 浮点型:Kotlin 内置的浮点型类型如下表所示:

      Type Size(bits) 有效数字(Significant bits) 指数位数(Exponent bits) 小数位数(Decimal digits)
      Float 32 24 8 6-7
      Double 64 53 11 15-16

      举个例子:

      val pi = 3.14              // 默认为 Double 类型
      val e = 3.14f              // 后缀使用`f`或`F`显示声明为 Float 类型
      val eFloat = 2.7182818284f // Float 类型小数个数超出 6-7 位时会舍入,其实际值为 2.7182817
      

      数值类型的一些注意事项如下:

      • 字面常量(Literal constants)表示法:Kotlin 支持十进制、十六进制和二进制字面常量表示:

        val a = 123        // 十进制表示
        val b = 0x0F       // 十六进制表示
        val c = 0b00001011 // 二进制表示
        

        :Kotlin 不支持八进制表示法

      • 下划线表示法:Kotlin 支持下划线数字常量表示:

        val oneMillion = 1_000_000
        val creditCardNumber = 1234_5678_9012_3456L
        val socialSecurityNumber = 999_99_9999L
        val hexBytes = 0xFF_EC_DE_5E
        val bytes = 0b11010010_01101001_10010100_10010010
        
      • 底层表示:在 Java 平台上,数值类型会被存储为 Java 的基本数据类型(自动拆箱),除非使用了可空类型(比如Int?)或者使用了泛型(比如List<Int>),此时会编译为基本类型的包装类型(自动装箱):

        val a: Int = 100                  // 自动拆箱为 int
        val boxedA: Int? = a              // Integer(自动装箱)
        val anotherBoxedA: Int? = a       // Integer
        
        val b: Int = 10000                // int
        val boxedB: Int? = b              // Integer
        val anotherBoxedB: Int? = b       // Integer
        
        println(boxedA === anotherBoxedA) // true,自动装箱缓存机制
        println(boxedB === anotherBoxedB) // false
        
  • 字符类型:在 Kotlin 中,字符类型使用Char表示,字符由单引号进行包裹:

    val ch: Char = 'a'
    
  • 布尔类型:布尔类型使用Boolean表示,其有两个值:truefalse

    val flag: Boolean = true
    
  • 字符串类型:Kotlin 中字符串类型使用String表示,Kotlin 支持转义字符串和原义字符串,各自的表示方法如下所示:

    • 转义字符串:字符串常量使用双引号包裹表示:

      // \n 会被转义为换行符
      val str: String = "Hello World\n" // => Hello World
      
    • 原义字符串(raw string):原义字符串使用"""包裹表示,原义字符串会保留字符串原始内容,包括换行等特殊字符:

      // \n 保持原义输出
      var rawStr = """Hello World\n"""
      print(rawStr) // => Hello World\n
      
      rawStr = """
          Hello
          World
      """
      print(rawStr) // =>     Hello
                    //        World
      

      :当原义字符串包含换行时,可使用trimMargin()函数去除前导空格,默认以|作为一行起始边界前缀标识,也可以使用自定义边界前缀,比如:trimMargin(">")

      rawStr = """
          |Hello
          |World
      """.trimMargin()
      print(rawStr)       // => Hello
                          //    World
      
      rawStr = """
          >Hello
          >World
      """.trimMargin(">")
      print(rawStr)       // => Hello
                          //    World
      
      

    String数据是不可修改的(immutable)。常见的字符串操作列举如下:

    • 长度:获取字符串长度方法如下:

      val str = "Hello Kotlin"
      // 法一:通过属性 length 直接获取字符串长度
      var len: Int = str.length
      println(len) // => 12
      
      // 法二:count() 的底层实现就是直接返回 length 属性
      len = str.count()
      println(len) // => 12
      
    • 拼接(concat):字符串拼接方法如下:

      // 法一:通过 + 号拼接
      var str = "Hello" + "World"
      println(str) // => Hello World
      
      // 法二:通过操作符 plus() 拼接
      str = "Hello".plus("World")
      println(str) // => Hello World
      
      // 法三:字符串模板
      str = "Hello"
      println("${str} World")
      
    • 子串:字符串截取获取子串方法如下:

      val str = "Hello Kotlin"
      // 法一:通过 subSequence 方法截取
      // [startIndex, endIndex),即 [0, 1)
      var subStr1: CharSequence = str.subSequence(0,1)
      println(subStr1) // => H
      
      // [0, 1]
      subStr1 = str.subSequence(IntRange(0,1))
      println(subStr1) // => He
      
      // 法二:通过 substring 方法截取
      // [startIndex, length - 1],即 [0, length -1]
      var subStr2: String = str.substring(0)
      println(subStr2) // => Hello Kotlin
      
      // [startIndex, endIndex),即 [0,1)
      subStr2 = str.substring(0,1)
      println(subStr2) // => H
      
      // [0, 1]
      subStr2 = str.substring(IntRange(0,1))
      println(subStr2) // => He
      
  • 数组类型:数组类型使用Array表示。数组的常见操作如下所示:

    • 创建:创建数组主要有如下几种方式:

      // 法一:通过构造函数创建数组
      val array1: Array = Array(3) { i -> i }          // => [0, 1, 2]
      // 法二:创建并初始化数组元素
      val array2: Array = arrayOf(1, 2, 3)             // => [1, 2, 3]
      // 法三:创建指定长度数组,且初始化为空
      val array3: Array<Int> = arrayOfNulls<Int>(3)    // => [null, null, null]
      // 法四:创建一个空数组
      val array4: Array<String> = emptyArray<String>() // => []
      

      以上所有方法创建的都是 Kotlin 内置的Array类型数组,Kotlin 也支持创建平台原生类型数组(避免自动拆箱),其内置了一些数组类来表示平台原生数组:ByteArrayShortArrayIntArray等等,每种原生数组都提供相应的工厂方法方便创建,比如:intArrayOf()longArrayOf()...:

      // 通过构造函数创建原生数组,即 int[]
      var primitiveArray: IntArray = IntArray(3) // => [0, 0, 0]
      primitiveArray = IntArray(3) { 4 }         // => [4, 4, 4]
      primitiveArray = IntArray(3) { i -> i }    // => [0, 1, 2]
      // 通过工厂方法创建原生数组
      primitiveArray = intArrayOf(1, 2, 3)       // => [1, 2, 3]
      
    • 增/改:数组设置或修改元素内容方法如下:

      val array = arrayOfNulls<String>(2)
      // 通过 set 方法设置
      array.set(0,"value0")
      // 通过操作符 [] 设置元素
      array[1] = "value1"
      array.forEach { println(it) }
      

      Array重载了操作符[],使用该操作符可设置和获取相应索引元素内容。

    • :对数组的常见查询操作大致有如下几种:

      • 查询元素内容:获取数组相应索引元素内容方法如下:

        val array = arrayOf(1, 2)
        // 通过 get(index) 方法获取
        val idx0  = array.get(0)
        // 通过操作符 [] 获取
        val idx1 = array[1]
        println("array[0] = $idx0, array[1] = $idx1")
        
      • 遍历:常使用for表达式或forEach方法对数据进行遍历,大致有如下几种格式:

        val array = intArrayOf(1, 2, 3)
        // 格式一: 遍历数据内容
        // for 表达式
        for (item in array) {
            println(item)
        }
        // forEach 方法
        array.forEach { item -> println(item) }
        // 可以使用方法引用进行简化
        array.forEach(::println)
        
        // 格式二:遍历元素索引和内容
        // for 表达式
        for ((index, item) in array.withIndex()) {
            println("array[$index] = $item")
        }
        // forEach 方法
        array.forEachIndexed{ index, item ->
            println("array[$index] = $item")}
        
  • Any:表示 Kotlin 中所有非空类型的基类:

    val obj1: String = "test superclass"
    println(obj1 is Any)  // => true
    
    // 可空类型不为 null 也为 Any 的子类
    val obj2: Int? = 2
    println(obj2 is Any)  // => true
    
    val obj3: Int? = null
    println(obj3 is Any)  // => false
    println(obj3 is Any?) // => true
    
    

    Any即相当于 Java 中的Object
    :依据类型拆分机制,Any的可空类型为Any?,且Any的超类为Any?Any() is Any?true

  • Unit:当函数没有返回值时,使用Unit进行修饰:

    fun returnNothing(): Unit{
        println("This function returns void")
    }
    

    :当函数没有返回值时,通常无需显示声明Unit,而是直接忽略不写
    Unit本质是一个全局单例object实例对象,所以它是一个Any类型数据,它的效果等同于 Java 语言的void关键字:

    public object Unit {
        override fun toString() = "kotlin.Unit"
    }
    
  • Nothing:表示一个永远不存在的值,可认为是 空类型。其源码实现如下:

    package kotlin
    
    /**
     * Nothing has no instances. You can use Nothing to represent "a value that never exists": for example,
     * if a function has the return type of Nothing, it means that it never returns (always throws an exception).
     */
    public class Nothing private constructor()
    

    可以看到,Nothing的构造函数是私有的,因此无法实例化,实际上,在程序运行期间,也不会存在任何Nothing实例,它更多的只是表示一个编译期抽象概念,即没有值或运行异常。Nothing的主要作用有如下几种:

    • 当函数返回值类型声明为Nothing时,表示该函数永远不会正常终止,通常表现为函数运行时永远会抛出异常:

      fun fail(msg: String): Nothing {
          throw RuntimeException(msg)
      }
      

      Nothing的这个特性通常有两方面用途:

      1. 由于Nothing函数不会正常终止,因此如果能执行到调用Nothing函数,那么其后面的代码就不会被执行到,这些代码可被优化掉:

        val addr = company.address ?: fail("No address")
        println(addr.city)
        

        如果company.addressnull,那么后续代码就不会被执行,因为fail会抛出异常,另外,只要addr成功返回,那么编译器会把addr的类型推断非空,因为如果为空,fail函数就终止程序了,这里一个优化的操作就是,后续使用addr不用进行判空操作。

      2. 通常使用Nothing的这个特性用来声明一些暂未完成的函数:

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

      NothingUnit的区别在于:Unit是一个真正的实例对象,其类型为Any,只是它表示返回值内容为空,但我们仍然能通过val ret = funcReturnsUnit()来获取函数返回值,而Nothing表示函数抛出异常,没有返回值。

    • Nothing?是所有可空类型的子类型,且Nothing?允许的有且只有唯一的值为null,所以其可作为其他任何可空类型的空引用。从这段话又可以延伸出如下内容:

      1. null的类型是Nothing?Nothing?是其他可空类型的子类型,这也就是为什么其他可空类型的值可以设为null
      2. Nothing是所有类型(可空+非空)的子类型,因为NothingNothing?的子类型,而Nothing?是所有可空类型的子类型,所以Nothing还可用于作为泛型参数,比如:List<Nothing>

最后,Kotlin 的类型系统中,除了将数据类型分为可空和非空类型外,还引入了一种 平台类型:平台类型主要是为 Java 引入的,因为在 Java 中,所有对象均为可空类型,这样当在 Kotlin 中引用 Java 变量时,就会存在很多判空操作,相对繁琐,而如果将 Java 变量看成非空类型,那么运行时就可能导致NullPointerException异常,因此,Kotlin 为了平衡对 Java 变量的操作,引入了平台类型,简单说就是:Kotlin 在引入 Java 变量时,即可以将其作为可空类型,也可以将其作为非空类型,由开发者自行决定,以平衡判空操作与空指针异常。

变量

格式

Kotlin 中变量的声明与定义格式如下所示:

[可见性修饰符] {var | val} 名称 [: 类型] [=] [值]

其中,Kotlin 中的类、对象、接口、构造函数、方法、属性和它们的setter都可以有 可见性修饰符,Kotlin 中支持如下四种可见性修饰符:

  • public:表示所有类可见。缺省时默认可见性修饰符为public
  • private:当前类可见或当前文件内可见
  • protected:当前类及其子类均可见
  • internal:同一模块中可见

变量类型

Kotlin 中有三种变量类型:

  • 可变变量:表示可以更改变量指向。
    可变变量使用var关键字进行表示:
    kotlin var a: Int = 1 // 可变变量可更改指向 a = 2

  • 常量变量:表示变量指向无法更改。
    常量变量使用val进行表示:
    kotlin val constRef: Int = 1 // 常量变量无法更改指向 constRef = 2 // error

    **注**:`val`变量相当于 Java 语言中的`final`变量
    
  • 编译期常量:编译期常量使用const修饰:

    const val CONST_STRING = "Hello World"
    

    :编译期常量有几个使用限制:

    1. const必须作用于顶层作用域,或者object成员变量,或者companion object成员变量。
    2. const必须以String类型或基本数据类型值进行初始化。
    3. const不能设置自定义getter访问器。

建议优先使用val声明变量,无法满足时再改为var,这样可以避免不经意的数据修改,增加程序健壮性。

延迟初始化

Kotlin 支持对变量的延迟初始化,也即变量的懒加载,主要有如下两种:

  • lateinitlateinit可以对非空类型var变量进行延迟初始化。

    通常情况下,非空类型成员变量必须在构造器中进行初始化,这种设定限制了程序编写灵活性,因此 Kotlin 提供了一个关键字lateinit,使能非空成员变量的延迟初始化:

    private lateinit var a: String
    
    fun main() {
        a = "lazy load variable"
    }
    

    lateinit使用有如下几个注意事项:

    1. lalteinit只能作用于非空类型变量,并且该变量不能是基本数据类型。
    2. lateinit也可以作用于类成员属性,但只能作用于类中var成员变量,且要求该成员变量不能位于主构造函数中,不能有自定义访问器。
      从 Kotlin 1.2 开始,lateinit也支持顶层属性和局部变量的延迟初始化。
  • by lazy()by lazy()用于懒加载val变量,且对任意类型数据均可懒加载。

    lazy()是一个参数为 Lambda,返回值为Lazy<T>的高阶函数,其返回值Lazy<T>val变量或类属性的延迟初始化委托对象,被lazy()委托的变量或属性在第一次访问时,会触发lazy()参数 Lambda 执行,该 Lambda 的最后一个语句作为返回值,赋值给被修饰的变量或属性,后续访问该变量不会再触发 Lambda,因为变量已初始化(加载并赋值)成功:

    private val b: Int? by lazy {
        println("initialized variable only when first access")
        8 // This is the return value
    }
    
    fun main() {
        println(b) // => initialized variable only when first access
                   // => 8
        println(b) // => 8
    }
    

函数

函数的声明与定义格式如下所示:

[可见性修饰符] fun 函数名称 (参数1: 类型, 参数2: 类型...)[: 返回值类型] {
    函数体
}

:函数默认返回类型为Unit,此时可忽略函数返回值类型

Kotlin 中支持如下几种类型函数:

  • 普通函数:普通类型函数函数,也即顶层函数,可单独定义在一个文件中。

    举个例子:定义一个函数add,参数接收两个Int类型数据,返回参数之和:

    fun add(a: Int, b: Int): Int {
        return a + b
    }
    

    Kotlin 函数调用大概有如下几种方式:

    法一:按参数顺序进行调用
    add(1, 3)
    
    法二:指定命名参数,此时参数可任意顺序
    add( b = 3, a = 1)
    
    法三:也可以两者混用,此时要遵循函数参数顺序
    add(1, b = 3)
    

    :命名参数无法作用于 Java 函数,因为在 Java8 之前,字节码文件不存储参数名信息,而 Kotlin 最低兼容 Java6。

    Kotlin 函数支持 默认参数可变参数

    • 默认参数: 可以为函数参数设置一个默认值。当调用函数时,未指定默认参数具体值时,则使用默认参数值。

      举个例子:比如将函数add的第二个参数设置默认值为10

      fun add(a: Int, b: Int = 10): Int {
          return a + b
      }
      
      // 调用
      add(1)      // => add(1, 10)
      add(1,3)    // => add(1, 3)
      // 支持命名参数调用
      add( a = 1) // => add(1, 10)
      

      :当重载函数时,默认参数会自动被忽略(即重载函数该默认参数降级为普通参数)
      :默认参数相当于 Java 中的函数重载,可为 Kotlin 默认参数函数增加@JvmOverloads注解,这样编译器在编译时,会生成相应的 Java 重载函数,方便 Java 进行调用。

    • 可变参数:Kotlin 支持可变长度参数,可变参数使用关键字vararg表示。

      举个例子:定义一个函数add,其支持长度可变的参数个数,要求返回所有参数之和:

      fun add(vararg args: Int): Int {
          return args.reduce { a, b -> a + b }
      }
      
      // 调用
      add(1)       // => 1
      add(1, 2)    // => 3
      add(1, 2, 3) // => 6
      

      :Kotlin 中的可变参数与 Java 类似,其底层实现都是基于数组(比如:Array<out T>IntArray...),但与 Java 不同的是,在 Kotlin 中,如果传入的是一个数组,不同于 Java 可直接传入数组对象,Kotlin 必须使用 展开运算符 显示展开数组,具体为在数组前添加符号*作为展开运算符:

      val array = arrayOf(1, 2, 3)
      add(*array)
      
  • 成员函数:其定义格式与普通函数一致,只是成员函数定义在类中,作为对象的一个方法:

    class MyClass {
        fun add(a: Int, b: Int): Int {
            return a + b
        }
    }
    
  • 单表达式函数:当函数体只有一条表达式时,此时可省略函数体:

    // 函数体只有一条表达式:a + b
    fun add(a: Int, b: Int): Int = a + b
    

    另外,当编译器能直接推导出函数返回类型时,则单表达式函数可忽略函数返回类型:

    // 编译期自动推导出函数返回类型为 Int
    fun add(a: Int, b: Int) = a + b
    
  • 局部函数:Kotlin 支持局部函数定义,局部函数可直接使用外部函数的参数和局部变量,因此 Kotlin 支持 闭包

    fun outter() {
        val msg = "Hello Kotlin"
        // 局部函数定义
        fun localFunc() {
            println("This is local function")
            println(msg) // 使用外部局部变量
        }
        // 调用局部函数
        localFunc()
    }
    

    :局部函数不能声明为内联函数,且拥有局部函数的函数也不能声明为内联函数。

  • 扩展函数:扩展函数是 Kotlin 中提供的一个非常有用的功能,它可以为已存在的类动态增加成员函数。

    举个例子:为类String增加一个成员函数println,用于输出字符串内容:

    // StringUtil.kt
    fun String.println() {
        // 扩展函数中,this 表示类型对象
        println(this.toString())
    }
    
    // 调用扩展函数
    "Hello Kotlin".println() // => Hello Kotlin
    

    :扩展函数基本等同于类成员函数,但扩展函数不能访问privateprotected属性。

    :扩展函数的本质是一个静态函数,类名为定义扩展函数的文件名,参数为调用对象,因此上述扩展函数String.println()其实最终编译为的等效 Java 代码如下所示:

    public class StringUtilKt {
        public static void println(String self) {
            System.out.println(self.toString());
        }
        public static void main(String[] args) {
            StringUtilKt.println("Hello Kotlin");
        }
    }
    

    :Kotlin 同时也支持为类增加 扩展属性

    var StringBuilder.lastChar: Char
        get() = this.get(length - 1)
        set(value: Char) {
            this.setChar(this.length - 1, value)
        }
    
  • 泛型函数:泛型函数的定义如下所示:

    fun <T> singletonList(item: T): List<T> { /*...*/ }
    
  • 高阶函数(Higher-Order Function):所谓高阶函数,即支持将函数作为参数或返回一个函数的函数。Kotlin 支持函数式编程范式,因此,在 Kotlin 中,函数是一等对象,每个函数都属于一种函数类型,比如:

    fun add(a: Int, b: Int): Int {...}
    

    上述函数add的类型为:(Int, Int) -> Int,即参数为IntInt,返回值为Int类型的函数。

    我们可以使用变量指向该函数:

    // 指向
    val funcAdd: (Int, Int) -> Int = ::add
    // 调用
    funcAdd(1,3)
    

    所以,定义一个高阶函数,其实很简单,只需参数设置为某个函数类型或返回某个函数类型即可:

    举个例子:定义一个没有返回值的函数add,该函数对前两个参数进行相加操作,然后将结果传递给第三个回调函数参数:

    // 参数为函数
    fun add(a: Int, b: Int, callback: (sum: Int) -> Unit) {
        val sum = a + b
        callback(sum)
    }
    
  • Lambda 表达式(Lambda Expression):Lambda 表达式是一种函数字面量(function literals),其将函数作为一种值来对待,这样我们在编写代码时,就无需创建一个对应类的实例或声明一个函数,而是直接传递函数即可。

    Kotlin 中,Lambda 表达式由一对大括号进行包裹,其格式如下所示:

    { 参数1: 类型, 参数2: 类型... -> 函数体}
    

    Lambda 表达式的返回值为其函数体最后一条语句的返回值

    举个例子:比如,对于上文高阶函数中的add函数,其第三个参数为一个函数类型数据,通常我们都直接传入一个 Lambda 表达式,代码调用更加简洁:

    add(1, 2, { sum -> println(sum) }) // => 3
    

    当函数的最后一个参数为 lambda 表达式时,则可将 lambda 表达式置于函数括号外面:

    add(1, 2) { sum -> println(sum) }
    

    :如果函数只有一个参数,且该参数为 Lambda 表达式,则函数括号可省略,比如:singleArgFunc({...})可简写为singleArgFunc {...}

    当 Lambda 表达式只有一个参数时,我们可忽略这个参数,而在函数体中直接使用内置变量it获取该参数:

    add(1, 2) { println(it) }
    
  • 匿名函数(Anonymous function):Kotlin 支持匿名函数,其定义方式与普通函数定义一致,只是忽略函数名称:

    val anonymousFunc = fun(a: Int, b: Int): Int { return a + b }
    // 调用
    anonymousFunc(1,2) // => 3
    

    Lambda 表达式无法显示指定函数返回类型,但绝大多数情况下,编译器都能自动推断函数返回类型,所以并没有什么必要性,但是如果一定需要显示指定函数返回值类型,此时可选择使用匿名函数。

  • 内联函数:Kotlin 中内联函数使用inline修饰符声明,内联函数消除 Lambda 表达式带来的运行时开销。

    inline

    函数调用时,存在一定的运行开销,对于大量微小的函数片段,通常将其设置为内联,将函数打平,即编译时将内联函数内部代码直接嵌入到调用处,消除函数调用开销。
    :实际上,Kotlin 会将 Lambda 表达式编译为某种函数类型的匿名类对象实例,实际运行的是该匿名类实例对象的方法,因此实际上,运行 Lambda 表达式,不仅仅存在函数调用开销,还存在对象实例创建开销。

    举个例子:比如对于我们上述高阶函数add,我们可将第三个函数参数callback设置为内联函数,消除callback调用开销:

    // 将函数 callback 设置为内联函数,消除其函数调用开销
    inline fun callback(sum: Int) = println(sum)
    
    fun main() {
        add(1,2,::callback)
    }
    

    上述效果其实就相当于如下代码:

    fun add(a: Int, b: Int) {
        val sum = a + b
    //    callback(sum)
        println(sum) // 内联函数体
    }
    

    当内联函数的参数为 Lambda 表达式时,该 Lambda 表达式通常也会被内联:

    inline fun add(a: Int, b: Int, callback: (sum: Int) -> Unit) {
        val sum = a + b
        callback(sum)
    }
    

    上述代码中,add为内联函数,此时,callback也会自动被设置为内联。
    :并不是所有的 Lambda 参数都会被内联,比如如果 Lambda 参数在某个地方被保存起来,那么该 Lambda 参数将无法被内联。一般来说,参数如果被直接调用或者作为参数传递给另一个inline函数(内联函数的 Lambda 参数不能传递给其他非inline函数,因为 Lambda 会被打平,嵌入到调用处,以代码块形式存在,消除了其函数类型,故不能被引用),则该 Lambda 参数是可以被内联的,否则编译器将给出错误。

    noinline

    如果我们想禁止函数内联,保持函数类型,那么可以为函数添加noinline修饰符:
    :通常如果高阶函数内,有一个非inline高阶函数也要引用该 Lambda 表达式,那么就需要将高阶函数的 Lambda 参数设置为noinline,保持函数类型。

    inline fun add(a: Int, b: Int, noinline callback: (sum: Int) -> Unit) {
        val sum = a + b
        callback(sum)
    }
    

    此时,callback函数不会被内联。

    crossline

    另外,默认情况下,Lambda 函数不支持 非局部返回,即不能直接在 Lambda 函数中直接return,如下例子所示:

    fun highOrderFunc(lambda: () -> Unit) {
        lambda()
    }
    
    fun main() {
        highOrderFunc {
            // return            // => error,非局部返回,直接退出 main 函数
            return@highOrderFunc // 支持,局部返回,退出 highOrderFunc 函数,main 函数后续代码可继续执行
        }
        println("main end")      // => main end
    }
    

    但是如果 Lambda 函数为内联函数,依据内联的运作方式,此时相当于直接将 Lambda 函数体嵌入到调用处,则此时支持非局部返回,如下所示:

    inline fun highOrderFunc(lambda: () -> Unit) {
        lambda()
    }
    
    fun main() {
        highOrderFunc {
            return // 非局部返回,成功,直接退出 main 函数
        }
        println("main end")      // 不被执行
    }
    

    上述代码实际等价于下面内容:

    fun main() {
        return
        println("main end")
    }
    

    有些时候可能需要禁止非局部返回操作,比如高阶函数中可能创建了其他嵌套函数或局部对象,在这些不同的上下文中,是不能调用具备非局部返回的内联函数的,否则可能意外打断调用者本身流程:

    inline fun highOrderFunc(lambda: () -> Int) {
        val runnable = Runnable { lambda() } // => error
    }
    

    此时,只需为 Lambda 表达式增加crossinline修饰符,就可禁止该 Lambda 表达式非局部返回:

    // 禁止 lambda 函数非局部返回
    inline fun highOrderFunc(crossinline lambda: () -> Int) {
        val runnable = Runnable { lambda() }
    }
    
    fun main() {
        highOrderFunc {
            // return // => error
        }
    }
    

    reified

    最后,内联函数还支持一个特别有用的特性:具体化的类型参数,即可以在运行时获取泛型具体类型。

    我们知道,Java 中存在泛型类型擦除问题,也就是我们无法在运行时直接获取泛型具体类型信息,但是 Kotlin 中通过内联函数却解决了这个问题,只需为泛型参数增加reified修饰符即可:

    // 此时可直接获取泛型 T 的具体类型信息
    inline fun <reified T> membersOf() = T::class.members
    
    fun main(s: Array<String>) {
        println(membersOf<StringBuilder>().joinToString("\n"))
    }
    

    原理其实就是内联函数在编译时会直接将泛型替换为具体类型,因此可以在运行期间获取泛型具体信息。

  • 中缀表达式/中缀函数:可使用过infix关键字定义中缀表达式/中缀函数,中缀表达式在使用上更加接近自然语言。

    中缀表达式必须满足以下要求才能定义:

    • 仅支持成员函数或扩展函数
    • 只支持单参数函数
    • 不支持默认参数和可变参数

    举个例子:定义一个中缀表达式vs,用于比较两个字符串长度大小,且返回长度较长的字符串:

    infix fun String.vs(other: String): String {
        return if (this.length > other.length)  this else other
    }
    
    // 调用
    "Hello" vs "World" // => World
    
  • 尾递归函数(Tail recursive function):Kotlin 支持尾递归函数,其使用tailrec修饰符表示。尾递归函数在编译期间会被编译为循环代码,有效规避递归函数堆栈溢出风险。

    举个例子:使用尾递归函数编写一个阶乘运算:

    tailrec fun factorial(n: Int, total: Int = 1): Long {
        if (n == 1) {
            return total.toLong()
        }
        return factorial(n - 1, n * total)
    }
    

    上述代码会最终被编译为类似如下的循环代码(以 Java 为例):

    public static final long factorial(int n, int total) {
      while(n != 1) {
         int value = n - 1;
         total = n * total;
         n = value;
      }
    
      return (long)total;
    }
    

    :Kotlin 中尾递归函数的要求是:尾递归函数的最后一个表达式必须是调用函数自身,且不能夹杂其他操作,比如不能用在try/catch/finally块中,甚至就连多余连接等操作都不能有(很容易导致最后一个操作不是调用自身),否则会导致尾递归无法编译为循环操作,比如如果将上述阶乘计算改为下述代码,就会导致尾递归优化失败:

    tailrec fun factorial(n: Int): Long {
        if (n == 1) {
            return 1L
        }
        return n * factorial(n - 1)
    }
    

    虽然看似上述代码的最后一个操作是调用了函数自身,但其实最后一个操作是乘法操作,即n * factorial_result,这样就就会导致尾递归失效,不会被转化为循环代码。

面向对象

Kotlin 支持面向对象编程范式,主要涉及的内容有如下:

在 Kotlin 中,类使用class关键字进行修饰,类的定义格式如下所示:

[可见性修饰符] class <类名> {...}

比如,定义一个类Human,表示人类:

class Human {
}

当类未携带任何属性或方法时,可省略类体大括号:

class Human

方法和属性

类中可以定义一些属性和方法,又由于类是对对象的抽象,因此类中的属性和方法依据归属不同又可再细分为如下:

  • 成员属性:表示对象的属性
  • 类属性:表示类的属性,在 Java 中也被称为 静态属性
  • 成员方法:表示对象的方法
  • 类方法:表示类的方法,在 Java 中也被称为 静态方法

:由于 Kotlin 支持全局函数和全局变量(全局函数和全局属性会被编译为 Java 类的静态成员),因此 Kotlin 取消了类属性和类方法,也即 Kotlin 不支持静态成员和静态方法。如果一定要为类实现类似 Java 的静态调用(即类名.xx),可以使用 伴生对象,具体内容请参考后文:面向对象 - 类 - 单例类 - 伴生对象

:面向对象范式中,有几个概念比较相近,可能会让人有所混淆,在此进行区分一下:

  • 函数(Function):指一段命名的代码块
  • 方法(Method):定义在类中的函数即称为方法
  • 字段(Field):表示对象或类的成员变量,主要用于承载数据,因此通常都是私有的,即private
  • 属性(Property):可以理解为拥有gettersetter方法的字段

类中方法的定义与普通函数定义格式一致,类中属性的定义完整格式如下:

// 成员变量
var <变量名>[: <类型>] [= <初始化>]
    [<getter>]
    [<setter>]

// 成员常量
val <变量名>[: <类型>] [= <初始化>]
    [<getter>]

举个例子:为类Human增加一些成员属性:nameagegender,以及相应的getter成员方法:

open class Human {
    // 完整写法
    public var name: String = "unborn"
        get() = field // getter
        set(value) {  // setter
            field = value
        }

    // 省略写法
    public var age: Int = 0

    // Java 常用的 getter 方式(不推荐,因为本身已具备 getter 方法)
    private var gender: String = "unknown"
    fun sex() = this.gender

    override fun toString(): String {
        return "${this.name} - ${this.sex()} - ${this.age}"
    }
}

settergetter统称为属性访问器,其中,setter为写访问器,getter为读访问器,访问器中通过幕后字段field读取或设置字段变量。

最后,Kotlin 还支持属性委托机制,详情参考后文:面向对象 - 类 - 委托 - 属性委托

构造函数

构造函数是类中一个特殊的成员方法,创建对象时,会自动调用对象的相应构造函数,进行对象初始化操作。

Kotlin 构造函数使用constructor关键字修饰,且支持如下两种类型构造函数:

  • 主构造函数:主构造函数直接定义在类名后面,且不具备函数体,其具体格式如下所示:

    class <类名> [[可见性修饰符] [注解] constructor](参数1: 类型, 参数2: 类型...) {...}
    

    :当主构造函数未显示指定可见性修饰符与注解时,可省略constructor关键字:

    class <类名> (参数1: 类型, 参数2: 类型...) {...}
    

    对主构造函数的初始化是通过初始化代码块init{..}进行的。
    :类中可以有 0 个或多个init {..}代码块,其执行顺序与声明顺序一致(包含成员变量)

    举个例子:为类Human增加主构造函数,并接收三个参数nameagegender,并进行初始化:

    // 主构造函数
    class Human(name: String, age: Int, gender: String) {
        private var name: String = "unborn"
        private var age: Int = 0
        private var gender: String = "unknown"
    
        // 初始化代码块
        init {
            this.name = name
            this.age = age
            this.gender = gender
        }
        // ...
    }
    

    如果主构造函数的参数直接赋值给对象成员变量,那么可以直接在主构造函数中使用valvar修饰,被valvar修饰的变量会自动成为类的成员属性,无需在类内显示声明和进行初始化:

    class Human(private val name: String,
                private val age: Int,
                private val gender: String) {
        // ...
    }
    

    最后,任何一个类,最多只能有一个主构造函数,且当未显示声明主构造函数时,Kotlin 会自动为每个类生成一个无参主构造函数(除非该类还声明了次构造函数,则此时不生成默认主构造函数,即主构造函数个数为 0)

  • 次构造函数:次构造函数在类内声明,其格式如下所示:

    class <类名> {
        constructor(参数1: 类型, 参数2: 类型...) {...}
    }
    

    Kotlin 中规定:一个类可以存在一个或多个次构造函数,但是当主构造函数存在时,每个次构造函数必须直接或间接调用主构造函数

    举个例子:为类Human添加多个次构造函数:

    class Human(private val name: String,
                private val age: Int,
                private val gender: String) {
    
        // this(..) => 主构造函数
        constructor(name: String, age: Int) : this(name, age, "unknown") {
        }
    
        // this(..) => 次构造函数 => 主构造函数(间接调用)
        constructor(name: String) : this(name, 0) {
        }
    
        // ...
    }
    

    其实,次构造函数很少会用到,因为 Kotlin 支持函数默认参数,即上述代码可简化为如下:

    class Human(private val name: String,
                private val age: Int = 0,
                private val gender: String = "unknown") {
                    // ...
                }
    
    

对象创建

创建一个类的对象只需调用其相应的构造函数即可:

// 无需使用 new 关键字
val human = Human("anonymous")

继承

类的继承其实就是基于已有类(基类)派生出新类(子类),子类自动拥有基类所有可继承的属性和方法。

Kotlin 中的继承语法如下所示:

// 使用冒号表示继承关系
class <子类> : <基类>(参数1, 参数2...) {...}

:Kotlin 由于主构造函数没有函数体,因此继承的时候,基类的表现形式是以构造函数形式,用来表示子类主构造函数默认调用父类的哪个构造函数。
当基类不存在主构造函数时(即定义了次构造函数,但未定义主构造函数时),且子类也不存在主构造函数,则可以使用类名即可:

class <子类> : <基类> {...}

此时子类的次构造函数必须直接或间接使用super关键字调用基类相关构造函数进行初始化:

class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

在 Kotlin 中,所有类、属性和方法默认采用final修饰,因此不能被继承与覆写,需要手动为支持继承与覆写的类或方法/属性增加open修饰符。

举个例子:定义一个新类Female,并继承于Human,并覆写方法sex()

// 为类 Human 添加 open 修饰符,使能继承
open class Human(private val name: String,
                 private val age: Int = 0,
                 private val gender: String = "unknown") {

    // 为方法增加 open 修饰符,使能覆写
    open fun sex() = this.gender
    // ...
}

// 定义新类继承 Human
class Female(name: String, age: Int): Human(name, age, "female") {
    // 覆写需要使用 override 关键字
    override fun sex(): String {
        return "female"
    }
}

:对属性或方法的覆写使用关键字override修饰。

特殊类

Kotlin 内置了其余一些特殊功能的类,大致有如下:

  • 内部类:内部类默认持有外部类引用,故内部类可直接访问外部类成员。

    内部类使用关键字inner进行修饰:

    class Outter {
        private val bar = 1
        inner class Inner {
            // 内部类默认只有外部类引用,故可直接引用外部类成员
            fun foo() = bar
        }
    }
    
    // 调用
    val obj = Outter().Inner().foo() // ==> 1
    

    :内部外访问外部类对象使用this@Outter进行表示。

  • 嵌套类:也即 Java 中的静态内部类,实际就只是将类定义在一个类里面而已:

    class Outer {
        class Nested {
            fun foo() = println("nested class")
        }
    }
    // 调用
    val obj: Outer.Nested = Outer.Nested()
    obj.foo() // => nested class
    

    :嵌套类无法直接访问外部类成员,但是由于处于同一文件中,因此可通过实例化外部类对象从而访问外部类对象所有成员(包括私有成员也可直接访问)。

  • 抽象类:拥有抽象属性或抽象方法的类为抽象类,抽象类使用abstract关键字修饰,抽象类只能被继承,无法实例化:

    abstract class AbstractClass {
        // 抽象属性
        protected abstract var id: Int
        // 抽象方法
        abstract fun absMethod()
    }
    

    :如果一个类包含抽象属性或抽象方法,那么该类必须声明为抽象类(即必须使用abstract修饰),但是如果一个类是抽象类,它可以不包含任何抽象属性或抽象方法。

  • 泛型类:Kotlin 支持定义泛型类:

    class Box<T>(t: T) {
        var value = t
    }
    
    // 调用
    val box: Box<Int> = Box<Int>(1)
    
  • 数据类:数据类是 Kotlin 中一个十分受欢迎的特性,它可以自动生成equals()hashCode()toString()componentN()copy()等方法,使用数据类我们就可以不用手动写这些模板方法:

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

    :只有声明在主构造函数中的属性才会被加入到equalshashCode等自动生成的方法中进行计算,声明在类体中的属性不纳入自动生成范畴。

  • 单例类:Kotlin 提供了一个特别的关键字object,其会创建一个类并同时实例化一个对象,使用该关键字可创建如下几种类型对象实例:

    • 对象声明:对象声明是一个命名单例,即创建一个全局单例对象:

      对象声明可以包含属性、方法、初始化语句块init {..}...但不支持构造函数声明,因此声明时即创建了全局单例对象,无需通过构造函数创建,同时这也能保证其全局单例特性(实际底层实现会生成一个构造函数私有的类)。

      object SingleInstance {
          fun doSomething(){
              println("global single instance")
          }
      }
      
      fun main() = SingleInstance.doSomething()
      

      :对象声明创建的是一个立即加载全局单例,其等价 Java 代码如下:

      public final class SingleInstance {
         @NotNull
         public static final SingleInstance INSTANCE;
      
         private SingleInstance() {
         }
      
         static {
            INSTANCE = new SingleInstance();
         }
      }
      
    • 对象表达式:对象表达式会创建一个匿名对象,它相当于 Java 中的匿名内部类:

      Thread(object : Runnable {
          override fun run() {...}
      }).start()
      

      :如果对象是函数式 Java 接口(即只具备一个抽象方法的 Java 接口,单抽象方法接口)实例,那么在 Kotlin 中可将其视为一个函数类型,即上述代码可更改为如下:

      Thread(Runnable {
          override fun run() {...}
      }).start()
      

      其实就是将Runnable看成一个函数,其参数为一个 Lambda 表达式。

      另外,如果 Java 的函数式接口,其抽象方法只有 0 个或 1 个参数时,此时可省略接口名:

      Thread{
          ...
      }.start()
      
    • 伴生对象:可以为类定义一个伴生对象,实现静态调用。

      伴生对象处理可以实现静态调用外,另一个好处就是可以访问外部类的私有成员:

      class Outter {
          private val str = "private Outter variable"
          companion object Companion{
              // 定义外部类实例作为成员变量
              private val outter = Outter()
      
              fun say() {
                  // 直接访问外部类私有成员
                  println(outter.str)
              }
          }
      }
      
      // 类似于 Java 一样的静态调用
      Outter.say()
      

      Kotlin 中规定,每个类中最多只能有一个伴生对象,因此伴生对象可省略名称,即companion object Companion {...}可简写为companion object {...}

      :伴生对象在编译时会被编译为 Java 中的静态内部类。

  • 枚举类:枚举类的主要特性是其内部会定义相关类实例对象,以限制该类型实例对象。

    Kotlin 中枚举类使用关键字enum修饰:

    enum class Direction {
        NORTH, SOUTH, WEST, EAST
    }
    

    枚举类内部可以声明的内容与普通类一致,因此可以定义构造函数,可以定义抽象方法...:

    enum class Color(val rgb: Int) {
        RED(0xFF0000) {
            override fun paint(): String {
                return "red: ${this.rgb}"
            }
        },
        GREEN(0x00FF00) {
            override fun paint(): String {
                return "green: ${this.rgb}"
            }
        },
        BLUE(0x0000FF) {
            override fun paint(): String {
                return "blue: ${this.rgb}"
            }
        }; // 注:此处需要使用分号
    
        abstract fun paint(): String
    }
    
    fun main() = println(Color.RED.paint()) // => red: 0xFF0000
    
  • 密封类:Kotlin 中使用密封类来定义受限的类继承结构。

    密封类使用sealed关键字修饰,被sealed修饰的密封类默认为abstract,且其构造函数为private

    枚举类是将实例定义在同一个类中,而密封类则要求将子类定义在同一个基类中,或者定义在基类所在的文件中(因为sealed class构造函数是私有的),以此控制子类继承结构。

    换种说法,枚举类可认为是限定了其实例数量,而密封类可认为是限定了其子类数量,因为它们都写死在了与基类同一处位置。

    如果把有限状态看成类集合,那么密封类就是一种非常适合的定义结构,将状态定义为密封基类,然后子类覆写基类,更改状态,由于状态有限,因此,子类个数是有限的,并且密封类支持when表达式,且数量有限制,因此无须使用多余的else语句,使得状态选择非常容易:

    sealed class State
    data class success(val status: Int) : State()
    data class failure(val status: Int, val extra: String) : State()
    
    fun checkState(state: State) = when (state) {
        is success -> println(state.status)
        is failure -> println("${state.status} ${state.extra}")
    }
    
    // 调用
    fun main() = checkState(failure(404, "not found")) // => 404 not found
    
  • 内联类:Kotlin 中使用inline关键字修饰内联类。

    内联类的主要作用是对基本数据类型的包装,因此基本数据类型的表达范围比较大,有时无法清楚表达变量代表的含义,比如,假设有一个变量:val time: Int,该变量的类型为Int,但是我们无法直接知道该Int是采用秒、分钟还是小时作为单位,此时如果能将该Int包装为具体的类型,比如SecondMinute或者Hour的话,就能明确具体类型,方便调用。

    但是重新定义一个新的类型,运行时会有对象创建等开销,本质上我们只需一个基本数据类型数据,而借助内联类,就可以同时实现上述需求。

    内联类在源码中相当于定义了一个新的类型,但在编译时,会被编译器拆箱为基本数据类型,消除运行时对象创建开销。

    举个例子:创建一个内联类Hour,表示小时:

    inline class Hour(val time: Int) {}
    
    // 调用
    val time = Hour(2) // => 实际被编译为:int time = 2
    

    内联类支持部分普通类特性,比如属性和方法定义,但有如下一些限制:

    • 内联类必须具备主构造函数,且主构造函数只能接受一个只读(即val)基础类型数据,编译器最终拆箱为该基础类型数据
    • 内联类不能有init
    • 内联类内部属性不能有幕后字段,即不能使用field,因此内部属性是对基础类型属性的计算代理
    • 内联类内部属性不能是lateinit和委托属性
    • 内联类不支持继承,既不能继承其他类,也不能被其他类继承(因为最终编译后的类型为基础数据类型)
    • 内联类支持继承接口
  • 局部类:局部类是定义在函数/方法中的类(不常用到):

    class Outter {
        // 局部类不能访问外部类成员
        private var outter_var = "outter variable"
    
        fun func() {
            var func_var = "func variable"
            class LocalClass {
                fun say() {
                    println("inside local class")
                    // 可修改外部方法局部变量
                    func_var = "change func variable"
                    // 可访问外部方法局部变量
                    println(func_var)
                }
            }
            LocalClass().say()
        }
    }
    

    局部类只在定义它的方法内可见,因此局部类不能调用外部类任何成员,但局部类可以访问和修改外部方法局部变量。

委托

Kotlin 支持对类的委托和对属性的委托,委托即代理,通过将类或属性委托给其他对象,让其他对象代理类或属性的行为。

Kotlin 中通过关键字by指定委托对象。

  • 类委托:类委托即委托类将自己的行为委托给被委托类(即代理类),一个要求是,委托类和被委托类必须是同一类型,这样 Kotlin 就能为委托类自动生成相应的方法,这些方法内部实现都由被委托类进行代理,如果委托类内部实现了相应方法,则不走代理,而是使用自己的方法:

    // 共同类型
    interface IWorker {
        fun doWork()
        fun relax()
    }
    
    // 秘书
    class Secretary: IWorker{
        // 工作当然是交给秘书去做
        override fun doWork() {
            println("Secretary do work")
        }
    
        override fun relax() {
            println("Secretary relax")
        }
    }
    
    // 老板 委托给 秘书
    class Boss : IWorker by Secretary() {
        // 休息当然是老板自己休息
        override fun relax() {
            println("Boss relax")
        }
    }
    
    fun main() {
        val boss = Boss()
        boss.doWork() // => Secretary do work,代理成功
        boss.relax()  // => Boss relax,不走代理
    }
    

    上述代码直接写死了代理类,比较不灵活(代理类和被代理类紧耦合),其实可以将代理类作为委托类的一个属性,代理给该属性对象即可,这样外部只需更改传入的代理对象,就可实现不同代理行为,扩展性更好:

    //...
    // 组合优先于继承
    class Boss(private val worker: IWorker) : IWorker by worker {
        override fun relax() {
            println("Boss relax")
        }
    }
    
    fun main() {
        // 外部传入代理类,解耦
        val boss = Boss(Secretary())
        boss.doWork() // => Secretary do work,代理成功
        boss.relax()  // => Boss relax,不走代理
    }
    
  • 属性委托:可以将属性委托给代理类对象,对该属性的访问与赋值会委托给代理对象实现。

    属性委托的执行机制为:属性的get()方法会委托给代理对象的getValue()方法,属性的set()方法会委托给代理对象的setValue()方法,因此属性委托不要求属性与代理对象类型一致,只需代理对象实现相应的getValue()方法和setValue()方法,其中,val属性只需代理对象实现getValue()var属性代理对象需同时实现getvalue()setValue()方法。

    class Boss {
        // 老板的报告交给秘书做
        var report: String by Secretary()
    }
    
    class Secretary {
        // thisRef 为被代理类对象,即 Boss,
        // property 为被代理属性
        operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
            return "被代理类:$thisRef,被代理属性:${property.name}"
        }
    
        // s 为要设置的值
        operator fun setValue(thisRef: Any?, property: KProperty<*>, s: String) {
            println("$thisRef.${property.name} = $s")
        }
    }
    
    fun main() {
        val boss = Boss()
        boss.report = "make a report" // setValue()
        println(boss.report)          // getValue()
    }
    

接口

接口的特性是所有方法都默认为抽象方法,所有属性都默认为抽象属性,且不能具备状态(即不能定义普通属性)。可以将接口当做一种特殊的抽象类,其使用interface进行修饰。

interface IVoice {
    // 默认 abstract
    val who: String
    fun description(): String
    // 可以有默认实现
    fun sound(): String {
        return "quiet"
    }
}

fun main()  {
    val sheep: IVoice = object : IVoice {
        override val who: String = "Sheep"
        override fun description(): String = this.who
        override fun sound() = "mie mie"
    }

    println("${sheep.description()} sound like: ${sheep.sound()}")
}

:Kotlin 中的接口方法支持默认实现,当未重写时,则使用默认实现。

运算符 / 操作符

Kotlin 支持如下几种类型运算符操作:

  • 算术运算符(Arithmetic Operators):执行代数运算。常见算术运算符如下表所示:

    Operator Description
    + 加法运算
    - 减法运算
    * 乘法运算
    / 除法运算
    % 取余

    :除法运算符/需要注意下是否是整除(都是整型情况下)

    *也可作为展开运算符使用,常用于展开数组传递给可变参数

  • 赋值运算符(Assignment Operators):执行变量赋值操作。常见赋值运算符如下表所示:

    Operator Description
    = 等于(赋值)
    += 加等
    -= 减等
    *= 乘等
    /= 除等
    %= 余等
  • 关系/比较运算符(Relational (Comparison) Operators):执行比较操作。常见关系运算符如下表所示:

    Operator Description
    < 小于
    > 大于
    <= 小于或等于
    >= 大于或等于
    == 等于
    != 不等于
    === 同一对象
    !== 不是同一对象
  • 逻辑运算符(Logical Operators):执行逻辑运算。常见逻辑运算符如下表所示:

    Operator Description
    || 或运算
    && 与运算
    ! 非运算
  • 位运算符(Bitwise Operators):执行位运算(比特操作)。常见位运算符如下表所示:

    Operator Description
    and
    or
    inv 非(取反)
    xor 异或
    shl 有符号左移
    shr 有符号右移
    ushl 无符号左移
    ushr 无符号右移
  • 一元运算符(Unary Operators):有些编程语言支持一元运算符。常见的一元运算符如下表所示:

    Operator Description
    + 正号
    - 负号(正负数转换)
    ++ 自增
    -- 自减

    :一元运算符中的++--同时支持前置和后置功能。

  • if表达式:Kotlin 中的if表达式相当于三目运算符,其格式如下:

if <expression> <TruthValue> else <FalseValue>

表达式(Expression)语句(Statement) 的区别是:表达式有返回值,而语句不一定有。

即当expressiontrue时,返回TruthValue,否则返回FalseValue

举个例子:if 3 > 2 "true" else "false",条件为3 > 2,返回字符串"true"

流程控制

Kotlin 中的流程控制语句大致有如下几种:

  • 选择结构语句:主要有如下两种选择/条件判断语句:

    • if语句:条件判断语句,其格式如下所示:

      if (<expression1>) {
          ...
      } else if (<expression2>) { // else if 可多个,也可忽略
          ...
      } else {                    // else 也可忽略
          ...
      }
      

      if也可用作表达式,其会返回一个值

    • when语句when语句类似于 Java 中的switch语句:

      // 格式一:带参数
      when (x) {
          1 -> print("x == 1")
          2 -> print("x == 2")
          else -> { // Note the block
              print("x is neither 1 nor 2")
          }
      }
      
      // 格式二:只带 Lambda 表达式
      when {
          x.isOdd() -> print("x is odd")
          y.isEven() -> print("y is even")
          else -> print("x+y is odd.")
      }
      

      when语句也可作为表达式,其会返回一个值:

      val result: String = when(x) {
          1 -> "one"
          2 -> "two"
          else -> "NaN"
      }
      
  • 循环结构语句:Kotlin 中主要有如下几种循环语句:

    • while循环:其格式如下所示:

      // 当 expression 为 true 时,执行循环
      while(<expression>) {
          ...
      }
      
    • do while循环:其格式如下所示:

      do {
          ...
      } while(<expression>)
      

      :Kotlin 中的do while语句与其他语言的一个不同之处在于,位于do语句体中的变量,while语句中是可见的,而其他大多数语言时不可见的:

      fun main() {
          do {
              var x = 10
          } while (--x != 0) // x is visible here!
      }
      
    • for循环for语句可遍历任何携带迭代器的对象:

      // 遍历集合
      for (item in collection) print(item)
      // 遍历区间
      for (i in 1..3) {
          println(i)
      }
      ...
      

      :对数组和集合的遍历还可以使用forEach(..)/forEachIndexed(..)函数。

  • 跳转语句:Kotlin 中的跳转语句主要有:continuebreakreturn,与其他语言功能一致,不再赘述。

代码组织

Kotlin 中的代码组织(及导包)与 Java 类似,每个 Kotlin 文件都属于某个包,每个文件都以package关键字开头,用以指定包名,文件中定义的所有声明(类、函数及属性)都隶属于当前文件所在的包。同个包中的声明可直接使用,不同包中的声明需先导入import后才能使用:


package com.whyn.demos

import kotlin.properties.Delegates
import kotlin.reflect.KProperty

:如果存在相同声明,可使用as关键字设置一个别名,以解决命名冲突:

import com.whyn1.bean.User
import com.whyn2.bean.User as User2 // User2 为 com.whyn2.bean.User 的别名

注释

Kotlin 支持如下三种类型注释:

  • 单行注释:只注释当前行:

    fun main() {
    // 单行注释
    }
    
  • 多行注释:可注释多行内容:

    fun main() {
        /* 多行注释
        val str = "Hello World"
        println(str)
         */
    }
    
  • 文档注释:对声明进行注释:

    /**
     * Standard property delegates.
     */
    public object Delegates {...}
    

区间操作

详情参考思维导图

解构声明

Kotlin 可以直接把一个对象赋值给多个变量,这种操作称为 解构声明(Destructuring Declaration)

比如:将一个数据类User对象进行解构:

fun main() {
    data class User(var name: String, val age: Int)
    val (name, age) = User("Lisi", 30)
    println("$name is $age years old") // => Lisi is 30 years old
}

解构声明的原理是类中重载了comonentN方法,进行解构赋值时,会依次调用获取类对象component1()component2...,赋值给相应位置变量。

:如果无需获取某个解构变量,则使用_占位即可(被_占位的变量不会调用对应的componentN方法):

// 忽略 age 变量
val (name, _) = User("Lisi", 30)

安全操作运算符

对于可空类型,Kotlin 提供了几种非常有用的辅助调用操作,可以让我们安全地进行非空类型操作:

  • 安全调用运算符:使用?.可以将null检查和方法调用合并为一个操作,当变量为null时,直接返回null,而当变量不为null时,则调用相应方法/属性:

    fun isEmpty(str: String?): Boolean {
        return str?.length == 0
    }
    

    上述代码等价如下:

    fun isEmpty(str: String?): Boolean {
        var ret = false
        if (str != null) {
            ret = str.length == 0
        }
        return ret
    }
    
  • Elvis运算符?:接收两个运算数,如果第一个运算数不为null,则返回第一个运算数,否则返回第二个运算数:

    fun isEmpty(str: String?): Boolean {
        val ret = str ?: ""
        return ret.isEmpty()
    }
    

    上述代码等价如下:

    fun isEmpty(str: String?): Boolean {
        var ret: String
        if (str != null) {
            ret = str
        } else {
            ret = ""
        }
        return ret.isEmpty()
    }
    
  • 非空断言!!可以将任何值转换为非空类型:

    fun isEmpty(str: String?): Boolean {
        return str!!.isEmpty()
    }
    

    上述代码强制将str转换为非空类型,当strnull时,会抛出KotlinNullPointerException,因此只有在确定变量绝对不为null时,才可尝试使用非空断言。

  • 安全转换运算符as?可以将可空变量转换为指定类型,如果无法进行转换,则返回null

    fun isEmpty(obj: Any?): Boolean {
        var ret: String? = obj as? String
        ret = ret ?: ""
        return ret!!.isEmpty()
    }
    

操作符重载(Operator overloading)

Kotlin 支持操作符重载,只需覆写相应操作符方法即可。比如:

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(this.x + other.x, this.y + other.y)
    }
}

上述代码为Point类重载了运算符+,其对应的操作符方法为plus,这样,Point对象就可以使用操作符+生成一个新对象:

fun main() {
    val p1 = Point(1, 2)
    val p2 = Point(10, 20)
    println(p1 + p2)      // => Point(x=11, y=22)
}

常见的操作符重载方法如下所示:

  • 一元操作符:如下表所示:

    表达式 操作符方法
    +a a.unaryPlus()
    -a a.unaryMinus()
    !a a.not()
    ++a, a++ a.inc()
    --a, a-- a.dec()
  • 二元操作符:如下表所示:

    表达式 操作符方法
    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)
  • in操作符:如下表所示:

    表达式 操作符方法
    a in b b.contains(a)
    a !in b !b.contains(a)
  • 索引访问操作符:如下表所示:

    表达式 操作符方法
    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)
  • 函数调用操作符:如下表所示:

    表达式 操作符方法
    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)
  • 赋值运算符:如下表所示:

    表达式 操作符方法
    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 a.compareTo(b) > 0
    a < b a.compareTo(b) < 0
    a >= b a.compareTo(b) >= 0
    a <= b a.compareTo(b) <= 0
    a == b a?.equals(b) ?: (b === null)
    a != b !(a?.equals(b) ?: (b === null))

更多详细内容,请参考:Operator overloading

异常机制

Java 采用 Check Exception 异常机制,即当调用处可能出现受检异常时,必须显示对异常进行声明(throws)或捕获(try..catch)(非受检异常不强制进行声明或捕获)。然而,在 Kotlin 中,却取消了这种异常检测机制,根据 Kotlin官方 说法,主要原因是 Check Exception 在大型项目上,对代码质量并无本质提升,且由于繁琐的格式,会降低开发效率。

理论上来说,Kotlin 取消了异常检测机制,可能会造成程序运行中由于缺少必要的异常捕获而导致程序崩溃率上升,然而实际中统计得出,Kotlin 开发的程序性并未比 Java 开发的程序健壮性差,但代码却更加简洁。

最后,如果代码调用处确实很可能在运行中出现异常,那么手动进行捕获即可。

更多 Kotlin 异常机制内容,可参考:浅谈Kotlin的Checked Exception机制

泛型

Kotlin 支持泛型函数和泛型类...详细内容可参考上文,这里主要介绍下泛型机制中的几个特性:

  • 协变:一个协变类是一个泛型类,协变类保持了泛型间的继承关系。协变采用关键字out进行表示。

    具体来说,假设类A继承于类B,那么,Producer<A>Producer<B>实际上是没有任何关系的,但是如果将泛型类声明为协变,即Producer<out T : B>,那么,Producer<A>就是Producer<B>的子类,也即,协变可以保留子类型化关系(保留泛型继承关系)。

    :Kotin 中的协变,即out T相当于 Java 中的? extends T,更多具体内容可参考:理解<? extends T>,<? super T>

  • 逆变:协变可以用于保持泛型继承关系,而逆变却反转了泛型继承关系。逆变采用关键字in进行表示。

    具体来说,假设类A继承于类B,那么,对于逆变Consumer<in T : B>,则有:Consumer<B>Consumer<A>的子类型,可以看到,逆变将泛型继承关系反转了。

    :Kotin 中的协变,即in T相当于 Java 中的? super T,更多具体内容可参考:理解<? extends T>,<? super T>

  • 星号投影:即采用符号*来代替类型参数。

    具体来说,星号投影(比如List<*>)表示容器存储了某种特定类型元素,但具体类型未知,因此不能对容器进行存储操作(因为具体类型未知),但可以进行获取操作(因为无论是哪种类型,都可以看做Any?)。当不关心容器存储元素类型信息时(即T),可直接使用星号投影表示元素。

    :星号投影List<*>相当于 Java 中的List<?>

带接收者的 Lambda

Kotlin 标准库提供了一些很好用的扩展函数,下面介绍几个常用的 作用域函数(Scoping Function)

  • runrun函数接收一个函数参数,且返回值为函数参数返回值(Lambda 最后一行),其源码如下所示:

    // 扩展函数
    @kotlin.internal.InlineOnly
    public inline fun <T, R> T.run(block: T.() -> R): R {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        return block()
    }
    
    // 全局函数
    @kotlin.internal.InlineOnly
    public inline fun <R> run(block: () -> R): R {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        return block()
    }
    

    示例:如下所示:

    fun main() {
        val str = "Outter"
        val ret = str.run {
            println(this)               // => Outter
            val str = "Inner"
            println("inside run" + str) // => Innder,局部作用域
            str
        }
        println(ret)                    // => Inner
        println(str)                    // => Outter,外部作用域
    
    }
    

    run函数可以用于创建局部作用域,其特点在于 Lambda 接收者为this(即调用者),返回值为 Lambda 最后一行。

  • withwith相对来说比较特殊,它不是一个扩展函数,with接收一个对象和该对象类型的扩展函数,扩展函数参数的上下文绑定到了该对象上,且其返回值为扩展函数返回值,其源码如下所示:

    @kotlin.internal.InlineOnly
    public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        return receiver.block() // 绑定上下文
    }
    

    示例:如下所示:

    fun main() {
        val str: String = with(StringBuilder()) {
            this.append("Hello ")
            this.append("World")
            this.toString() // 返回值
        }
    
        println(str)        // => Hello World
    }
    

    with的主要特性为指定了 Lambda 函数上下文,且其返回值为 Lambda 函数的返回值(即 Lambda 最后一行)

  • letlet函数是调用者的扩展函数,其将调用者以参数形式传递给 Lambda,因此 Lambda 内部作用域上下文可以使用it来获取调用对象,其返回值为 Lambda 最后一行:

    @kotlin.internal.InlineOnly
    public inline fun <T, R> T.let(block: (T) -> R): R {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        return block(this)
    }
    

    示例:如下所示:

    fun main() {
        val str: String? = "Hello"
        val ret = str?.let {
            println(it)      // => Hello
            "World"          // 返回值
        }
        println(ret)         // => World
    }
    
  • alsoalsolet基本一样,除了返回值为调用对象,即this

    @kotlin.internal.InlineOnly
    @SinceKotlin("1.1")
    public inline fun <T> T.also(block: (T) -> Unit): T {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        block(this)
        return this // 返回调用者
    }
    

    示例:如下所示:

    fun main() {
        val str: String? = "Hello"
        val ret = str?.also {
            println(it)       // => Hello
        }
        println(ret === str)  // => true
    }
    
  • applyapply函数是调用对象类型扩展函数,其参数也为调用对象类型扩展函数,因此 Lambda 内部作用域上下文为调用对象this,且其返回值也为调用对象this

    @kotlin.internal.InlineOnly
    public inline fun <T> T.apply(block: T.() -> Unit): T {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        block()
        return this
    }
    

    示例:如下所示:

    fun main() {
        val str: String? = "Hello"
        val ret = str?.apply {
            println(this)          // => Hello
        }
        println(ret === str)       // => true
    }
    

综上,其实可以看出,这些作用域函数之间的特性与区别主要在于如下三方面:

  1. 都具备局部作用域
  2. 作用域的接收者为thisit
  3. 都具备返回值:返回值为 Lambda 的返回值(Lambda 最后一行)或调用者自身(this

具体如下表所示:

函数 Lambda 内部接收者 函数返回值
run this Lambda 最后一行
with this Lambda 最后一行
let it Lambda 最后一行
also it this
apply this this

最后,个人觉得,其实这些作用域函数选择还是主要在于返回值上,因为无论哪个函数,接收者都可以获取得到(无非就是通过thisit的区别而已),而如果想函数调用完成后,返回调用者自身,那么可以选择alsoapply,如果想返回 Lambda 函数返回值,那么可选择runwithlet
大多数情况下,使用run(或let)和apply即可满足所有操作。

协程

具体内容请参考:Kotlin - 协程 简介

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 1.变量与函数 val:用于声明不可变的变量,这种变量在初始赋值之后就再也不能重新赋值,对应 Java 中的 fi...
    青年心路阅读 503评论 0 0
  • 概述 Kotlin是面向对象的静态类型语言; 在Kotlin中,所有东西都是对象,在这个意义上可以在任意变量上调用...
    CodeMagic阅读 382评论 0 0
  • 1. 重点理解val的使用规则 引用1 如果说var代表了varible(变量),那么val可看成value(值)...
    leeeyou阅读 502评论 0 0
  • 声明,这里是我平时日常的笔记Zone,所以记录可能会偏向于我认为的重点区域,会有些疏漏或者缺失的地方,或是排版或者...
    哥哥是欧巴Vitory阅读 818评论 0 2
  • 夜莺2517阅读 127,712评论 1 9