Kotlin

1. 简介

  • Java代码编译后生成的并不是计算机可以识别的二进制语言,而是特殊的class文件,这种class文件只有java虚拟机才能识别,而这个虚拟机其实担任的就是解释器的角色,会在程序运行时将编译后的class文件解释成计算机可以识别的二进制数据。因此,java严格来说属于解释性语言。
  • Kotlin的工作原理:如同java原理一样,使用自己的编译器将代码编译成与java虚拟机编译出的相同规格class文件,java虚拟机依然可以识别出由kotlin编写出的class文件。
  • kotlin相较于java来说,语法更简洁,更高级,甚至几乎杜绝了空指针异常问题的出现。

2. 编程之本:函数与变量

2.1 变量

1. Kotlin定义一个变量,只允许在变量前声明两种关键字:varval

  • (1) val:value的简写,用来声明一个不可变的变量,该变量在初始赋值后就不能再次赋值,对应java中的被final修饰的变量。
  • (2) var:variable的简写,用来声明一个可变的变量,该变量在初始赋值后还可以再次赋值,对应java中的非final变量。
  • (3) Kotlin具有出色的类型推导机制,如果一个变量在初始声明的时候就被赋值,那么Kotlin就会自动推导出该变量的类型

2. Kotlin中的代码每一行的结尾不加分号

fun main() {
    val a = 10
    println("a= "+ a )
}

3. 类型推导机制

  • Kotlin具有出色的类型推导机制,如果一个变量在初始声明的时候就被赋值,那么Kotlin就会自动推导出该变量的类型。
  • 但是如果对一个变量延迟赋值,则Kotlin就无法自动推导该变量的类型,这个时候就需要显示的声明该变量的类型。
var a = 10
or
var a : Int = 10
or
val a : Int
a= 10

4. 数据类型

  • 如果足够细心,你就会发现,在刚刚的代码中,数据类型IntI是大写的,而java中的int的首字母是小写的。这代表Kotlin完全抛弃了java中的基本数据类型,而全部改用对象数据类型。
Java    Kotlin   数据类型说明
int     Int      整形
long    Long     长整形
short   Short    短整形
float   Float    单精度浮点型
double  Double   双精度浮点型
boolean Boolean  布尔型
char    Char     字符型
byte    Byte     字节型
  • 什么时候使用var?什么时候使用val?
    • 建议永远先使用val!val!val!声明一个变量,当val没有办法满足要求时在使用var。这样设计出来的程序会更健壮,更符合高质量编码规范。

2.2 函数

1. 语法规则

①fun ②methodName(③param1: Int,param2:Int):④Int{
     ⑤return 0
}
  • ①:定义函数的关键字
  • ②:函数名
  • ③:参数列表,param1: Int,冒号前面为变量名,后面为变量类型
  • ④:函数返回类型
  • ⑤:函数体

2. 语法糖

  • 当一个函数只有一行代码的时候,可以直接将唯一的一行代码写在函数定义部分的尾部,中间用等号连接即可
fun largerNumber(num1:Int,num2:Int) = max(num1,num2)
  • 可以发现该函数省略了大括号,return,以及函数返回值类型,这依赖于kotlin的类型推导机制。max()方法返回的是一个Int值,而我们在largerNumber函数的尾部又衔接了max函数,因此kotlin可以推导出largerNumber函数的返回值类型也必然是Int类型。

3. 逻辑控制

3.1 if条件语句

  • Kotlin中的if语句与Java中的if语句几乎是没有区别的,但是也并不是完全相同
  • Kotlin中的if语句是带有返回值的
fun largerNumber(a:Int,b:Int):Int{
    val c = if(a>b){
                  a
                }else{
                  b
                }
}
  • 而且我们可以进一步将上述代码简化一下
fun largeNumber(a:Int,b:Int) = if(a>b){
    a
}else{
    b
}

3.2 when条件语句

  • 与if一样,也是有返回值的,因此也可以使用语法糖
  • 使用方式:
when(params){
    匹配值 ->{执行逻辑}
}
  • 当执行逻辑只有一句话的时候,{}可以省略
  • 除了精确匹配外,when还支持类型匹配。类似于java中的instanceof。核心是is
fun checkNumber(num:Number) = when(num){
    is Int->println("is int")
    is Long->println("is Long")
    else->println("no such number")
}
  • when还有一种不带参数的用法
fun getScore_2(name:String) = when{
    name.startsWith("Tom") ->55
    name == "Jim" ->34
    name == "Lily" -> 35
    else->0
}
  • 可以发现Kotlin中判断字符串和对象是否相等可以直接使用==,而不用像java一样使用equals()

3.3 循环语句

3.3.1 while循环语句

  • kotlin中的while循环语句与java中的while循环语句完全相同。、

3.3.2 for循环语句

  • Kotlin中的for循环进行了很大的修改。Java中常用的for-i循环在kotlin中直接被舍弃了,而另一种for-each则被kotlin进行了大幅度的加强,变成了for-in循环
fun printNumber(){
    for(i in 0..10){
        println(i)  // 0 1 2 3 4 5 6 7 8 9 10
    }
    for(i in 0 until 10 ){
        println(i) // 0 1 2 3 4 5 6 7 8 9
    }
    for(i in 0 until 10 step 2){
        println(i) // 0 2 4 6 8
    }
    for(i in 10 downTo 1){
        println(i) // 10 9 8 7 6 5 4 3 2 1 
    }
}
  • Kotlin中新增了区间的概念:val rang = 0..10表示创建了一个0到10的区间,且两端都是闭区间。在..的两边指定区间的左右端点就可以创建一个区间了
    • for(i in 0..10)代表的是遍历0到10之间的数字
  • until关键字表示:创建一个左闭右开的区间
  • step的作用:for(i in 0 until 10 step 2)的意思是每次循环都会在区间范围内加2
  • downTo:创建一个降序闭区间

4. 继承与构造函数

4.1 继承

4.1.1 Kotlin中的继承与Java中的区别

  • Kotlin在继承这方面与java有些不同;Kotlin中任何一个非抽象类默认都是不可以被继承的,相当于java中的类被final修饰。
  • Effective Java中明确提到如果一个类不是专门为了继承而设计的,那么就应该主动将他加上final声明,禁止其可以被继承。

4.1.2 如何实现继承

  • 为了使Kotlin中的类可以被继承,我们需要用open关键字来修饰该类
open class Person{
    var name =""
    var age = 0
    fun eat(){
    }
}

class Student : Person(){
    var sno = 0
    var grade = 0
}

4.2 构造函数

  • 细品的话会发现,上述代码实现继承时,继承Person类后面还跟着一个括号,这个跟java中也不相同。这涉及到Kotlin中的主构造函数与次构造函数。
  • 在Java与Kotlin中,任何一个类都会有构造函数。在Kotlin中,构造函数被分为两类:主构造函数次构造函数

(1) 主构造函数:

  • 主构造函数是我们最常用的构造函数,每一个类都会有一个默认的不带参数的主构造函数。当然,我们也可以显示的给它指明参数。
  • 特点:没有函数体,直接定义在类名后面即可
class Student(var sno:String,var grade:Int) : Person(){
      //no func body
}
  • 如果我们需要创建一个Student对象,则只需要以下步骤,就能获取一个student对象了
val student = Student("1",1)
  • 虽然主构造函数没有函数体,但是如果我们又想在主构造函数中加入一些逻辑,这样该怎么办呢?Kotlin中提供了init结构体,所有主构造函数的逻辑可以写在里面
class Student(var sno:String,var grade:Int) : Person(){   
    init{
        //TODO
    }
}
  • 说了这么多还是没有涉及到Person后跟着的括号,那么到底有什么关系呢?这涉及到继承中的一个性质,子类的构造函数必须要调用父类的构造函数,这个规定在Kotlin中也要遵守。因此我们就会发现,既然Kotlin中主构造函数没有函数体,那我们该如何调用父类的构造函数呢?第一种办法,写在init结构体中,按理说这样可以,但是我们在大多数场景中是不需要写init结构体的;第二种,就是我们说的括号子类的主构造函数调用父类的哪个构造函数,在继承的时候通过括号来进行指定。
class Student(var sno:String,var grade:Int) : Person(){} 
//这个括号就代表调用的是Person中的无参构造函数
  • 即使在无参数的情况下,这对括号也不能省略。
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age){} 
//这个括号就代表调用的是Person中带有对应参数的构造函数
  • 注意,我们在向Student类的主构造函数中增加的nameage这两个字段时,不能再将它们声明成valvar,因为在主构造函数中声明成val或var的参数将自动成为该类的字段,这会导致与父类中的同名的参数发生冲突。因此,这里的nameage参数前面不用加任何关键字,使其作用仅限于主构造函数中即可。

(2) 次构造函数

  • 任何一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数可以用于实例化一个类,不过与主构造函数不同的是,它是有函数体的。
  • Kotlin中规定:次构造函数是通过constructor关键字来定义的,当一个类既有主构造函数又有次构造函数时,所有的次构造函数必须调用主题构造函数(包括间接调用)。举一个简单栗子:
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age){   
    //直接调用主构造函数
    constructor(name:String,age:Int):this("",0,name,age){
        //TODO
    }
    //间接调用,调用上一个次构造函数,间接调用主构造函数
    constructor():this("Tom",2){
        //TODO
    }
}
  • 还有一种特殊的情况:类中只有次构造函数,没有主构造函数。Kotlin中,当一个类没有显示定义主构造函数,且定义了次构造函数时,那么这个类就是没有主构造函数的。
class Teacher : Person{
    constructor(name:String,age:Int):super(name,age)
}
  • 我们来分析一下:首先,Teacher类中的后面没有显示的定义主构造函数,同时又因为定义了次构造函数,所以目前的情况下,Teacher类是没有主构造函数的。既然没有主构造函数了,继承Person类的时候也就不需要加上括号了。然后,由于没有主构造函数,次构造函数只能直接调用父类的构造函数了。

5. 接口与修饰符

5.1 接口

  • Kotlin中接口部分与Java中几乎是一致的。Java是单继承的,每一个类最多只能继承一个父类,但是却可以实现多个接口,Kotlin也是如此。
  • 接口中的函数不要求有函数体。
interface Study{
    fun readBook()
    fun doHomework()
}
  • 用之前定义的Student类进行举例,让其实现Study接口。
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age),Study{
    override fun readBook(){
        //TODO
    }
    
    override fun doHomework(){
        //TODO
    }
}
  • 观察上述代码可以发现,Kotlin中统一用冒号来表示继承和实现,中间用逗号进行分隔。并且接口后面不需要加上括号,因为它没有构造函数可以去调用。
  • Kotlin中为了让接口的功能更加灵活,增加了这样一个功能:允许对接口中定义的函数进行默认实现。所谓的默认实现是指:接口中的一个函数具有了函数体,这个函数体中的内容就是它的默认实现。以Study接口为例,一个类实现了Study接口时,只会强制要求实现readBook()函数,而doHomework()可以自由的选择是否实现,如果实现就是使用实现后的逻辑,如果不实现则使用默认的逻辑。
interface Study{
    fun readBook()
    fun doHomework(){
        println("do homework default impl")
    }
}

5.2 可见性修饰符

我们可以通过一个表格来了解Java中的修饰符与Kotlin中的修饰符的区别。

修饰符                    JAVA                             Kotlin
public                   所有类可见                        所有类可见(默认)

protected                当前类,子类,同                   当前类,子类可见
                         一包中的类可见

private                  当前类可见                        当前类可见

default                  同一包路径下的类                   无
                         可见(默认)
internal                 无                               同一模块中的类可见

6. 数据类与单例类

6.1 数据类

  • 数据类一般都占据着很重要的角色,用于将服务器端或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持。
  • Java中的数据类,也叫JavaBean类:
public class Phone{
    String brand;
    double price;
    public Phone(String brand,double price){
        this.brand = brand;
        this.price = price;
    }
    
    @Override
    public boolean equals(Object obj){
        //TODO
    }
    
    @Override
    public int hashCode(){
        //TODO
    }
    
    @Override
    public String toString(){
        //TODO
    }
}
  • Java中的数据类比较复杂,无意义代码较多。Kotlin中数据类的实现方式则极其简单:
data class Phone(val brand:String,val price:Double)
  • 是的,你没有看错,仅仅需要这一行代码,就能实现Kotlin中的数据类。神奇的地方就在于data这个关键字,在Kotlin中当一个类前面声明了data关键字时,就表明你希望这个类是一个数据类。Kotlin中会跟主构造函数中的参数帮你将equals()、hashCode()、toString()’等固定且无意义的方法自动生成。并且,我们可以发现这个类是没有大括号的,在Kotlin中,当一个类中没有任何代码时,还可以将大括号省略掉。

6.2 单例类

  • Kotlin中将固定的,重复的逻辑隐藏了起来,只暴露给我们最简单的用法。创建单例类时,仅需要将class更改为object即可。
object Singleton{
    fun singletonTest)(){
        //TODO
    }
}
  • 在Kotlin中我们不需要私有化构造函数,也不需要提供getInstance()这种静态方法,只需要把class关键字改为object即可。调用方法为SingleTon.singletonTest()

7. Lambda编程

7.1 集合的创建与遍历

7.1.1 Kotlin中创建list与set集合

  • Kotlin可以与Java中创建ArrayList集合的方式相同
val list = ArrayList<String>()
list.add("one")
list.add("two")
list.add("three")
}
  • 但对于Kotlin来说,上述这种方式比较繁琐,Kotlin中内置了listOf()来简化初始化集合的方法。仅用一行代码就可以实现集合的初始化操作。不过需要注意的是,listOf()函数创建的是不可变的集合,意思是被创建的集合无法进行添加,删除或修改操作
val list = listOf("one","two","three")
  • 如果我们需要创建一个可变集合的话,使用mutableListOf()函数即可.。
val list = mutableListOf("one","two","three")
  • 创建Set集合的方法与List集合几乎一模一样。只是将创建集合的方式更换为setOf()mutableSetOf()。需要注意的是由于set集合底层是使用hash映射机制来存储的,因此set集合中的元素是无序的

7.1.2 Kotlin中创建Map集合

  • Kotlin可以与Java中创建map集合的方式相同
fun createMap(){
    val map = HashMap<String,Int>()
    map.put("one",1)
    map.put("two",2)
    map.put("three",3)
}
  • 但是Kotlin中其实不建议使用put()get()方法来对Map进行添加和读取数据操作,而是更加推荐使用一种类似于数组下标的语法结构。
fun createMap2(){
    val map = HashMap<String,Int>()
    map["one"] = 1
    map["two"] = 2
    map["three"] = 3
}
  • 但是Kotlin依然觉得这种方式太麻烦了,因此如同list与set一样,Kotlin依然提供了一对mapOf()mutableMapOf()方法来简化Map。我们以mapOf()来举例。
val map = mapOf("one" to 1,"two" to 2,"three" to 3)
  • 遍历map集合的方法依然是forin。只不过需要把一对(key,value)放在变量的位置上。
val map = mapOf("one" to 1,"two" to 2,"three" to 3)
    for((numName , num) in map){
        println("numName is " + numName +" ,num is" + num )
    }

7.2 集合的函数式API

7.2.1 Lambda表达式

1. 首先学习函数式API的语法结构,也就是Lamdba表达式的语法结构。

  • 按照我们在java中的思路,如果我们想要找出单词最长的水果名,这个算法该如何写呢
fun maxLength(){
    val list = listOf<String>("Apple","Banana","Pear")
    var maxLengthFruit="" 
    for(fruit in list){
        if (maxLengthFruit.length<fruit.length){
            maxLengthFruit = fruit
        }
    }
}
  • 但是在Kotlin中我们可以使用Lambda表达式来使这段代码更简洁
fun maxLengthLambda() {
    val list = listOf<String>("Apple","Banana","Pear")
    var maxLengthFruit = list.maxBy { it.length }
}
  • 只用一行代码就能完成这个功能

2. 理解Lambda表达式的简化步骤

(1)Lamda的定义:一小段可以作为参数传递的代码
(2)语法结构:

{paramsName 1: paramsType,paramsName 2: paramsType -> funcBody}

  • 首先,最外层是一层大括号
  • 如果有参数传入的话,我们还需要声明参数列表
  • 参数列表的结尾用->表示参数列表的结束语函数体的开始
  • 函数体中可以编写任意行代码(但不建议太长),并且最后一行代码会自动作为Lambda表达式的返回值
(3)正常的Lambda表达式
  • 在刚刚的Lambda表达式中使用的maxBy函数就是一个普通的函数,只不过接收的是Lambda类型的参数,并且在遍历集合时将每次遍历的值作为参数传递给Lambda表达式。这个函数的工作原理是:根据我们传入的条件来遍历集合,从而找到该条件下的最大值
fun maxLengthLambdaNormal() {
    val list = listOf<String>("Apple","Banana","Pear")
    var lambda = {fruit:String -> fruit.length}
    var maxLengthFruit = list.maxBy(lambda)
}
(4) 开始简化
  • 第一步,我们并不需要定义一个Lambda变量,可以直接将这个表达式传入函数中
fun maxLengthLambdaNormal() {
    val list = listOf<String>("Apple","Banana","Pear")
    var maxLengthFruit = list.maxBy({fruit:String -> fruit.length})
}
  • 第二步,在Kotlin中规定,当Lambda表达式作为函数的最后一个参数时,可以将其移到括号外边
fun maxLengthLambdaNormal() {
    val list = listOf<String>("Apple","Banana","Pear")
    var maxLengthFruit = list.maxBy(){fruit:String -> fruit.length}
}
  • 第三步,如果lambda表达式是函数的唯一一个参数时,还可以将函数的括号省略
fun maxLengthLambdaNormal() {
    val list = listOf<String>("Apple","Banana","Pear")
    var maxLengthFruit = list.maxBy{fruit:String -> fruit.length}
}
  • 第四步,由于Lambda具有出色的类型推导机制,因此大多数情况Lambda表达式中的参数列表其实在大多数情况下不必声明参数类型。
fun maxLengthLambdaNormal() {
    val list = listOf<String>("Apple","Banana","Pear")
    var maxLengthFruit = list.maxBy{fruit -> fruit.length}
}
  • 第五步,当Lambda表达式中只有一个参数时,也不必声明参数名,而是可以直接使用it来代替,那么代码就会变成最初的那样
fun maxLengthLambdaNormal() {
    val list = listOf<String>("Apple","Banana","Pear")
    var maxLengthFruit = list.maxBy{ it.length}
}

7.2.2 函数式API的相互配合

1. map函数与filter函数

  • 集合中的map函数是非常常用的一种数据结构,用于将集合中的每个元素都映射成另外的值,映射的规则由Lambda表达式指定,最终形成一个新的集合。
  • 举个🌰;这样会将原list中的单词全部变为大写模式,除此之外,我们还可以将单词全转换为小写,或只取单词首字母,结果如何是根据Lambda中的规则来的。
    val list = listOf<String>("Apple","Banana","Pear")
    var newList = list.map{ it.toUpperCase()}
    for(fruit in newList){
        println(fruit)
    }
  • filter函数是用来过滤集合中的数据的,并将过滤后的数据返回为一个新的集合。可以单独使用,也可以配合刚才的map一起使用。
    val list = listOf<String>("Apple","Banana","Pear")
    var newList = list.filter{it.length<=5}.map{it.toUpperCase()}
    for(fruit in newList){
        println(fruit)
    }

2. any与all函数

  • any函数是用来判断集合中是否至少存在一个元素满足指定条件。
  • all函数是用来判断集合中是否所有元素都满足指定条件。
fun anyAndall(){
    val list = listOf<String>("Apple","Banana","Pear")
    var anyResult = list.any{it.length<=5}
    var allResult = list.all{it.length<=5}
    println("anyResult is " + anyResult+",allResult is " + allResult)
}

7.2.3 Java函数式API的使用

  • 如果我们在Kotlin代码中调用了一个java方法,并且让方法接收一个Java单抽象方法接口,这种情况下可以使用函数式API。
  • 单抽象方法接口:接口中只有一个抽象方法
  • 以Java原生API中一个最常见的单抽象方法为例--Runnable接口。该接口中只有一个待实现的run()方法。在java中使用Runnable接口的方法是这样的:
    new Thread(new Runnable(){
        @Override
        public void run(){
            //func body
        }
    }).start();
  • 如果将Java版本的代码改为Kotlin版本,那么结果是什么呢?
    Thread(object:Runnable{
        override fun run(){
            println("Hello ")
        }
    }).start()
  • 由于Kotlin完全舍弃了new关键字,因此创建匿名类实例的时候不能在使用new,而是改用了object关键字。但是目前Thread的构造方法是符合Java函数式API的使用条件的,因此我们可以对其进行精简。一下这段代码就是精简的结果。因为Runnable类中只有一个待实现方法,即便没有显示的重写run()方法,Kotlin也能自动明白Runnable后面的Lambda表达式就是要在run()方法中实现的内容。
    Thread(Runnable{
            println("Hello ")
    }).start()
  • 另外,如果一个Java方法的参数列表不存在一个以上Java单抽象方法参数接口,我们还可以将这个接口省略
    Thread({
            println("Hello ")
    }).start()
  • 精简还未结束!别忘了,当Lambda表达式是方法的最后且唯一一个参数时,可以将Lambda表达式移到方法括号外边,且将方法的括号省略。那么最终结果就如下:
    Thread{
            println("Hello ")
    }.start()
  • 本小节中的Java函数式API的使用都限定于从Kotlin中调用Java方法,并且单抽象方法接口也必须是用Java语言定义的。在Kotlin中会经常调用android的SDK,而因为android中的SDK都是用Java来进行编写的,因此在调用这些SDK接口会经常用到Java函数式API。举个栗子,Android中有一个极为常用的点击事件接口OnClickListner。如果我们用Java代码来注册这个点击事件,需要这么写:
button.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onClick(View v){
        //funcbody
    }
})
  • 而用Kotlin代码实现同样的功能,就可以使用函数式API的思想来对上述代码进行简化,结果如下。是不是很简单
button.setOnClickListener{
        //funcbody
    }

8 空指针检查

在刚开始的简介就说过,在Kotlin中几乎杜绝了空指针问题的出现。Kotlin利用编译时判空检查的机制几乎杜绝了空指针异常,所谓的编译时判空检查机制就是将空指针异常的检查从运行时提前到了编译期,如果程序存在空指针异常的风险,那么在编译时会自动报错。

  • 以之前的一个函数为例
fun doStudy(study:Study){
    study.readBooks()
    study.doHomework()
}

  • 这段代码看上去和Java版本的代码并没有什么区别,但是它是没有空指针异常的风险的。因为Kotlin默认所有的参数和变量都不可为空,所以这里传入的study参数也一定不会为空。但是,这种方法虽然避免了空指针异常的出现,又会导致另一个问题,如果我们需要某个传入的参数或变量为空的话该怎么办呢?为了解决上面这个问题,Kotlin提供了另外一套可为空的类型,只不过这套方法需要我们在编译期就将所有的潜在的空指针异常都处理掉,否则代码将无法编译通过。
  • 可为空的类型系统是指:在类名后面加一个问号。例如:
Int:表示不可为空的类型 
Int?:表示可为空的类型
String:表示不可为空的类型
String?:表示可为空的类型
  • 继续拿上面的函数举例:如果我们希望传入的参数一可以为空,那么就应该将参数的类型从Study改为Study?。但是继续以下的写法的话又会报出错误提示。
    fun doStudy(study:Study?){
    study.readBooks()
    study.doHomework()
    }
  • 理由很简单,由于我们将参数改为了可空的类型,那么调用参数的方法就会有可能造成空指针异常,因此Kotlin在这种情况下不允许编译通过。处理方法为,把空指针异常都处理掉就可以了,做个判空处理。
fun doStudy(study:Study?){
    if(study!=null){
       study.readBooks()
       study.doHomework()
    }
}

8.1判空辅助工具

8.1.1 ?.操作符

这个操作符的作用非常好理解,就是当对象不为空的时候正常调用相应的方法,当对象为空时则什么也不做。比如以下处理。

if(a!=null){
       a.readBooks()
    }
简化为
a?.readBooks()

8.1.2 ?:操作符

这个操作符的左右两边都接收一个表达式,如果左边表达式不为空则返回左边表达式的结果,否则就返回右边表达式的结果。

    val c = if(a!=null){
        a
    }else{
        b
    }
    简化为
    val c = a?:b
  • 用一个例子函数来讲上述两个辅助工具结合一下。假如我们要获取一个字符串的长度,如果不用判空工具的话是如下这种写法。
    fun getTextLength(text:String?) : Int{
        if(text!=null){
            return text.length
        }
       return 0
    }
  • 如果要用操作符进行简化的话,首先,text是可能为空的,因此我们在调用其length字段时需要使用?. 操作符,可以简化为text?.length;其次,text?.length返回值为null,那我们就可以借用?:操作符使其返回值为0。
    fun getTextLength(text:String?) = text?.length?:0

8.1.3 !!操作符

用一段代码来解释这个操作符的作用。我们先定义一个可为空的全局变量content,然后将其变为大写模式

var content:String?="Hello"

fun main() {
    if(content!=null){
            printUpperCase()
        }
}
fun printUpperCase(){
        val upperCase = content.toUpperCase()
        println(upperCase)
}
  • 上述代码看起来是没有问题的,但是遗憾的是这段代码一定是无法正常运行的。因为printUpperCase()函数无法知道外部已经对content进行了非空检查,所以编译的时候会认为存在空指针风险,导致无法编译通过。
  • 这种情况下,就可以用到!!操作符了,又名非空断言工具,写法是在对象的后面加上!!。这是一种有风险的写法,意在告诉Kotlin,我非常确信这里的对象不会为空,所以不用帮我来做空指针检查了。
fun printUpperCase(){
        val upperCase = content!!.toUpperCase()
        println(upperCase)
}

但是这并不是一种好的实现方法,因为每一次使用非空断言工具的时候,就有可能出现空指针异常。所以在使用非空断言工具的时候最好提醒一下自己,是不是有更好的实现方式。

8.1.4 let辅助工具

let既不是操作符,也不是什么关键字,而是一个函数。该函数提供了函数式API的编程接口,并将原始调用对象作为参数传递给Lambda表达式中。

obj.let{
        obj ->
        //编写具体业务
}
  • 这里调用了obj对象的let函数,然后Lambda表达式中的代码会立即执行,并且将这个obj对象本身还会作为参数传递到Lambda表达式中。

但是我们知道,这一小节是来介绍判空辅助工具的,那么let与空指针检查有什么关系呢?以之前的doStudy()函数举例

fun doStudy(study:Study?){
       study?.readBooks()
       study?.doHomework()
}
  • 细品的话,本来我们进行一次if判断就能随意调用study对象的任何方法,但是受制于?.操作符的限制,现在变成了每次调用study对象的方法都需要进行一次if判断。那么这个时候就可以结合let函数对代码进行优化了。
fun doStudy(study:Study?){
    study?.let{ stu ->
        stu.doHomework()
        stu.readBook()
    }
}
0
  • ?.操作符的作用下,对象为空时就什么都不做,不为空时则调用let函数将study对象本身作为参数传递到Lambda表达式中,此时study对象本身肯定不为空了。并且根据Lambda语法的特性,当Lambda表达式的参数列表中只有一个参数时,可以不用声明参数名,直接使用it关键字来代替即可,我们可以进一步简化代码。
fun doStudy(study:Study?){
    study?.let{ 
        it.doHomework()
        it.readBook()
    }
}

9.Kotlin中的小技巧

9.1 字符串内嵌表达式

Kotlin中添加了该功能,弥补上了java上相关功能的缺憾。不需要再复杂的拼接字符串了。

"hello , ${obj.name}.Nice to meet you"
  • 可以看到,Kotlin中允许我们在字符串中嵌入${}这种语法结构的表达式,并在运行时使用表达式的结果来替代这一部分。另外,当表达式中仅有这一个变量的时候,还可以将两边大括号给省略掉。
"hello , $name.Nice to meet you"
  • 加入我们要输出一串带变量的字符串,在原来的方法中,输出的写法应该是这样的
val name = a
val age = 10
println(“people(name = ” + name + ",age = " + age + ")")
  • 使用字符串内嵌表达式后,可以简化为
val name = a
val age = 10
println(“people(name =$name , age=$age)")

9.2 函数的默认参数值

在前面讲解次构造函数用法的时候就提到过,次构造函数在Kotlin中很少用,因为Kotlin中提供了给函数设定默认值的功能,在很大程度上能够替代次构造函数的作用。具体来讲,就是在定义函数的时候给任意参数设定一个默认值,这样调用此函数的时候就不会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。

  • 方法如下:
fun printParams(num:Int,str:String = "hello"){
    println("num is $num,str is $str")
}
  • 如果我们使用printParams(10)来调用该方法,会得到如下结果。
num is 10,str is hello
  • 但是上面的例子比较理想化,如果我们向要让第一个参数设定默认值,第二个参数使用赋值呢?
fun printParams(num:Int = 10,str:String){
    println("num is $num,str is $str")
}
  • 模仿刚刚的写法肯定是不可以的,因为编译器会认为我们想把字符串赋值给第一个变量,从而报类型不匹配的错误。而Kotlin提供的另一套比较神奇的机制,就是通过键值对的方式来传递参数,不用像传统方法那样按照参数定义的顺序来传递参数。我们可以写成这样:
printParams(str = "hello",num = 12)
  • 此时哪个参数在前哪个参数在后都无所谓,Kotlin可以准确的将参数匹配上。使用这种键值对的传参方式之后,我们就可以省略num参数了,代码如下:
fun printParams(num:Int = 10,str:String){
    println("num is $num,str is $str")
}
fun main(){
    printParams(str = "hello")
}

//输出值为
num is 10,str is hello

那么为什么说给函数参数设置默认值可以很大程度上代替次构造函数的作用的?

  • 前边学习次构造函数的代码
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age){   
    //直接调用主构造函数
    constructor(name:String,age:Int):this("",0,name,age){
        //TODO
    }
    //间接调用,调用上一个次构造函数,间接调用主构造函数
    constructor():this("Tom",2){
        //TODO
    }
}
  • 次构造函数在这里的作用是提供更少的参数来对Student类进行实例化的方式。无参的次构造函数会调用两个参数的次构造函数,并将这两个参数赋值成初始值。两个参数的次构造函数会调用4个参数的主构造函数,将缺失的参数赋值为初始值。但是学习了给参数设置默认值,就完全用不到上述方式了。我们只需要编写一个主构造函数,并且给参数设置默认值的方式来实现。
class Student(var sno:String="",var grade:Int =0,name:String="",age:Int=0) : Person(name,age){}

9.3 Kotlin中使用findViewById()

  • 加入要使用我们定义的一个button,在kotlin中的调用方法是这样的
    val button:Button = findViewById(R.id.button)
    button.setOnClickListener{
        //funcBody
    }

findViewById()方法返回的是继承View的泛型对象,因此Korlin无法自动推断出它是一个Button还是其他控件。因此我们需要将button变量显示的声明成Button类型。但是如果在布局文件中有十个控件,我们就需要重复调用十次findViewById()来获取这些控件,这无疑非常麻烦。

  • 但是Kotlin中不用再代码中重复调用 findViewById()方法来获取控件了,因为使用Kotlin编写的安卓项目会在app.gradle文件的头部默认引入了一个插件,该插件会根据布局文件中定义的控件id自动生成一个具有相同名称的变量,我们在Activity中可以直接使用这个变量,而不需要再调用findViewById()方法了。

9.4 使用get和set方法的语法糖

  • JavaBean
public class Book{
    private int pages;
    public int getPages(){
        return pages;
    }
    public void setPages(int pages){
        this.pages = pages;
    }
}
  • 在Java中调用Book类中的pages字段时,需要使用getPages()和setPages(int pages)方法。而在Kotlin中调用这种语法结构时,可以使用一种更简单的写法,比如用下面这种代码来设置Book中的pages字段
    val book = Book()
    book.pages = 500
    val bookPages = book.pages
  • 这里看上去好像我们并没有调用Book类中的setPages()与getPages()方法,而是直接对pages字段进行了赋值和读取。其实这就是Kotlin中的语法糖,他会在背后自动将上述代码转换成调用setPages()方法和getPages()方法

10. 标准函数与静态函数

10.1 标准函数

我们之前所学的let函数,其实就是标准函数,其主要作用就是配合?.操作符进行判空辅助处理。而标准函数其实是指在Standard.kt中定义的函数,任何Kotlin代码都可以自由的调用所有的标准函数。我们先主要掌握几个常用的标准函数with,apply,run标准函数。

(1)with函数

  • 参数:第一个参数可以为任何对象,第二个参数是一个Lambda表达式。
  • 原理:with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用最后一行代码作为返回值返回。以下面的代码为例
val result = with(obj){
    //这里是obj的上下文
    value//with函数的返回值
}
  • 作用:我们可以在连续调用同一个对象的多个方法时让代码变得更加精简。
val list = listOf("one","two","three")
val result = with(StringBuilder()){
    append("start \n")
    for(fruit in list){
        append(fruit + "\n")
    }
    toString()
}
println(result)

(2)run函数

该函数的用法和使用场景其实和with函数时非常类似的,只是在参数与调用方法上有所区别。

  • 首先,run函数是不能直接调用的,而是一定要被某个函数所调用才行。
  • 其次,run函数只接受一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。
val list = listOf("one","two","three")
val result = StringBuilder().run(){
    append("start \n")
    for(fruit in list){
        append(fruit + "\n")
    }
    toString()
}
println(result)

(2)apply函数

apply函数与run函数也及其类似,需要被某个对象调用,但无法指定返回值,只能返回调用对象本身。

val list = listOf("one","two","three")
val result = StringBuilder().apply{
    append("start \n")
    for(fruit in list){
        append(fruit + "\n")
    }
}
println(result.toString()) //此时result为StringBuilder()对象

(3)repeat函数

可以直接被调用,不需要通过对象,接受两个参数,第一个参数为数值n,第二个参数为Lambda表达式,然后会将Lambda表达式中的内容执行n次

10.2 静态函数

静态方法又叫做类方法,指的是那种不需要创建实例就能调用的方法。Java中定义静态方法只需要在函数声明时加上static关键字即可,但是Kotlin却极度弱化了静态方法这个概念。之所以要这样设计,是因为Kotlin提供了更好的语法特性,那就是单例类

10.2.1 单例类

object util{
    fun doAction(){
    }
}
  • 注意,doAction()方法实际上并不是静态方法,只是可以使用类似于调用静态方法的方式来调用,Util.doAction()。单例类的写法会将整个类中的所有方法全都变成类似于静态方法的调用方式,但是如果我们只想让一个或几个方法变成静态方法的调用方式话,这样就很不方便。这时就需要伴生关键字companion object

10.2.2 伴生关键字companion object

class Util{
    fun doAction1(){}
    
    companion object{
        fun doAction2(){}
    }
}
  • 以上两个函数的调用方法分别为:Util().doAction1(),Util.doAction2()
  • 但是doAction2()方法其实也并不是静态方法,companion object改关键字实际上会在Util类内部创建一个伴生类,doAction2()方法就是定义在这个伴生类中的实例方法。只是Kotlin会保证Util类始终只会存在一个伴生类对象
  • 由此可以看出,Kotlin中确实没有可以直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法。

10.2.3 注解@JvmStatic声明静态方法

之前也说过,单例类与companion object都不是真正的单例类,而如果我们在这些方法上加上@JvmStatic注解,kotlin会将这些方法编译成真正的静态方法

class Util{
    fun doAction1(){
        
    }
    
    companion object{
        // 经过该关键字修饰后,doAction2()方法为真正的静态方法
        @JvmStatic
        fun doAction2(){
            
        }
    }
}
  • @JvmStatic关键字只能加在单例类或companion object中的方法上,如果加在普通方法上会提示语法错误

10.2.4 顶层方法

顶层方法指的是没有定义在任何类中的方法,Kotlin会将所有的顶层方法全部编译成静态方法

//新建一个Kotlin文件,在该文件中直接定义方法
    fun doAction1(){
        
    }
  • 如果是在Kotlin中调用的话,所有的顶层方法都可以在任何位置上被直接调用,不用管包名,实例,直接输入doAction1()即可
  • 如果是java中,是无法直接调用的,会找不到这个方法,因为Java中没有顶层方法这个概念,kotlin会将顶层方法所在的文件创建一个类,比如该文件名为Help.kt,那么就会新建一个HelpKt.class的Java类,doAction1()就是以静态方法写在该类中的

11. 小总结

  • 类型强制转换符 为as

12. 延迟初始化和密封类

12.1 对变量的延迟初始化

在开发中,如果类中存在很多全局变量实例,为了保证他们能满足Kotlin中的空指针检查语法标准,我们不得不在代码中坐很多非空判断保护才行,比如

class TestClass {
 
    fun doXxx() {
        // TODO:
    }
}

class TestActivity : AppCompatActivity(), View.OnClickListener {
 
    private var mTestClass: TestClass? = null
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        mTestClass = TestClass()
    }
 
    override fun onClick(v: View?) {
        mTestClass?.doXxx()
    }
}

将 mTestClass 设置为全局变量,但是它的初始化工作是在 onCreate() 函数中进行的,因此不得不先将 mTestClass 赋值为 null,同时把它的类型声明成 TestClass?。
虽然在 onCreate() 函数中对 mTestClass 进行初始化,同时能确保 onClick() 函数必然在 onCreate() 函数之后才会调用,,但是在 onClick() 函数中调用 mTestClass 的任何函数时仍然要进行判空处理才行,否则编译肯定无法通过。
当代码中有了越来越多的全局变量实例时,这个问题就会变得越来越明显,到时候可能必须编写大量额外的判空处理代码,只是为了满足 Kotlin 编译器的要求。解决办法就是使用全局变量进行延迟初始化。

  • 延迟初始化的关键字为lateinit,该关键字可以告诉编译器,我会在晚些时候对这个变量进行初始化,那么这样就不用一开始声明的时候就将该变量赋值为null了。
class TestActivity : AppCompatActivity(), View.OnClickListener {
 
    private lateinit var mTestClass: TestClass
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        mTestClass = TestClass()
    }
 
    override fun onClick(v: View?) {
        mTestClass.doXxx()
    }
}
  • 但是,使用lateinit关键字不是没有风险的,当对一个全局变量使用lateinit关键字时,一定要保证在任何情况,任何位置调用的时候,该关键字已经被赋值了,否则程序一定会崩溃

另外,我们还可以通过代码isInitialized来判断一个全局变量是否已经被初始化了,这样在某些时候能有效的避免对某一个变量重复的进行初始化工作,比如

class TestActivity : AppCompatActivity(), View.OnClickListener {
 
    private lateinit var mTestClass: TestClass
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
       
        //判断mTestClass是否已经被初始化
        //::mTestClass.isInitialized的写法看上去有点奇怪,但这是固定的写法
        if (!::mTestClass.isInitialized) {
            mTestClass = TestClass()
        }
    }
 
    override fun onClick(v: View?) {
        mTestClass.doXxx()
    }
}

12.2 密封类

网上一篇对密封类介绍的比较详细的文章

13. 扩展函数与运算符重载

13.1 扩展函数

定义:表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数

  • 以一个功能为例,如果我们想统计字符串中字母的数量,那么我们该如何实现这个功能呢?
object StringUtil{
    fun count(str:String?) : Int{
        var count = 0
        str?.let{
            for(char in it){
                if(char.isLetter()){
                    count++
                }
            }
            return count
        }
        return 0
    }
}
  • 上面这种写法没有问题,可以正常使用。但是有了扩展函数后,我们就可以用扩展函数将count()方法添加到String类中。

定义扩展函数的语法结构,相比于定义一个普通的函数,定义扩展函数只需要在函数名的前面加上类名.的语法结构,就表示将该函数添加到指定类中了

fun className.merthodName(param1:Int,param2:Int):Int{
    return 0
}
  • 使用扩展函数将count()函数添加进String类中的方法:向哪个类添加扩展函数就定义一个同名的Kotlin文件,由于我们希望向String类中添加扩展函数,那么我们就需要先创建对应的Kotlin文件。在Kotlin文件中编写如下代码
fun String.count():Int{
    var count = 0
        this?.let{
            for(char in it){
                if(char.isLetter()){
                    count++
                }
            }
            return count
        }
        return 0
}
  • 我们将count()方法定义成String类的扩展函数,那么该函数就自动拥有了String实例的上下文,因此该函数就不用接收一个字符串参数了,而是直接遍历this即可,因为this就代表着字符串本身。而我们之后就可以使用该扩展函数了
    val count = "12s2w3e".count()

13.2 运算符重载

  • PS:本人觉得没太大用,虽然简化了代码量,但是增加了代码阅读的困难度,不适用于公共开发。

14. 高阶函数

14.1 定义高阶函数

高阶函数与Lambda的关系是密不可分的。像map,rum,apply这种接收Lambda参数的函数可以被称为具有函数式编程风格的api,如果我们想自己定义函数式api,就得借助高阶函数来实现。
什么是高阶函数?
所谓高阶函数,就是一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就可以被称为高阶函数

  • 高阶函数需要以另一个参数作为参数或者是返回值,那么怎么以一个函数作为参数呢?这就涉及到Kotlin新增的另一个概念:函数类型。这个函数类型类似于整形,浮点型,布尔型,是Kotlin中新增的。定义一个函数类型的方式如下:
(String, Int) -> Unit
  • 定义一个函数类型,最关键的是要声明该函数接收什么参数,与它的返回值类型是什么。因此,->左边表示声明该函数需要传入的参数,多个参数之间使用逗号隔开,如果没有参数,使用一对空括号就可以了,->右边表示声明该函数的返回值是什么类型,如果没有返回值就使用Unit,它大致相当于Java中的void,以下边函数为例
fun example(func:(String,Int)->Unit){
    //     函数名    参 数     返回值
    func("hello",123)
}
  • 可以看到example()函数接收了一个函数类型的参数,因此example()函数就是一个高阶函数。调用一个函数类型的参数,它的语法类似于调用一个普通的函数。但是上面这个例子没有办法直观的体现出高阶函数的作用,那么这种函数具体有什么用途呢?
  • 简单概括一下高阶函数的用途:高阶函数允许让函数类型的参数来决定函数的执行逻辑,即使是在同一个高阶函数,只要传入不同的函数类型参数,那么它的执行逻辑与最终的返回结果就可能是完全不同的。举个下面这个栗子说明。

定义一个num1Andnum2()的函数,参数类型为两个整形与一个函数类型。再定义两个与其函数类型匹配的函数。

fun num1Andnum2(num1:Int,num2:Int,operation(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
}

在main函数中编写如下代码

fun main() {
    val num1 = 5
    val num2 = 10
    val result1 = num1Andnum2(5,10,::plus)
    val result2 = num1Andnum2(5,10,::minus)
    println("result1= "+ $result1)
    println("result2=" + $result2)
}
  • 注意这里调用num1Andnum2()函数的方式,第三个参数使用了::plus::minus这种写法,表示将这两个函数作为参数传递给num1Andnum2()函数,然后num1Andnum2()函数根据传入的函数类型参数决定具体的计算逻辑。

这种写法虽然可以正常工作,但是写法是不是太复杂了,每次调用任何高阶函数的时候都还需要定义一个与其函数类型相匹配到的函数,没有起到简化代码的作用。因此,kotlin支持更多的方式来调用高阶函数,比如Lambda表达式,匿名函数,成员引用等。其中Lambda表达式是最常见最普遍的高阶函数调用方式

  • 将上述方法用Lambda表达式来实现,可以写为:
fun main() {
    val a = 10
    val b = 5
    val result1 = num1AndNum2(a,b){a,b -> a + b}
    val result2 = num1AndNum2(a,b){a,b -> a - b}
    println("result1 is $result1 ")
    println("result2 is $result2 ")
}
  • Lambda表达式同样可以完整地表达一个函数的参数声明和返回值声明,但是写法会更加简单

继续探究高阶函数的使用。回顾之前之前学习的apply函数,该函数可以给Lambda表达式提供指定的上下文,当需要连续调用同一个对象的多个方法时,这个函数可以让代码变得更加精简。比如StringBuilder,学习了高阶函数后我们就可以用高阶函数来模仿一个类似的功能。

fun StringBuilder.build(block : StringBuilder.() -> Unit):StringBuilder{
    block()
}
  • 这里给StringBuilder定义了一个扩展函数,该扩展函数接收一个函数类型参数,返回值为StringBuilder类型。
  • 注意,声明该高阶函数与之前的例子又有些不同:他在函数类型参数前面加了个StringBuilder.,这是什么意思呢?在函数类型参数的前面加上ClassName,就表示这个函数类型参数定义在对应的类中,这里就是讲函数类型参数定义在StringBuilder中。
  • 但是这样子有什么好处呢?好处就是,当我们调用build函数时传入的Lambda表达式会自动拥有StringBuilder的上下文,与apply函数非常相似。

14.2 内联函数

14.2.1 高阶函数的实现原理

  • 仍然用刚刚的 num1Andnum2函数举例,代码如下
fun main() {
    val a = 10
    val b = 5
    val result1 = num1AndNum2(a,b){a,b -> a + b}
    val result2 = num1AndNum2(a,b){a,b -> a - b}
    println("result1 is $result1 ")
    println("result2 is $result2 ")
}

fun num1Andnum2(num1:Int,num2:Int,operation(Int,Int)->Int){
    val result = operation(num1,num2)
    return result
}
  • 上述代码调用了 num1Andnum2函数,并通过Lambda表达式指定对传入的两个整形参数进行求和。但是上述调用方法再Kotlin中比较好理解,比较基础的高阶函数用法。可是Kotlin最终还是要编译成Java字节码的,但是Java中并没有高阶函数的概念,那么Kotlin是如何让Java来支持这种高阶函数的用法的?Kotlin强大的编译器会将这些高阶函数的语法转换成Java支持的语法结构,上述的Kotlin代码大致会被转换成如下Java代码
public static int num1AndNum2(int num1,int num2,Function operation){
    int result = (int)operation.invoke(num1,num2)
    return result;
}

public static void main(){
    int num1 =100;
    int num2 =80;
    int result = num1AndNum2(num1,num2,new Function(){
       @Override
        public Integer invoke(Integer n1,Integer n2){
            return n1 + n2;
        }
    });
}
  • 这就表明,我们一直使用的Lambda表达式在底层被转换为了匿名类的实现方式,每当调用一次Lambda表达式,都会创建一个新的匿名对象,造成额外的性能开销。为了解决这个问题,Kotlin提供了内联函数的功能,可以将Lambda表达式带来的运行时开销完全消除。

定义内联函数的方式很简单,定义高阶函数时加上inline关键字的声明即可。内联函数的工作原理就是,Kotlin编译器会将内联函数中的代码在编译时自动替换到调用它的地方

inline fun num1AndNum2(num1:Int , num2 : Int , operation : (Int , Int) -> Int) : Int{
    val result = operation(num1,num2)
    return result
}

14.2.2 noinline和crossinLine

noinline

之前讨论的情况是,一个高阶函数只接受了一个函数类型参数,如果一个高阶函数中接受了两个或更多的函数类型参数,这是我们加上inline关键字,Kotlin编译器会将所有引用的Lambda表达式全部替换,但是我们只想内联其中一个的话该怎么办呢?这时,我们可以使用noinline关键字

inline fun inlineTest(block1 : () -> Unit , noline block2 : () -> Unit){}
  • 原本block1与block2所引用函数类型都会被内联,但我们在block2参数的前面加上noinline关键字,那么就只会对block1参数所引用的Lambda表达式进行内联了
  • 但是我们在上一小节已经说完内联的好处了,它可以减少系统的开销,那么为什么我还用noinline关键字取消内联呢?因为内联的函数类型参数在编译的时候会进行代码替换,因此它没有真正的参数属性,而非内联的函数类型参数可以自由地传递给其他函数,因为它就是一个真是的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性,因此可以这么理解内联函数=一段代码,非内联函数=一个真正的函数参数
  • 另外,内联函数与非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中可以使用return关键字返回,非内联函数只能使用局部返回
    fun printString(string: String, block1: (String) -> Unit) {
        printlin("printString start")
        block1(string)
        printlin("printString end")
    }

    fun main() {
        println("main start")
        val str = ""
        printString(str){s ->
            println("lambda start")
            if(s.isEmpty())return@printString
            println(s)
            println("lambda end")
        }
        println("main end")
    }
  • 以上函数的结果为除了“lambda end”这句没有输出,其余皆输出。注意的是,Lambda表达式中是不允许直接使用return关键字的,代码中的return@printString代表局部返回
  • 但是如果我们将printString()函数声明成一个内联函数,那么情况就不一样了
    inline fun printString(string: String, block1: (String) -> Unit) {
        printlin("printString start")
        block1(string)
        printlin("printString end")
    }

    fun main() {
        println("main start")
        val str = ""
        printString(str){s ->
            println("lambda start")
            if(s.isEmpty())return
            println(s)
            println("lambda end")
        }
        println("main end")
    }
  • 输出的结果为除"lambda end"与"main end"外全打印。因为内联函数本质上是代码替换,因此可以将上述函数写为
    fun main() {
        println("main start")
        val str = ""
        printlin("printString start")
        println("lambda start")
        if(s.isEmpty())return
        println(s)
        println("lambda end")
        printlin("printString end")
        println("main end")
    }
crossinline
  • 将高阶函数声明为内联函数是一种良好的编程习惯,大多数高阶函数都可以直接声明为内联函数的,但是也有少数情况,如下。
    inline fun runRunnable(block()->Unit){
        val runnable = Runnable{//本质上为匿名对象,只能使用局部返回
            block()//内联函数,可以使用return
        }
        runnable.run()
    }
  • 这个出现错误的原因比较复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runna ble的Lambda表达式中调用了传入的函数类型参数,之前也讲过,编译器实际上会将Lambda表达式编译为匿名内部类的方式,也就是说上述方法是在匿名对象中调用了传入的函数类型参数。
  • 内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名内部类中调用的函数类型参数,最多只能对匿名类中的函数调用进行返回。因此如果我们在高阶函数中创建了另外的Lambda表达式或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明为内联函数就一定会出错。这时就需要使用 crossinLine关键字。
    inline fun runRunnable(crossinLine block()->Unit){
        val runnable = Runnable{//本质上为匿名对象,只能使用局部返回
            block()//内联函数,可以使用return
        }
        runnable.run()
    }
  • crossinLine关键字就像一个契约,保证在内联函数的Lambda表达式中一定不会使用return关键字,这样就不会有矛盾了。