# 变量

可变声明 变量名 : 变量类型 = 变量值 是变量的完整声明方式,其中不同的部分在不同的地方是可省或不可省是不同的,但是顺序是固定的。可以这样理解:Kotlin 第一关注安全性,要求开发者说明变量是 val 还是 var,其次关注类型,最后关注初始化赋值

# 变量声明

Kotlin 支持两种关键字声明,编译时利用它出色的类型推导机制自主判断变量类型

  • val 用于声明不可变的变量,初始化后不能重新赋值,对应 final
  • var 用于声明可变变量,初始化后可以被重新赋值,对应非 final

如果需要对一个变量延迟赋值,需要显式声明变量类型,如 var a : Int

函数定义时使用 vararg 关键字表示不固定个数的变量

如果既需要声明类型,又需要赋值,使用 var a : Int = 10

声明变量时,永远优先使用 val,除非不得已,才使用 var。这样设计出的程序更加健壮,更符合高质量的编码规范

Kotlin 所有的变量在声明时必须进行初始化赋值或者赋类型,不能直接 var a

# 数据类型

Kotlin 完全抛弃了 Java 的基本数据类型,全部使用对象数据类型,有 Int, Long, Short, Float, Double, Boolean, Char, Byte 注意首字母大写

kotlin 中的强制类型转换符为 as。 a as b 表示把 a 转化为 b(相当于 (b) a)

# 字符串内嵌表达式

像 PHP 那样,可以在字符串中使用 "${}" 来代换变量值进行字符串拼接。如果只有一个值的话大括号还可以省略

# 数组

  • 可以使用 arrayOf(vararg elements) 传入可变数量的参数,可以在 arrayOf 后面加上 <T> 指定泛型 (但一般不需要,会自己判断。而且不加泛型时,数组可以存放不同类型的元素)
  • 可以使用 byte/short/int/long/char/float/double/booleanArrayOf(...) 来建立指定类型的数组
  • 可以使用 arrayOfNulls(size) 来创建指定大小的空数组
  • 可以使用工厂函数 Array(size: Int, init: (Int) -> T) 来指定方式创建数组。第一个参数是数组大小,第三个参数是一个函数,其接受下标,返回数组的值
  • 可以使用 Int/Short/Long/Float/Double/Boolean/Char/ByteArray 建立原始类型数组

# 函数

# 函数声明

函数需要关键字 fun 开头,下面举一个例子

fun largerNumber (param1: Int, param2: Int) : Int{
    return max(param1, param2)   //Kotlin 代码不需要分号结尾
}
// 以上代码表示传入两个 Int 类型的参数,函数返回值是 Int
// 简化 1:只有一条语句的函数可以放在一行,中间用等号连接
fun largerNumber (param1: Int, param2: Int) : Int = max(param1, param2)
// 简化 2:根据类型推导特性,函数的返回值类型 Int 可以省略
fun largerNumber (param1: Int, param2: Int) = max(param1, param2)

Kotlin 函数和 Java 一样,不需要事先声明或定义(不同于 C)

使用这一语法糖,函数变成了形似赋值语句,这也是 Kotlin “函数式编程” 的一个体现

# 函数调用

//Kotlin 函数支持默认值
fun printParams(num: Int, str: String = "Hello") { //str 可以不传入
    println("num is $num, str is $str")
}
// 如果想要给第一个参数加默认值,Kotlin 支持键值对形式直接调用函数
fun printParams(num: Int = 100, str: String) {
     println("num is $num, str is $str")
}
printParams(str = "hi") // 键值对指定调用,不需要考虑传入参数顺序

# 逻辑控制

# if 语句

Kotlin 的 if 语句有返回值,每一个分支判断的最后一个值就是返回值,因此有了下面这个极度简化的函数:

fun largerNumber (param1: Int, param2: Int) = if (param1 > param2) param1 else param2

# when 语句

when 语句类似于 switch,但是功能强大得多,举例如下

//when 的带参数用法,精确匹配
fun getScore(name : String) = when(name) {
    "Tom" -> 86
    "Jim" -> 77
    "Tim" -> 99
    else -> 0
}
//when 的带参数用法,类型匹配
fun checkNumberType(num : Number) { // 这里 Number 是一个抽象类,Float、Double、Int 等类型是它的子类
    when(num){
        is Int -> println("is Int")      //is 相当于 Java 里的 instanceof
        is Double ->println("is Double") // 不加 f 的小数一律按 double 处理
        else -> println("cannot check")
    }
}
//when 的不带参数用法
fun getScore(name : String) = when {
    name == "Tom" -> 86
    name == "Tim" -> 99
    name.startsWith("A") -> 100
}
  • when 语句和 if 一样,每一个条件分支最后一句是返回值
  • when 可以进行带参匹配和不带参匹配,前者在 "->" 符号前只需要指定结果,后者需要写出完整的表达式,好处是可以随便判断(比如在一个 when 里对不同的变量进行验证,如果使用传统的 if-esle 语句的话,代码量爆炸)

# while 语句

和 Java 等编程语言完全相同

# for 语句

在 Kotlin 中,Java 常用的 fori 被舍弃,而 forin 大大加强。以下是需要补充的背景知识:

val range = 0..10 // 双端区间,表示 [0,10]
val range = 0 until 10 // 单端区间,表示 [0,10)
for (i in 0..10) { /* 循环语句 */ }    //i 不需要定义或声明,默认自增 1
for (i in 0 until 10 step 2) { /* 循环语句 */ } //i 每轮自增 2
for (i in 10 downTo 1) { /*...*/ } // 双端降序区间 [10,1]

实际上 (a…b) 是一个 Range 对象。它有一些方法,比如 random () 可以随机获取这个区间的一个值

# 标准函数

标准函数是指 Standard.kt 文件中定义的函数

  • let 配合?: 进行辅助判空处理
  • with 接受两个参数,第一个是任意类型对象,第二个是 lambda 表达式。with 会在 lambda 表达式中提供第一个参数对象的上下文(省略了 " 对象名." 的部分),并使用 lambda 表达式最后一行代码作为返回值返回,with 不需要实例化对象
  • run 需要实例化对象,也就是需要调用某个对象的 run 方法。它只接受一个 lambda 参数,并在其中提供这个对象的上下文,也使用最后一行代码作为返回值返回
  • apply 需要实例化对象,无法指定返回值,最后返回调用对象本身。总的来说 with、run、apply 都是简化上下文的函数。
  • repeat (n) {…} 将大括号内的函数执行 n 次

# 扩展函数

扩展函数表示在不修改某个类的源码的情况下,向这个类添加新的函数,语法结构是

fun ClassName.MethodName(param: type): returnType { body }

扩展函数最好放在一个 ClassName.kt 中,方便查找用。如此一来它处于顶层函数位置,方便各个源文件使用

扩展函数优势在于可以像使用类内本来就存在的方法一样,更加方便,举例:

//String.kt
fun String.lerrersCount():Int{
    var count = 0
    for (char in this){	 // 扩展函数自带上下文,这里的 this 即指 String
        if (char.isLetter()) count++
    }
    return count
}
// 在文件中使用扩展函数时
val count = "ABCD1234xyz!@#".letterCount() // 直接.letterCount (),如同这一方法本来就是 String 自带的一样

kotlin 自己也有一些自带的扩展函数,比如

  • String 类带有.reverse (), .capitalize () 等
  • use {lambda} 保证 lambda 表达式代码执行完毕后自动关闭外层的流,不需要在 try-catch 中手动编写 finally 语句
  • forEachLine {lambda} 用于从文件逐行读取

扩展函数可以被添加到任意类中

# 高阶函数

如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么这个函数就是高阶函数(Kotlin 增设 “函数类型”)

语法规则为 **(参数类型,参数类型,…) -> 返回值类型 ** 如果函数返回值是空,那么返回值类型写 Unit

当函数作为参数时,传入方法为 **:: 函数名 **

// 也可以改用 lambda 表达式实现
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int) : Int {
    val result = operation(num1, num2)
    return result
}
fun plus(num1: Int, num2: Int) : Int {
    return num1 + num2
}
fun minus(num1: Int, num2: Int) : Int {
    return num1 - num2
}
fun main() {
    val num1 = 100
    val num2 = 80
    val result1 = num1AndNum2(num1, num2, ::plus) // 高阶函数的适用方式
    val result2 = num1AndNum2(num1, num2, ::minus)
    println("result1 is $result1 and result2 is $result2")
}

Kotlin 高阶函数在 Java 中是没有的。编译器把它转化成字节码的时候实际上是创建了一个匿名类,并实现了一个接口。这就会带来额外的开销。

# 内联函数

内联函数可完全消除 lambda 表达式带来的开销,只需要在定义高阶函数时前面加上 inline 关键字声明

# 运算符重载

kotlin 支持运算符重载,使用关键词 operator。和 C++ 重载不同的是,kotlin 重载运算符函数名是固定的单词,而不是符号形式的 ±*/

// 重载类的加法
class Money(val value: Int) {
    // 对于 obj1 + obj2 
    operator fun plus(money: Money): Money{
        val sum = value + money.value
        return Money(sum)
    }
    // 对于 obj + num
    operator fun plus(money: Int): Money{
        val sum = value + money
        return Money(sum)
    }
}
语法糖表达式 实际调用函数(使用的固定函数名)
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++ a.inc()
a– a.dec()
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a == b a.equals(b)
a > b a < b a >=b a <= b a.compareTo(b)
a…b a.rangeTo(b)
a[b] a.get(b)
a[b] = c a.set(b,c)
a in b b.contains(a)

# 静态方法

Kotlin 极度弱化了静态方法的概念,因为它提供了更好用的语法特性,即单例类

// 单例类。单例类里的函数不是严格意义上的静态方法,但是可以使用和静态方法相同的方式来调用(对象来自自动创建)
object Util{
    fun doAction(){
        ...
    }
}
// 但是单例类的所有方法都是静态的。如果只希望类中某一部分方法变成静态方法的调用方式,需要使用 companion object
class Util{
    fun doAction1(){
        ....
    }
    companion object {
        fun doAction2(){
            ....
        }
    }
}
// 但是 companion object 内的方法并不是静态方法,而是在 Util 里创建一个伴生类,由 Kotlin 保证 Util 类只有一个伴生对象。
// 如果确实要定义真正的静态方法,有两种方式
//1. 加 @JvmStatic 注解
class Util{
    fun doAction1(){
        ....
    }
    companion object {
        @JvmStatic // 这个注解只能加在单例类或者 companion object 里的方法上
        fun doAction2(){
            ....
        }
    }
}
//2. 使用顶层方法:创建独立的 kt 文件,然后直接写 fun...
//kotlin 编译器会将所有的顶层方法都编译成静态方法。在 kotlin 代码中可以在任意位置直接调用。但如果在 java 中调用,需要指定文件名。(因为编译器自动根据文件名创建一个同名 class 供 java 调用)

# infix 函数

infix 是 kotlin 一种语法糖,它可以使得函数更贴近英语表达,比如之前 mapOf 里面的 to

infix fun String.beginWith(prefix: String) = startsWith(prefix)
// 定义了 String 的拓展函数 beginWith,则 A.startsWith (B) 可以写为 A beginWith B
// 更加常见的有位运算的语法糖
a xor b  // 等价于 a.xor (b)
a and b | a shl b | a shr b | a ushr b | a and b | // 等等. ushr 是无符号右移,左端补 0, 相当于 java 的 ">>>"

infix 函数有两个严格限制

  • 不能定义为顶层函数,必须是某个类的成员函数,比如使用拓展函数将其添加入某个类
  • infix 函数必须且只能接受一个参数,不限制参数类型

# 几种方法

  • System.currentTimeMillis () 获取系统时间

# 面向对象

Kotlin 实例化对象不需要使用 new 关键字,直接 val a = Person () 即可(举例)

定义内部类时,使用 inner class 关键字

定义常量的关键字是 const,只有在单例类、companion object 或者顶层方法中才能使用 const 关键字

# 继承与构造函数

//kotlin 中默认所有的非抽象类都是不可继承的。如果想让它可继承,需要在 class 前加 open 关键字
open class Person {
    // 类内定义变量,在 “所有变量必须初始化赋值或类型” 的前提下,有一些额外规定:
    var name = ""   // 这里是赋值,也可以使用 lateinit var name : String 完成赋类型
    var age = 0     // 这里是赋值,不能使用 lateinit var age : Int 规定 lateinit 不能用于八大基本类型
}
//kotlin 中继承是:符号,和 C++ 相同,而和 Java 的 extends 不同;此外 Person 后面需要加一对括号(Java 不需要)
class Student : Person() {
    var sno = ""
    var grade = 0
}

lateinit 是 “延迟初始化” 关键字。在类内定义全局变量时可能需要在多个函数中进行访问,但由于 kotlin 的安全限制,需要每一处都进行判空处理,并初始化为 null(即便是开发者能保证这个变量不会抛出空指针异常)。这一关键字可以避免繁琐的判空处理,但是同时也带来了异常的风险。为了降低这种风险,kotlin 提供了如下语法,判断全局变量是否初始化,从而避免空指针并且避免重复初始化

if (!::varable.isInitialized){
    // 进行初始化操作
}

以下对 Person 后的括号进行解释

Kotlin 的构造函数分为主构造函数次构造函数

  • 主构造函数

    • 任何一个类只能有一个主构造函数

    • 是最常用的构造函数

    • 主构造函数特点是没有函数体,直接定义在类名的后面,比如 class Student (val sno: String, val grade: Int) : Person (){},这样一来对 Student 实例化的时候必须传入参数。因为构造函数使用的参数不需要重新赋值,所以使用 val。注意:主构造函数中声明成 val 或 var 的参数将自动成为该类的字段(这样一来类内不要重复定义这个字段),如果这是一个普通的参数,不要加 var 或 val

    • 如果主构造函数需要逻辑代码,可以在类内定义一个 init {…} 结构体,主构造函数的逻辑写在里面

      class Fraction(str: String) {
          var integer: Int = 0
          var numerator: Int
          var denominator: Int
          init {
              val arr = str.split('/')
              numerator = arr[0].toInt()
              denominator = arr[1].toInt()
          }
      }
    • 每个类都有一个默认的不带参数的主构造参数,当然也可以显式指明参数

    • 因为继承特性中子类的创建必须调用父类的构造函数,所以需要显式给 Person 后面加一个括号,即便 Person 构造函数不需要参数也要加括号。如果需要参数,那么 Student 的主构造函数应当写成 class Student (val sno: String, val grade: Int, name: String, age : Int) : Person(name,age){}

  • 次构造函数

    • 一个类可以有多个次构造函数,也可以没有

    • 次构造函数使用 constructor 关键字定义在类的内部

    • // 这里 sno、grade 将成为 Student 的成员变量
      class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age){
         	// 这一个次构造函数将于以 val a = Student ("Tim", 19) 的方式实例化对象时被调用
          constructor(name: String, age: Int) : this("", 0, name, age) {
              //this 将会进一步实例化对象,这一次就有四个参数,会调用主构造函数
          } 
          // 这一个次构造函数将于以 val a = Student () 的方式实例化对象时被调用
          constructor() : this("", 0) {
              //this 将会进一步实例化对象,这一次有两个参数,将会调用第 4 行的次构造函数,继而间接调用主构造函数
          }
      }
      // 以上没有使用默认参数,也可以直接以添加默认参数的方式,可以省略两个次构造函数,并可以以键值对形式直接指定值
      • 次构造函数可以用于实例化一个类,与主构造函数区别在于它有函数体

      • 如果一个类同时具有主、次构造函数,那么所有的次构造函数都必须调用主构造函数(在一次实例化中可以只有用到的次构造函数去调用主构造函数,没有用到的就不调用了)

  • 只有次构造函数而没有主构造函数

    • kotlin 允许一个类只有次构造函数而没有主构造函数:没有显式定义主构造函数,而定义了次构造函数

    • class Student : Person { // 注意这里 Person 后面没有括号了,因为 Student 类没有主构造函数
          constructor(name: String, age: Int) : super(name, age){
              //super 表示调用父级的构造函数,因为 Student 自己没有主构造函数,所以需要调用父级的构造函数,不能用 this
          }
      }

# 接口

Kotlin 的接口和 java 几乎一样,以下是接口的示例

// 新建 interface 文件,只保存如下代码
interface Study { 
    fun readBooks() // 只需要声明函数,使用接口的类强制实现这个函数
    fun doHomework(){ //kotlin 中使用接口的类可以不实现这个函数(则使用这里的默认函数),java 的话 jdk1.8 以后支持默认实现
        println("do homework default implementaion")
    }
}
// 在定义实现这个接口的类所在的文件中
class Student(name: String, age: Int) : Person(name, age), Study{ // 逗号后表示实现接口,接口后面不用加括号(但如果这个类只用来实现一个接口,那么使用冒号
    override fun readBooks(){			
        println(name + " is reading")
    }
}

只要接口中的函数拥有函数体,那么它就是一个具有默认实现的函数,实现接口的类可以不 override 此方法。

反之,如果接口里这个函数只有函数头,那么实现接口的类必须 override 这个方法

# 可见性修饰符

kotlin 的四种可见性修饰符:public (默认), protected, private, internal(只对同一模块的类可见)

Java 的四种可见性修饰符:public, protected, private, default (默认,同一包路径下的类可见)

  • Kotlin 的 public 和 java 一样,都表示所有类可见
  • Kotlin 的 protected 表示对当前类和子类可见,Java 中 protected 表示对当前类、子类、同一包下的类可见
  • Kotlin 的 private 和 java 一样,都表示当前类可见

# 数据类

在规范的系统架构中,数据类是一种将服务器端或者数据库的数据映射到内存中,为编程逻辑提供数据模型的特殊的类。(也就是把不同类型的对象进行封装,然后提供一些对外的方法)

数据类通常需要重写 equals (), hashCode (), toString () 等方法。在 Java 中,构建一个数据类需要大量的模板化代码,而且使用同样的数据构造数据类时的方法也不相同

kotlin 定义了 data 关键字用于声明一个数据类,比如

data class Cellphone (val brand: String, val price: Double) // 一个类没有内部代码时可以省略大括号
// 在使用时
fun main () {
    val cellphone1 = Cellphone("Huawei", 4000.00)
    val cellphone2 = Cellphone("Huawei", 4000.00)
    println (cellphone1)
    println ("" + (cellphone1 == cellphone2))
}
===========输出结果============
Cellphone(brand-Huawei, price=4000.0)
true

# 单例类

单例模式是最基础的设计模式之一,用于避免创建重复的对象,当希望某个类在全局最多拥有一个实例时使用单例模式

Java 中创建单例类的方法:

public class Singleton {
    private static Singleton instance; // 静态成员变量
    private Singleton() {}  // 将构造函数私有化,阻止外部调用
    public synchronized static Singleton getInstance() { // 静态函数
        if (instance == null) {
            instance  = new Singleton();
        } 
            return instance;
    }
}

而在 kotlin 中创建单例类很简单:

object Singleton {} // 创建了一个空的单例类
object Singleton { // 创建了一个具有一个函数的单例类
    fun singletonTest() {
        println ("singletonTest is called")
    }
}
Singleton.singletonTest() // 调用单例类的的方法

# 密封类

当一个类实现某个接口时,如果这个接口用于表示某个操作的结果,那么当某个函数对结果进行判断的时候,往往需要多加一个 else 来补充条件分支以通过 kotlin 的编译。即便是这样,如果这个结果某天多了一种情况,也不便于确认所有函数都对这种情况进行判断(这种情况下会进入 else(往往是异常处理)。

// 原方法:使用接口
interface Result
class Success(val msg: String) : Result
class Failure(val error: Exception) : Result
fun getResultMsg(result: Result) = when (result) { // 这里是面向对象的知识,子类 is 父类恒为真(upcasting),所以传入 Success 或者 Failure 都满足 result:Result 的要求
	is Success -> result.msg
    is Failure -> result.error.message
    else -> throw IllegalArgumentException() // 编译器强制要求
}

密封类是一个可继承的类,所以实现时需要在名字后加括号,关键字是 sealed class。Kotlin 会自动检查并该密封类的子类,并强制要求对每一个子类的对应条件全部处理,而不需要使用 else。

密封类及其所有的子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中

sealed class Result
class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()
fun getResultMsg(result: Result) = when (result) {
	is Success -> result.msg
    is Failure -> result.error.message
}

# 泛型和委托

泛型允许不指定具体类型的情况下进行编程,这样的代码具有更好的扩展性

默认情况下,所有的泛型可以为空,因为不手动指定上界的时候,默认值为 Any?

# 泛型类

// 举例
class className<T> {
    fun method(param: T): T{
        return param
    }
}
val myClass = className<Int>()
val result = myClass.method(123)

# 泛型方法

// 举例
class className{
    fun <T> method(param: T): T{
        return param
    }
}
val myClass = className()
val result = className.method<Int>(123)  // 这里的 & lt;Int > 也可以省略
fun <T: Number> method(param: T) : T { } // 指定了泛型类型的上界为数字型

委托是一种设计模式:操作对象自己不会去处理某段逻辑,而是把工作委托给另一个辅助对象去处理。

# 类委托

类委托的核心思想是将一个类的具体实现委托给另一个类去完成。通过在接口声明后面使用关键字 by 与受委托的辅助对象的名字,可以免去一大堆辅助代码,如:

class MySet<T> (val helperSet: HashSet<T>) : Set<T> by helperSet {
    fun helloWorld() = println("Hello world") // 增加新的方法
    override fun isEmpty() = false // 演示,重写 HashSet 里的方法
    // 其他方法使用 HashSet 的方法来实现
}

# 委托属性

将一个字段的具体实现委托给另一个类完成

class MyClass {
    var p by Delegate() // 将 p 属性的具体实现交给 Delegate 类去完成,调用和赋值时使用 Delegate 类的 getValue 和 setValue 方法
}
// 需要对 Delegate 类进行具体的实现
// 在 Delegate 类中必须实现 getValue 和 setValue,都要用 operator 关键词声明
class Delegate {
    var propValue: Any? = null
    operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any?{ 
        return propValue /* 可以在什么类使用 */  /* 一个操作属性类 */  
    }
    operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
        propValue = value                              /* 具体要赋给委托属性的值,需要和 getValue 返回值类型相同 */
    }
}

# Lambda 编程

# 集合的创建与遍历

// 以下各类集合推荐 [] 选取元素
val myList1 = listOf(...) // 创建不可变列表
val myList2 = mutableListOf(...) // 创建可变列表(val 表示 myList2 本身不可改变,即指向的列表不改变,但列表内容可以增删改
val mySet1 = setOf(...) // 创建不可变集合,与列表的区别是它的元素是无序、不重复的 因为底层实现采取的是 hash 映射
val mySet2 = mutableSetOf(...) // 创建可变集合
val myMap1 = mapOf(to, ..) //map 特点是键重复时,后来的值将会覆盖之前的 这里的 to 不是关键字,而是一个 infix 函数
val myMap2 = mutableMapof(to, ..) // 创建可变映射 
for ((key,value) in myMap1) {...} // 遍历 map 的方式

# 集合的函数式 API

lambda 函数的完整结构为 {参数 1 名 :参数 1 类型,参数 2 名 :参数 2 类型 -> 函数体 }

lambda 函数可以用做别的函数的参数,它执行时按指定的参数名来执行函数体,并将函数体最后一个语句的返回值作为 lambda 函数的返回值。函数体没有限定长度,但一般不要太长

// 举例,获取 list 中元素长度最长的那个字符串
val myList = listOf("a", "ab", "abc")
//version1,最朴素的用法
val lambda = {name: String -> name.length} // 定义一个 lambda 函数,它接收一个字符串作为参数,并返回字符串的长度
val maxLengthString = myList.maxBy(lambda) //maxBy 是 myList 一个接受 lambda 函数的方法,它返回 “最大” 的元素
//version2,lambda 变量不需要定义,可以直接传入
val maxLengthString = myList.maxBy({name: String -> name.length})
//version3,Kotlin 规定,lambda 参数为函数最后一个参数时,可以将 lambda 函数移到括号外
val maxLengthString = myList.maxBy() {name: Stirng -> name.length}
//version4,如果 lambda 参数是函数唯一一个参数,那么函数的括号可以省略
val maxLengthString = myList.maxBy {name: String -> name.length}
//version5,借助 Kotlin 出色的类型推导机制,一般不需要写出 lambda 参数的类型
val maxLengthString = myList.maxBy {name -> name.length}
//version6,如果 lambda 只有一个参数,那么不需要声明参数列表,而是用关键字 it 直接代替
val maxLengthString = myList.maxBy {it.length}

# 集合中常用的函数式 API 总结

使用范式集合.API {lambda 函数} 以下例子省略了 "集合." 部分

  • maxBy 返回长度最大的元素
  • map 将集合中的每个元素映射成另一个元素,返回这个集合
  • filter 将集合中元素长度大于 10 的元素组成新的集合并返回
  • any 判断集合中是否有长度大于 5 的元素,返回 true 或 false
  • all 判断集合中是否所有元素长度都大于 5,返回 true 或 false

# 空指针检查

Kotlin 为了彻底规避空指针异常,规定所有变量都不为空,并在编译时检查(而不是运行时)。对于那些必须为空的变量设定了 “可空类型”。在普通类型后加一个问号’?' 表示这个元素是可为空,否则是不可为空。

  • 对于可空类型,调用它的方法时需要使用 "?." 操作符。该操作符表示此对象为不为空时才执行,否则什么也不做
  • 另一个操作符是 "?:",当它左边不为空时返回左边的结果,左边为空时返回右边的结果
  • 当自己已经做了非空处理,但是编译器无法判断出来时,在这个对象后面加上 "!!"(非空断言)强制通过编译 (但要提醒自己,因为最自信对象不为空的时候往往是潜在的发生空指针异常的时候 hhhh)
  • 标准函数 let 提供了函数式 API 的编程接口,并将原始调用对象作为参数传递到 lambda 表达式中,可以配合?: 使用
fun doStudy(study: Study?) {
    study?.let{  //?. 是调用可控类型方法的做法,let 是立即执行内部函数。
        it.readBooks()  // 只有一个参数时可以省略 "标识符 ->" 部分,并用 it 代替标识符
        it.doHomework() // 使用 let 好处是只需进行一次?. 的判断就能执行一系列函数,降低开销
    }
}

此外,let 函数还可以处理全局变量的判空问题,因为全局变量可以被其他线程修改,使用 if 判断 null 依然会报错

# 并发

# 线程

Kotlin 的多线程运用 lambda 函数非常简单,只需要 thread { 代码 } 即可

# 协程

Kotlin 中协程技术很有特色,大部分编程语言不具有,可以简单理解为轻量级线程。协程仅在编程语言层面实现不同协程的切换,极大提高了并发编程的运行效率

# Kotlin 语法糖

  • JavaBean 类是一种根据类内字段自动生成 getter 和 setter 的方法,Kotlin 中可以不使用这一方法而是直接 ". 字段 " 直接获取或修改值
  • Kotlin 的 lambda 函数在调用时允许将没有用到的参数使用一个下划线代替,但注意不能改变参数位置

# Android 开发

  • Kotlin 绑定控件不需要手动调用 findViewById,它会自动根据 id 创建好同名控件,使用时直接当成已经定义好的控件就行了。这一点是借助了 kotlin-android-extensions 插件,但它无法在适配器中使用,只能在活动与碎片中使用
  • Kotlin 启动一个 intent 时传入的第二个参数是 " 活动名::class.java",java 开发的话是 活动名.class
  • javaClass.simpleName 返回当前活动的名称
// 启动一个 activity 的最佳方法
//kotlin 规定,所有写在 companion object 结构体的方法都可以使用类似于 java 静态方法的形式调用
// 这样的话更直观看出此活动创建时所需要传递的参数,有利于多人协作。
// 同时它提供的调用接口也可以简化编程:SecondActivity.actionStart (this, "data") 即可直接启动此活动
//
class SecondActivity : BaseActivity(){
    ...
    companion object {
        fun actionstart(context: Context, data1: String){
            val intent = Intent(content, SecondActivity::class.java)
            intent.putExtra("param1", data1)
            context.startActivity(intent)
        }
    }
}
// 数据持久化三种方式:文件存储、sharedpreference 存储、数据库存储
private fun dataSave (data: String){
    try {
        val output = openFileOutput("data", Context.MODE_PRIVATE)
        val writer = BufferedWriter(OutputStreamWriter(output))
        writer.use {
            it.write(data)
        }
    } catch (e: IOException){
        e.printStackTrace()
    }
} 
private fun dataLoad () : String{
    val content = StringBuilder()
    try {
        val input = openFileInput("data")
        val reader = BufferedReader(InputStreamReader(input))
        reader.use {
            reader.forEachLine {
                content.append(it)
            }
        }
    } catch (e: IOException){
        e.printStackTrace()
    }
    return content.toString()
}
// 使用 sharedPreference 时比较简单,写入部分如下
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("键名", "值") 
editor.putInt("键名",) // 写入的方式要指明写入类型、写入的键值对
...
editor.apply() // 写入文件
// 读取
val pref = getSharedPreferences("data", Context.MODE_PRIVATE) // 第一种,指明文件名的方式
val pref = getPrefercences(Context.MODE_PRIVATE) // == getSharedPreferences(getLocalClassName(), mode);
val name = pref.getString("键", "Default") // 取值时要调用相关类型的函数,并指明键名和缺省值
// 总而言之,使用 sharedPreferences 第一步是 get 到这个文件或者 editor(有两种方法,指定文件名或默认类名)
// 第二步就是 get/put 分别对 pref 或 editor 进行读写
// 如果是写,还有第三步的提交:apply ()
// 第三种方法是数据库,这里以基本的 SQLite 数据库为例。它有两种方法:API 与 SQL 语句,这里采取后者
//CRUD 的增、删、改用的都是 execSQL 方法,查用的是 rawQuery 方法
//Android 提供了抽象类 SQLiteHelper 用于数据库的创建与升级,需要重写 onCreate 与 onUpgrade 方法
// 创建一个类继承上述抽象类
class MyDatabaseHelper(val context: Context, name: String, version: Int)
	: SQLiteOpenHelper(context, name, null, version) { // 第三个参数允许传入自定义 cursor,这里用 null 即可
    private val createBook = "create table Book (" +
            " id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text," +
            "category_id integer)"
    private val createCategory = "create table Category (" +
            "id integer primary key autoincrement," +
            "category_name text," +
            "category_code integer)"
    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL(createBook)
        db.execSQL(createCategory)
        Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
    }
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        if (oldVersion <= 1) {
            db.execSQL(createCategory)
        }
        if (oldVersion <= 2) {
            db.execSQL("alter table Book add column category_id integer")
        }
    }
}
// 使用 sql 语句
db.execSQL("insert into Book(name, authoe, pages, price) values (?, ?, ?, ?)",arraryOf("The Da Vince Code", "Dan Brown", "454", "16.96"))) // 插入
db.execSQL("update Book set price = ? where name = ?", arrayOf("10.99", "The Da Vince Code")) // 修改
db.execSQL("delete from Book where pages > ?", arrayOf("500")) // 删除
val cursor = db.rawQuery("select * from Book", null) 
cursor.moveToFirst() // 光标移动到第一行
cursor.getInt(cursor.getColumnIndex("字段名")) // 取值
cursor.moveToNext() // 光标下移一行
cursor.close() // 关闭光标
// 使用 API
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val dbHelper = MyDatabaseHelper(this, "BookStore.db", 3)
        createDatabase.setOnClickListener {
            dbHelper.writableDatabase
        }
        contentValuesOf("" to "")
        addData.setOnClickListener {
            val db = dbHelper.writableDatabase
            val values1 = ContentValues().apply {
                // 开始组装第一条数据
                put("name", "The Da Vinci Code")
                put("author", "Dan Brown")
                put("pages", 454)
                put("price", 16.96)
            }
            db.insert("Book", null, values1) // 插入第一条数据
            val values2 = ContentValues().apply {
                // 开始组装第二条数据
                put("name", "The Lost Symbol")
                put("author", "Dan Brown")
                put("pages", 510)
                put("price", 19.95)
            }
            db.insert("Book", null, values2) // 插入第二条数据
        }
        updateData.setOnClickListener {
            val db = dbHelper.writableDatabase
            val values = ContentValues()
            values.put("price", 10.99)
            val rows = db.update("Book", values, "name = ?", arrayOf("The Da Vinci Code"))
            Toast.makeText(this, "rows is $rows", Toast.LENGTH_SHORT).show()
        }
        deleteData.setOnClickListener {
            val db = dbHelper.writableDatabase
            db.delete("Book", "pages > ?", arrayOf("500"))
        }
        queryData.setOnClickListener {
            val db = dbHelper.writableDatabase
            // 查询 Book 表中所有的数据
            val cursor = db.query("Book", null, null, null, null, null, null)
            if (cursor.moveToFirst()) {
                do {
                    // 遍历 Cursor 对象,取出数据并打印
                    val name = cursor.getString(cursor.getColumnIndex("name"))
                    val author = cursor.getString(cursor.getColumnIndex("author"))
                    val pages = cursor.getInt(cursor.getColumnIndex("pages"))
                    val price = cursor.getDouble(cursor.getColumnIndex("price"))
                    Log.d("MainActivity", "book name is $name")
                    Log.d("MainActivity", "book author is $author")
                    Log.d("MainActivity", "book pages is $pages")
                    Log.d("MainActivity", "book price is $price")
                } while (cursor.moveToNext())
            }
            cursor.close()
        }
        replaceData.setOnClickListener {
            val db = dbHelper.writableDatabase
            db.beginTransaction() // 开启事务
            try {
                db.delete("Book", null, null)
//                if (true) {
//                    // 在这里手动抛出一个异常,让事务失败
//                    throw NullPointerException()
//                }
                val values = cvOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85)
                db.insert("Book", null, values)
                db.setTransactionSuccessful() // 事务已经执行成功
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                db.endTransaction() // 结束事务
            }
        }
    }
}
  • Android 异步消息处理机制由 Message、Handler、MessageQueue、Looper 组成
    • Message 是线程之间传递的消息,可以在内部携带少量的信息,用于在不同线程之间传递数据,具有 what、arg1、arg2、obj 字段等
    • Handler 是用于发送与处理消息,发送消息使用 sendMessage、post 等方法,最终传递到 Handler 的 handleMessage 中
    • MessageQueue 是消息队列,用于存放所有通过 Handler 发送的消息,他们一直存在于消息队列等待处理。每个线程只有一个 MessageQueue 对象
    • Looper 是每个线程中的消息队列管家,调用 Looper 的 loop 方法后将进入无限循环,每当发现消息队列存在一条消息时便会取出,然后传递到 Handler 的 handleMessage 方法。每个线程只会有一个 Looper 对象
val handler = object : Handler(){
    override fun handleMessage(msg: Message){
        //UI 操作
        when(msg.what){
            ... -> ...
        }
    }
}
...
thread {
    val msg = Message()
    msg.what = ...
    handler.sendMessage(msg)
}
  • 使用 AsyncTask:它是一个抽象类,通过对线程操作进行封装,提供了良好的接口。在使用 AsyncTask 时需要创建一个类去继承他,并指定 3 个泛型参数,重写 4 个方法,依次为:

    • 指定泛型参数
      • Params:在执行 AsyncTask 时传入的参数,它将进一步传递到自定义后台任务中使用
      • Progress:后台任务执行时,如果需要界面上显示进度,使用这里指定的泛型作为进度单位
      • Result:任务执行完毕后,如果需要对结果进行返回,选用这里的泛型作为返回值类型
    • 重写方法
      • onPreExecute:在后台任务开始前调用,用于进行界面初始化,如显示进度条
      • doInBackgroud(Params…):这个方法里的所有代码在子线程中进行,应当在这里处理耗时任务。在这里不能进行 UI 操作。如果需要更新 UI,要调用 publishProgress(Progress…)方法来完成
      • onProgressUpdate (Progress…):调用 publishProgress(Progress…)后,这个方法将调用,这个方法中可以对 UI 操作
      • onPostExecute(Result)后台任务执行完毕并通过 return 返回时,这一方法将被调用,可以利用返回数据操作 UI
  • Retrofit2 闪退:在项目级 gradle 文件的 Android 块添加

compileOptions {
          sourceCompatibility JavaVersion.VERSION_1_8
          targetCompatibility JavaVersion.VERSION_1_8
      }
  
      kotlinOptions {
          jvmTarget = "1.8"
      }

# 参考资料

  • 第一行代码 第三版
  • 初级 3_Kotlin 数据类型讲解
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

c01dkit 微信支付

微信支付

c01dkit 支付宝

支付宝

c01dkit qqpay

qqpay