2.6.Lambda编程
      Java在1.8后加入Lambda编程语法支持,Kotlin一开始就支持,我们将介绍高阶函数、DSL等高级Lambda技巧。
2.6.1.集合的创建与遍历
      传统意义上的集合有List(ArrayList、LinkedList)、Set(HashSet)和Map键值对(HashMap)结构。需求是创建包含许多水果名称的集合,Kotlin中创建一个ArrayList实例,将水果一个个添加进去,如下所示:

fun main(){
    val list = ArrayList<String>()
    list.add("Apple")
    list.add("Banana")
    list.add("Orange")
    list.add("Pear")
    for (fruit in list){
        println(fruit)
    }
}

      这种初始化集合方式比较繁琐,Kotlin提供内置的listOf函数来初始化集合。

 val list2 = listOf("Apple", "Banana", "Orange", "Pear")

      但这种方式创建的是不可变的集合,只能查,无法增删改。Kotlin在不可变性方面控制极其严格,可变集合mutableListOf函数解决了这一问题。

 val list3 = mutableListOf("Apple", "Banana", "Orange", "Pear")
 list3.add("WaterMelon")

      Set用法与List几乎一样,只是将创建集合的方法换成了setOf()和mutableSetOf()函数。Set底层基于hash映射机制存放数据,无法保证有序。
      Map集合是一种键值对形式的数据结构,传统的Map是创建Map实例,然后将键值对添加进Map中。如下所示:

    val map = HashMap<String, Int>()
    map.put("Apple", 1)
    map.put("Banana", 2)
    map.put("Orange", 3)
    map.put("Pear", 4)
    map.put("Grape", 5)

      但其实Kotlin不太建议用这种方式,而是推出了一种类似数组下标的语法结构。

    val map = HashMap<String, Int>()
    map["Apple"] = 1
    map["Banana"] = 2
    map["Orange"] = 3
    val num = map["Apple"]//读取一条数据

      当然这依然不是最简单的,Kotlin提供了mapOf()和mutablemapOf来简化Map用法。这里不是使用了to进行关联,而是使用了infix函数。

    val map = mapOf("Apple" to 1, "Banana" to 2, "Pear" to 3)
    for ((fruit, number) in map) {
        println("fruit is " + fruit + ",number is " + number)
    }

2.6.2.集合的函数式API
       需求如下:找到水果集合中单词最长的那个水果?代码如下:

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "WaterMelon", "Grape")
    var maxLengthFruit = ""
    for (fruit in list) {
        if (fruit.length > maxLengthFruit.length) {
            maxLengthFruit = fruit
        }
    }
    println("max length fruit is " + maxLengthFruit)
}

       其实使用集合的函数式API会使得这个功能更容易。

val maxLengthFruit = list.maxBy { it.length }  

       下面解释这玩意,Lambda是一小段可以作为参数传递的代码。正常情况,参数传递的是变量,现在确实一小段代码。其语法结构如下:

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

      因此刚才的问题按照语法结构进行套用:

val lambda = { fruit: String -> fruit.length }
val maxLengthFruit = list.maxBy (lambda )

      首先开始简化,不需要专门定义lambda变量,直接传入。

val maxLengthFruit = list.maxBy ({ fruit: String -> fruit.length } )

      其次Kotlin规定,最后一个参数可移到函数括号的外面,如果Lambda参数是函数唯一参数的话,可以将括号省略。

val maxLengthFruit = list.maxBy { fruit: String -> fruit.length }

      根据类型推导机制,参数列表不必声明参数类型,因为可以推导。

val maxLengthFruit = list.maxBy { fruit -> fruit.length }

      最后,当参数列表只有一个参数时,不必声明参数名,而是使用it关键字替代。因此变成了

val maxLengthFruit = list.maxBy { it.length }  

      现在函数式API已经讲得差不多了,我们练习一下,将所有水果名变为大写。Map函数能够将每个元素映射为另外一个值,映射规则在Lambda中指定。

val newList = list.map { it.toUpperCase() }

       在此基础上我们只保留5个字母以内的水果,可以使用filter函数来实现。先filter再map效率会高一些。

val newList = list.filter { it.length <= 5 }.map { it.toUpperCase() }

       最后我们再介绍较为常用的函数式API:any和all函数。Any判断集合中是否至少存在一个元素满足此条件,all函数用于判断集合中是否所有元素都满足此条件。

    val list = listOf("Apple", "Banana", "Orange", "Pear", "WaterMelon", "Grape")
    val anyResult = list.any { it.length <= 5 }
    val allResult = list.all { it.length <= 5 }
    println("anyResult " + anyResult + ",all Result " + allResult)

       输出结果如下:

anyResult true,all Result false

2.6.3.Java函数式API的调用
       Kotlin中调用Java方法也可以使用函数式API,但有限制。具体来讲,当Kotlin调用一个Java方法,该方法接受一个Java单抽象方法接口参数(只有一个,多个无法使用),才能使用函数式API。Java原生API的Runnable接口中有一个待实现的run方法符合要求,Runnable接口定义如下:

public interface Runnable{
    void run();
}

       创建子线程的代码接受了Runnable参数,其Java如下所示:

       new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("Thread is ruuning");
           }
       }).start();

        Kotlin代码舍弃了new关键字,创建匿名内部类实例不能使用new了,改用object关键字:

    Thread(object :Runnable{
        override fun run() {
            println("Thread is running")
        }
    }).start()

        简化如下,首先不用显式重写run方法,因为Kotlin知道Runnable后面的Lambda表达式是要在run方法中实现内容。

   Thread(Runnable {
        println("Thread is running")
   }).start()

        另外,如果一个Java方法的参数列表不存在一个以上Java单抽象方法接口参数,我们可以对接口名进行省略。

  Thread({
        println("Thread is running")
  }).start()

        最后,若此Lambda表达式是最后一个参数,可将表达式移到括号外面,因为方法只有唯一一个参数,括号也可以省略,最后的代码如下所示。

   Thread({
        println("Thread is running")
   }).start()

       这种其实很常见,Android中有一个常用的点击事件接口onClickListener,其定义如下。

public interface OnClickListener{
    void onClick(View v)
}

       此接口是单抽象方法接口,那么按钮button实例在点击事件注册时用Java可以这么写。

button.setOnClickListener(new View.OnClickListener(){
   @Override
   public void onClick(View v){
   }
});

       用Kotlin实现同样的功能可以使用函数式API写法对代码进行简化。代码精简很多。

button.setOnClickListener{}

       需要提醒的是,Java函数式API的使用仅限于Kotlin调用Java方法,且单抽象方法接口必须由Java定义。因为Kotlin中有专门的高阶函数来实现更加强大的自动定义函数式API功能。
2.7.空指针检查
       Android崩溃最高的异常类型可能是空指针异常,其不受程序员。来看一段2.5.3小节所述的Java代码。

public void doStudy(Study study) {
    study.readBooks();
    study.doHomeWork();
}

       很容易出现问题,假设往doStudy传的如果是null参数,则会发生空指针异常,更稳妥的做法是进行一个判空处理。如下所示:

public void doStudy(Study study) {
    if(study!=null){
       study.readBooks();
       study.doHomeWork();
   }
}

2.7.1.可用类型系统
       Kotlin将空指针异常检查提前到编译时期而非运行时期,因此几乎杜绝了空指针异常。因此这段代码没问题:

fun doStudy(study: Study) {
    study.readBooks()
    study.doHomeWork()
}

      假设业务逻辑确实是要求某个参数或者变量为空怎么办?Kotlin提供了一套可为空的类型系统,只不过在使用可为空的系统时,我们需要在编译时期将所有潜在空指针异常处理掉,才能编译通过。空的类型系统是在类名前面加一个问号。eg:Int表示不为空的整型,Int?表示可为空的整型。加上问号后我们发现依然提示错误,这是因为下面的两个方法可能会造成空指针异常,因此需要判空。最后代码如下所示:

fun doStudy(study: Study?) {
    if (study != null){
        study.readBooks()
        study.doHomeWork()
    }
}

       If判断语句会比较啰嗦,而且处理不了全局变量的判空问题,Kotlin提供一些列辅助工具进行更为简单的判空处理。
2.7.2.判空辅助工具
      首先学习?.操作符,其是当对象不为空时正常调用的方法,当对象为空时正常调用的方法。可将上面代码简化如下:

fun doStudy(study: Study?) {
    study?.readBooks()
    study?.doHomeWork()
}

      其次学习?:操作符,其是左右两边都接受一个表达式,如果左边表达式的结果不为空就返回左边表达式的值,否则返回右边表达式的值。举例来讲:

 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
}

         借助操作符可以使它更简单。

//调用length字段需使用?.操作符,进而判断是不是为零。
fun getTextLength1(text: String?) = text?.length ?: 0

      Kotlin空指针检测机制并非总那么只能,可以使用!!.加在对象后面使得强行通过编译,但不建议。
      最后介绍一个辅助工具let函数。其提供了函数式API的编程接口,将原始调用对象作为参数传递到Lambda表达式中,示例代码如下:

 obj.let{obj2 -> //具体逻辑}

       obj对象的let函数会立即执行Lambda表达式的代码,并且obj对象本身会作为参数传入,为了防止重名,我们将参数改为obj2,但实际是同一对象。刚才在doStudy函数中我们得到了这样的代码:

fun doStudy(study: Study?) {
    study?.readBooks()
    study?.doHomeWork()
}

      但表达式过于啰嗦,需要进行两次if判空判断。考虑使用?.和let来进行优化。优化代码如下所示。

fun doStudy(study: Study?) {
    //?.表示对象不为空时调用let函数
    //let函数会将study本身作为参数传递至Lambda表达式中,此时对象不为空,可以放心调用。
    study?.let { stu ->
        stu.readBooks()
        stu.doHomeWork()
    }
}

      Lambda表达式的参数列表只有一个参数时,可以不用声明参数名,可以直接使用it关键字代替。最终的代码为:

fun doStudy(study: Study?) {
    study?.let { 
        it.readBooks()
        it.doHomeWork()
    }
}

      需要说明的是,let是可以处理全局变量的判空问题的,而if无法做到这一点。譬如下面的代码:

//这样参数作为全局变量传入时会报错,因为其随时会被其他线程修改,即使做了判空处理,也无济于事,let可以解决。
var study: Study? = null

fun doStudy1(){
    if (study != null){
        study.readBooks()
        study.doHomeWork()
    }
}

2.8.Kotlin中的小魔术
2.8.1.字符串内嵌表达式

       Kotlin不像Java那样傻傻的拼接字符串了,使用了字符串内嵌表达式${}的语法规则:

    val str = "hello,${obj.name}.nice to meet u"
当仅有一个变量时,甚至可以将两边大括号缩写。以下代码示例最后两句是完全等价的:
    val brand = "Samsung"
    val price = 1299.99
    println("Cellphone(brand=" + brand + ",price=" + price + ")")
    println("Cellphone(brand=$brand,price=$price)")

       当仅有一个变量时,甚至可以将两边大括号缩写。以下代码示例最后两句是完全等价的:

    val brand = "Samsung"
    val price = 1299.99
    println("Cellphone(brand=" + brand + ",price=" + price + ")")
    println("Cellphone(brand=$brand,price=$price)")

2.8.2.函数的参数默认值
       Kotlin的参数默认值功能能够在很大程度上替代次构造函数的作用,定义函数时给任意参数设定一个默认值,那么调用此函数时不会要求调用方强制传值,可以使用默认值。

fun main() {
    printParms(123)
    printParms1(str = "world")
}
//情况1:第二个参数设定了一个默认值,调用时可以不用给第二个参数传值。
fun printParms(num: Int, str: String = "hello") {
    println("num is $num,str is $str")
}
//情况2:第一个参数设定了默认值,第二个参数没有,Kotlin此时使用键值对进行传值较为方便
fun printParms1(num: Int = 100, str: String) {
    println("num is $num,str is $str")
}

      此方式能够有效替代次构造函数,次构造函数时使用更少参数实例化Student类。下面的代码无参构造函数调用带两个参数的次构造参数;后者会调用4个参数的主构造函数。

class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {
    constructor(name: String, age: Int) : this("", 0, name, age) {}
    constructor() : this("", 0) {}
}

      其实在Kotlin完全没有必要,我们完全可以通过一个主构造函数设定默认值后使用键值对赋值的方式进行实现,代码如下。

class Student1(val sno: String="", val grade: Int=0, name: String="", age: Int=0) : Person(name, age) {
}