探究新语言,快速入门Kotlin语言

1. Kotlin语言简介

1.1 发展史

  • 2011年,JetBrains公布了Kotlin的第一个版本,并在2012年将其开源,但在早期,它并未受到太多关注
  • 2016年,Kotlin发布了1.0正式版,代表了Kotlin走向了成熟和稳重,JetBrains也在自己的旗舰IDE开发工具IntelliJ IDEA中加入对Kotlin的支持,Kotlin逐渐受到广泛关注
  • 2017年,Google在I/O大会上宣布Kotlin正式成为Android一级开发语言,Andriod Studio也加入了对Kotlin的支持,Kotlin开始大放异彩
  • 2019年,Google在I/O大会上宣布Kotlin正式成为Android的第一开发语言,虽然Java依然可以继续使用,但Google更推荐开发者使用Kotlin来编写安卓应用程序

1.2 Kotlin为什么是JetBrains开发的

1.2.1 Java语言的运行机制

Andriod操作系统明明是Google开发的,为什么JetBrains作为第三方公司,却能够设计出一门编程语言来开发Andriod应用程序呢,这就要探究Java语言的运行机制了。由于Java不是直接将代码编译成计算机能识别的二进制文件,而是需要Java虚拟机进行解释,所以严格的来说Java可以被称为是解释型语言

  • 编译:Java语言执行前需要编译成一种特殊的class文件
  • 解释:Java虚拟机(Andriod中叫ART)识别class文件并将其解释成计算机能识别的二进制数据后在执行
1.2.1 Kotlin的工作原理

了解了 Java语言的运行机制后不难发现,Java虚拟机并不直接和Java代码打交道,而是和编译好的class文件打交道。如果新开发了一种编程语言,自己制作了编译器,新语言的代码编译成同样规格的class文件,那么Java虚拟机同样也能将新语言的代码解释成计算机能识别的二进制文件后执行

1.3 Kotlin语言的优势

  • 语法更加简洁:使用Kotlin开发的代码量是Java的50%甚至更少
  • 语法更加高级:相比于Java比较老式的语法,Kotlin增加了很多现代高级语言的语法特性,使得开发效率大大提升
  • 语言安全性更高:几乎杜绝了空指针这个全球崩溃率最高的异常
  • 100%兼容Java:直接调用使用Java编写的代码,无缝使用Java第三方的开源库,Kotlin在加入诸多新特性时,还继承了Java的全部财富

2. 如何运行Kotlin代码

运行Kotlin代码有以下三种方式

3. 编程之本:变量和函数

3.1 变量

3.1.1 val和var

kotlin中只允许在变量前声明两个关键字 val 和 var

  • val(value的简写):可读,不可写
  • var(variable的简写):可读,可写
// val 可读不可写
fun main() {
    val a: Int = 10
    // a = a * 10  // 报错Val cannot be reassigned
    println("a = $a")
}
// var 可读可写
fun main() {
    var a: Int = 10
    a = a * 10 
    println("a = $a")  // a = 100
}

注意:val关键字有那么多束缚,为什么还有用这个关键字

其实Kotlin之所以这样设计,是为了解决Java中 final关键字没有被合理使用的问题。在 Java中,除非你主动在变量前声明了 final关键字,否则这个变量就是可变的。然而这并不是一件好事,当项目变得越来越复杂,参与开发的人越来越多时,你永远不知道一个可变的变量会在什么时候被谁给修改了,即使它原本不应该被修改,这就经常会导致出现一些很难排查的问题。因此,一个好的编程习惯是,除非一个变量明确允许被修改,否则都应该给它加上final关键字。但是,不是每个人都能养成这种良好的编程习惯。我相信至少有90%的 Java程序员没有主动在变量前加上 final关键字的意识,仅仅因为 Java对此是不强制的因此,Kotlin在设计的时候就采用了和Java完全不同的方式,提供了val和 var这两个关键字,必须由开发者主动声明该变量是可变的还是不可变的。那么我们应该什么时候使用val,什么时候使用var呢?这里我告诉你一个小诀窍,就是永远优先使用val来声明一个变量,而当val没有办法满足你的需求时再使用var这样设计出来的程序会更加健壮,也更加符合高质量的编码规范。

3.1.2 类型推导

将一个数值赋给变量后,kotlin能够根据数值的类型自动判断变量的类型而不用显式的指出。但当对一个变量延迟赋值时,Kotlin就无法自动进行类型推断。

fun main() {
    // 不用显式的给出a的类型 val a: Int = 10
    var a = 10 
}

3.2 函数

3.2.1 定义函数

定义一个函数最标准的方式见下,虽然Kotlin中还有许多其他修饰函数的关键字,但是只要掌握了下面的函数定义规则,你就已经能应对_80%以上的编程场景了,至于其他的关键字,我们会在后面慢慢学习。

package book.chapter2

//  关键字  函数名  (参数1: 类型  参数2: 类型): 返回值类型 {函数体  return 返回值} 
fun methodName(para1: Int, para2: Int): Int{
    return 0
}
  • 关键字:fun ( fnction的简写)是定义函数的关键字,无论你定义什么函数、都一定要使用 fun 来声明
  • 函数名:紧跟在 fun后面的是函数名,这个就没有什么要求了,你可以根据自己的喜好起任何名字,但是良好的编程习惯是函数名最好要有一定的意义,能表达这个函数的作用是什么
  • 入参:函数名后面紧限着一对括号里面可以吉明该函数接收什么参数(参数名: 参数类型, 参数名: 参数类型, …),如果你不想输入任何参数写一队空括号就行。
  • 返回值:参数括号后面的那部分是可选的,用于声明该函数会返回什么类型的数据,这部分可以直接不写。
  • 函数体:最后两个大括号之间的内容就是函数体了,我们可以在这里编写一个
3.2.2 使用代码补全

kotlin使用代码补全功能会自动导入使用的包,不使用补全功能需要收到导入

package book.chapter2

// 使用代码补全功能输入max 会自动进行导入,否则需要收到进行导入
import kotlin.math.max 

fun largerNumber(para1: Int, para2: Int): Int{
    return max(para1, para2)
}

3.2.3 语法糖–函数体只有一行代码

当函数体只有一行代码时 kotlin允许我们不使用{}的形式写函数体,而是使用**=**将唯一的一行代码写在函数的尾部。

package book.chapter2.kt02_function

import kotlin.math.max
fun main() {
    println(largerNumber1(1, 2))
}
// 函数体只有一行代码,用等号+函数体进行连接 可以不用中括号
// 使用等号连接时 返回值的类型会进行自动推导 不需要显示的指定返回值函数的类型
// fun largerNumber1(para1: Int, para2: Int): Int = max(para1, para2)
fun largerNumber1(para1: Int, para2: Int) = max(para1, para2)

4.程序的逻辑控制

程序的执行语句分为三种:顺序语句、条件语句和循环语句,顺序语句就是代码一行一行往下执行,但是这种方式在很多情况下不能满足我们的需求,所以引入了条件语句和循环语句。

4.1 if条件语句

kotlin中if语句有返回值,返回值为每一个条件中最后一行代码的返回值

package book.chapter2.kt03_logiccontrol

fun main() {
    println(largerNumber2(4, 5))
}
 version1  Java 中if的写法
//fun largerNumber2(para1: Int, para2: Int): Int {
//    var value = 0
//    if (para1 > para2) {
//        value = para1
//    } else {
//        value = para2
//    }
//    return value
//}

 version2  kotlin的if语句有返回值
//fun largerNumber2(para1: Int, para2: Int): Int{
//    return if (para1 > para2) {
//        para1
//    } else {
//        para2
//    }
//}

 version3  结合一行函数体的语法糖
//fun largerNumber2(para1: Int, para2: Int) = if (para1 > para2){
//    para1
//} else {
//    para2}

// version4  if 每个条件内的函数体也只有一行
fun largerNumber2(para1: Int, para2: Int) = if (para1 > para2) para1 else para2

4.2 when条件语句

kotlin中的when语句类型于Java的switch但远比switch语句强大的多。
when允许传入一个任意类型的参数,然后在when的条件体中定义一系列条件,格式为:
匹配值 -> {执行逻辑} 当执行逻辑只有一行时,可以不用中括号

package book.chapter2.kt03_logicControl

fun main() {
    println(getScore("Jack"))
    checkNumber(34.45)
    println(getScore2("Alex"))
}

 使用 if条件实现  精确匹配
//fun getScore(name: String) = if (name == "Jack") {
//        77
//    } else if (name == "Tom") {
//        88
//    } else if (name == "Jerry") {
//        99
//    } else {
//        0
//    }`

// 使用when实现  精确匹配
fun getScore(name: String) = when(name) {
    "Jack" -> 77
    "Tom" -> 88
    "Jerry" -> 99
    else -> 0
}

// 使用when实现类型匹配
// Number: kotlin内置的一个抽象类 Int、Double、Long、Float等与数字相关的都是他的子类
fun checkNumber(num: Number) = when(num) {
    is Int -> println("$num is Int")
    is Double -> println("$num is Double")
    else -> println("$num not support")
}

// when不带参数的用法  不常用但拓展性更强
fun getScore2(name: String) = when {
    // 只要名字是A开头就给100分
    name.startsWith("A") -> 100
    name == "Jack" -> 77
    name == "Tom" -> 88
    name == "Jerry" -> 99
    else -> 0
}

4.3 循环语句

包括while循环和fro循环,while循环和java或其他任何的主流编程用于一致,在此不做介绍,直接进入for循环的学习。Kotlin在for循环上进行了很大程度的修改。

  • for - i 循环:kotlin舍弃
  • for - each循环:Kotlin进行了大幅度的加强,变成了for - in 循环
  • 区间(range)概念:
// 表示区间[1, 10]
val range = 0..10

// 生成左闭右开区间 [0, 10)
val range = 0 until 10

// 生成任意步长区间 0,2,4,6,8
val range = 0 until 10 step 2

// 生成将序区间 [10, 0]
val range = 10 downTo 0
  • for in 循环
package book.chapter2.kt03_logicControl

fun main() {
    // 打印10,8,6,4,2,0
    for (i in 10 downTo 0 step 2) println(i)
}

5. 面向对象编程

kotlin也是面向对象的。面向对象的语言是可以创建类的,类就是对事物的一种封装,类中可以拥有自己的字段和函数,字段表示类所拥有的属性,函数表示类的行为,通过这种类的封装,就可以在适当的时候创建该类的对象,然后调用对象中的字段和函数来满足编程的实际需求。

5.1 类与对象

  • 构建一个kotlin类,并封装对应的字段和函数
package book.chapter2.kt05_classObject

fun main() {
    val p = Person()
    p.name = "Alex"
    p.age = 23
    p.eat()
}

class Person {
    var name = ""
    var age = 0
    
    // 不使用等号连接
    fun eat() {
        println("$name is eating and he is $age years old. ")
    }
}

5.2 继承与构造函数

定义Student类,包括字段 学号(sno)和年纪(grade),但学生属于Person类,也有姓名和年龄字段以及吃饭函数,因此不用再在Student类中再封装姓名和年龄字段以及吃饭函数,而是继承Person的字段和函数。

  • 创建Student 类 并封装字段
class Student {
    var sno = ""
    var grade = 0
}
  • 使Person可以被继承

和Java不同 kotlin中任何一个非抽象类默认都是不可以被继承的,相当于Java给类声明了final关键字,原因和val关键字差不多。类和变量一样,最好都是不可变的,而一个类如果可以被继承,它无法预知子类会如何实现,因此可能存在一些未知的风险。Effective java书中明确提到,如果一个类不是专门为继承而设计的,那么就应该主动加上final声明,禁止它可以被继承
强调非抽象类是因为抽象类的本身无法创造对象,一定要子类去继承它才能创建对象,因此抽象类必须可以被继承。而非抽象类默认无法被继承

// 非抽象类前加open 使其可以被继承
open class Person {
    ...
}
  • 让Student类 继承 Person类

在Java中使用extends关键字,在kotlin中使用**: 父类(),为什么一定要加一对括号**,括号的作用涉及主构造函数和次构造函数kotlin

class Student: Person() {
    var sno = ""
    var grade = 0
}
  • 主构造函数和次构造函数

任何一个面向对象的编程语言都有构造函数的概念,kotlin将构造函数分为了主构造函数和次构造函数

  • 主构造函数(常用):每个类都会有一个不带参数的主构造函数,当然也可以给他显示的指明参数。主构造函数的特点是没有函数体直接定义在类名的后面即可。
    • 传入主构造函数的参数,在实例化的过程中也必须进行传递。
    • 主构造函数中的参数是在实例化时进行传入,不需要在类中提前声明因此可以使用val
    • 主构造函数没有函数体,要想在主构造函数中编写代码逻辑通过init{}构造体实现
// 主构造函数中的参数,在实例化的过程中必须传递
fun main() {   
    val s1 = Student1("20221111", 3)    
    s.eat()  // 继承自Person
}

// 主构造函数中的参数,在实例化的过程中必须传递
class Student1(val sno: String, val grade: Int): Person(){
    // init{}构造体用于事项 主构造函数的逻辑
    init {
        println("my student number is $sno")
        println("my grade is $grade")
    }
}
  • 为什么继承父类时后面有括号

Java的一个继承特性规定,子类中的构造函数必须调用父类中的构造函数,这个规定kotlin也遵守。Student类的主构造函数也必须调用Person的主构造函数。如何调用?

  • 通过init{}构造体:是一种方法但不够好,因为大多数情况下我们是不需要编写init{}构造体的
  • 通过括号调用:不太好理解,但是简单
package book.chapter2.kt05_classObject


fun main() {
    val p1 = Person1("Alex", 22)
    val s2 = Student2("20223333", 3, "Alex", 33)
}

open class Person1(val name: String, val age: Int) {
}
// 在Student2的主构造函数中加入参数name和age时不能声明val 或 var
// 如果和Person1中的声明不一致,会产生冲突
 class Student2(val sno: String, val grade: Int, name: String, age: Int):
    Person1(name, age){
        init {
            println("my student number is $sno")
            println("my grade is $grade")
        }
}
  • 次构造函数

几乎用不到次构造函数,kotlin提供了一个给函数设定默认值的功能,基本上可以替代次构造函数的作用,会在后续章节进行介绍。

  • 一个类只能有一个主构造函数,但可以有多个次构造函数。次构造函数也可以用于实例化一个类,和主构造函数不同的是,次构造函数有函数体
  • kotlin规定,当一个类既有主构造函数,也有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)
package book.chapter2.kt05_classObject

fun main() {
    // 使用第二个次构造函数实例化对象
    val s31 = Student3()  // 输出:primary、second1、second2
    // 使用第一个次构造函数实例化对象
    val s32 = Student3("Alex", 32) // 输出:primary、second1
    // 使用主构造函数实例化对象
    val s33 = Student3("202233", 3, "Alex", 33)  // 输出:primary
}

open class Person2(val name: String, val age: Int) {
}

class Student3(val sno: String, val grade: Int, name: String, age: Int):
    Person1(name, age){
    init {
        println("i'm primary constructor!")
    }

    constructor(name: String, age: Int): this("", 0, name, age){
        println("i'm second1 constructor!")
    }

    constructor(): this("", 0){
        println("i'm second2 constructor!")
    }

}
  • 第一个次构造函数:
    • constructor(name: String, age: Int):通过constructor关键字创建次构造函数,需要传入name和age两个参数
    • this(“”, 0, name, age):通过this关键字调用了主构造函数并将sno和grade两个参数进行赋值
  • 第二个构造函数:间接调用了主构造函数因此也是合法的
  • 类中只有次构造函数,没有主构造函数
package book.chapter2.kt05_classObject

fun main() {
    // 使用第二个次构造函数实例化对象
    val s4 = Student4()  // 输出:primary、second1、second2
}

open class Person3(val name: String, val age: Int) {
}

class Student4: Person3 {
    constructor(): super("", 0){
        println("i'm second constructor!")
    }
}
  • Student4没有()主构造函数,但有次构造函数
  • Student4主构造函数,因此也不需要继承父类Person的主构造函数,Person也不加()
  • 由于没有主构造函数,次构造函数只能调用父类的构造函数 不用this而用super关键字

5.3 接口

接口是实现多态编程的重要组成部分,Java是单继承语言,任何一个类最多只能继承一个父类,但却可以轻松实现任意多个接口,kotlin也是如次。我们可以在接口中定义一系列的抽象行为,然后由具体的类去实现。

image.png

5.3.1 简单示例
  • Person类
package book.chapter2.kt06_interface

// 主构造函数传入name, age参数
open class Person(val name: String, val age: Int) {
    // 封装函数eat
    fun eat() {
        println("$name is eating and he is $age years old. ")
    }
}
  • Study接口
package book.chapter2.kt06_interface

interface Study {
    fun readBooks()
    fun doHomework()
}
  • Student类
package book.chapter2.kt06_interface

// Student 继承了Person()类 和 Study接口
class Student(val sno: String, val grade: Int, name: String, age: Int): Person(name, age), Study {
    // 调用接口的函数
    override fun readBooks() {
        println("$name is reading and it's sno is $sno.")
    }
    // 调用接口的函数
    override fun doHomework() {
        println("$name is doing homework and it is $grade grade now.")
    }
}
  • 继承:java的继承使用extend关键字,实现接口使用implemrnts,kotlin统一使用冒号进行分隔。
  • 括号:继承Person类时加括号必须继承其主构造函数 要加括号 实现Study接口不加括号 接口中没有任何构造函数
  • override:Study接口中定义了readBooks和doHomework两个函数,所以Student类必须实现这两个函数,使用override来重写父类或者接口中的函数
  • 1.简单示例.kt
package book.chapter2.kt06_interface

fun main() {
    val s1 = Student("20220929", 1, "Alex", 22)
    s1.eat()  // Alex is eating and he is 22 years old.
    doStudy(s1)  // Alex is reading and it's sno is 20220929.
                 // Alex is doing homework and it is 1 grade now.
}

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

5.3.2 对接口中的函数进行默认实现
  • 更改Study接口

如果一个接口中的函数有了函数体,那函数体中的内容就是他的**

package book.chapter2.kt06_interface

interface Study {
    fun readBooks()
    // 对doHomework函数进行默认实现
    fun doHomework() {
        println("do homework default implementation")
    }
}
  • 更改Student类Student类

去实现Study接口时,readBooks()函数会强制要求实现,而doHomework()函数可自由选择实现或不实现,如果不实现会自动使用默认的函数逻辑kotlin

package book.chapter2.kt06_interface

// Student 继承了Person()类 和 Study接口
class Student(val sno: String, val grade: Int, name: String, age: Int): Person(name, age), Study {
    // 调用接口的函数
    override fun readBooks() {
        println("$name is reading and it's sno is $sno.")
    }
   // 使用 doHomework() 函数的默认实现
}

5.3.3 函数的可见性修饰符

Java中的可见性修饰符包括public、private、protected和default(默认选项:什么都不写)。Kotlin也分四种public、private、protected和internal:

  • private:只对当前类内部可见
  • public:默认选项,对所有类都可见
  • protected:只对当前类和子类可见
  • internal:只对同一模块的类可见
    | 修饰符 | Java | Kotlin |
    | — | — | — |
    | public | 所有类可见 | 所有类可见(默认) |
    | private | 当前类可见 | 当前类可见 |
    | protected | 当前类、子类、同一包路径下的类可见 | 当前类、子类可见 |
    | default | 同一包路径下的类可见(默认) | 无 |
    | internal | 无 | 同一模块中的类可见 |

5.4 数据类与单例类

5.4.1 数据类

数据类用于将服务器或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持。在编写数据类时有以下常用方法需要编写:

  • equals():判断两个数据类是否相等
  • hashCode():equals():的配套方法,不写会导致HashMap、HashSet等hash相关的系统类无法正常工作
  • toString():用于更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址

使用Java编写每一个数据类是,都需要重写equals()、hashCode()、toString()等方法,而在Kotlin中使用一行简单代码即可实现

  • cellphone类:
package book.chapter2.kt07_dataAndObject

// 声明data关键字  表示该类为一个数据类
data class Cellphone(val price: Double, val brand: String)
  • 1.数据类型.kt

加上data关键字后,Kotlin会根据主构造函数中的参数帮你将equals()、hashCode()、toString()等固定且无实际逻辑意义的方法自动生成,从而大大的减少了开发的工作量。kotlin

package book.chapter2.kt07_dataAndObject

fun main() {
    val cellphone1 = Cellphone(1399.9, "华为")
    val cellphone2 = Cellphone(1399.9, "洛基亚")
    println(cellphone1)
    // 有data关键字: Cellphone(price=1399.9, brand=华为)
    // 去掉Cellphone 类去掉 data关键字打印的结果 book.chapter2.kt07_dataAndObject.Cellphone@6e8cf4c6
    println("price of cellphone1 equals cellphone2 is ${cellphone1.price == cellphone2.price}")
    println("brand of cellphone1 and cellphone2 are same is ${cellphone1.brand == cellphone2.brand}")
}

5.4.2 单例类

单例模式,最常用最基础的设计模式之一,它可以用于避免创造复杂的对象(如希望某个对象在全局最多只能拥有一个实例,这时就可以使用单例模式)。在Java中需要私有化构造函数并提供getInstance()这样的静态方法。而在Kotlin中只需将class关键字改为object关键字一个单例类就构造完成了。调用单例类中的函数使用Singleton.singletonTest(),这种写法虽然看上去像是静态方法的调用,但其实Kotlin在背后自动帮我们创建了一个Singleton类的实例,并保证全局只会出现一个Singleton实例。

6. lambda编程

许多现代的高级编程语言都支持Lambda编程,Java是从JDK1.8后加入Lambda编程的语法支持,而Kotlin从第一个版本开始就支持Lambda了,而且Kotlin的Lambda功能极其强大,可以认为Lambda才是Kotlin语言的灵魂所在。此处对Lambda的基础知识进行讲解,高阶函数、DSL等高级Lambda技巧在后续进行讲解。

6.1 集合的创建与遍历

  • Java中的集合

集合包含以下三种类型,在Java中List、Set、Map都是接口
List:主要实现类是ArrayList和LinkedList,作用类似Python列表
Set:主要实现类是HashSet,作用类似Python集合
Map:主要实现类是HashMap,作用类似Python字典

  • Kotlin中的集合

Java中集合初始化的方法是创建一个ArrayList的实例,然后再讲元素一个一个的添加。而Kotlin可以使用listOf()mutableListOf()函数分别实现不可变集合和可变集合的创建。
不可变集合:和val、类默认不可继承的设计初衷一致,可见
Kotlin在不可变性方面的控制极其严格

  • List:作用类似Python列表
package book.chapter2.kt06_lambda

fun main() {
    // 创建可变list
    val varList = mutableListOf("apple", "banana", "orange", "pear")
    // 创建不可变list    
    val valList = listOf("apple", "banana", "orange", "pear")
    
    varList.add("Grape")
    // valList.add("Grape") // 报错:Unresolved reference: add
    
    for (fruit in varList) {
        println(fruit)
    }
}
  • Set:作用类似Python集合
package book.chapter2.kt06_lambda

fun main() {
    // 创建可变set
    val varSet = mutableSetOf("apple", "apple", "orange", "pear")
    // 创建不可变set
    val valSet = setOf("apple", "apple", "orange", "pear")

    varSet.add("Grape")
//    valSet.add("Grape") // 报错:Unresolved reference: add

    for (fruit in varSet) {
        println(fruit)  //打印结果为apple、orange、pear、Grape 不会打印两次apple  set 自动去重
    }
}
  • Map:作用类似Python字典
  - "apple" to 1中to并不是关键字,而是一个infix函数,在后面章节进行学习
  - (fruit, number)fro in循环中使用括号一起声明两个变量对map的键和值进行接收
package book.chapter2.kt06_lambda

fun main() {
    // 创建不可变map
    val varMap = mutableMapOf("apple" to 1, "banana" to 2, "orange" to 3, "pear" to 4)
    // 创建不可变map
    val valMap = mapOf("apple" to 1, "banana" to 2, "orange" to 3, "pear" to 4)

    // 读map数据
    val mapNumber = varMap["apple"]
    println(mapNumber)

    // 往map写数据
    varMap["grape"] = 5
    // valMap["grape"] = 5 // 报错 Unresolved reference.

    // 遍历map
    for ((fruit, number) in varMap) {
        println("fruit is $fruit, number is $number")
    }
}

6.2 集合的函数式API

6.2.1 Lambda
  • Lambda概念

Lambda就是一小段可以作为参数传递的代码

  • 一小段:Kotlin没有严格的限制但Lambda的函数体不宜过长
  • 可以作为参数传递:相较于传递变量,将一小段代码作为参数传递功能上可以实现很多复杂需求
  • 语法

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

  • -> 符号:表示参数列表的结束和函数体的开始
  • 函数体:可以编写任意行代码(不建议编写的过长,一小段),最后一行代码会自动作为Lambda的返回值

6.2.2 常用函数式API

此处重点学习函数式API的语法结构,并介绍maxBY()、map()、filter()、any()和all()等API

  • API的语法结构:以实现选取名称长度最长的水果为例

maxBy() 函数的作用是接受一个Lambda作为参数,并且会在遍历集合时将每次

package book.chapter2.kt06_lambda

// TODO:API的语法结构:以实现选取名称长度最长的水果为例
fun main() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "Watermelon")
    val lambda = { fruit:String -> fruit.length }
    val maxLengthFruit = list.maxBy(lambda)
    println("The longest name fruit is $maxLengthFruit.")

    // 简化过程,以了解函数式API的语法结构

    // Step1: maxBy() 函数可以直接接受lambda表达式作为参数
    val maxLengthFruit1 = list.maxBy({ fruit:String -> fruit.length })

    // Step2: kotlin的类型推断机制
    val maxLengthFruit2 = list.maxBy({fruit -> fruit.length})

    // Step3: lambda特性
    // 当lambda表达式 作为函数的最后一个参数时可以写在括号外面
    val maxLengthFruit3 = list.maxBy() {fruit -> fruit.length}
    // 当lambda表达式的参数列表中只有一个参数时,不必声明参数而使用it代替
    val maxLengthFruit4 = list.maxBy() {it.length}

    // step4: 函数的特性 如果lambda表达式 是函数的唯一参数 括号可以不写
    val maxLengthFruit5 = list.maxBy {it.length}
}
  • map()函数

集合中最常用的一种函数式API,用于将集合中的每个元素都映射为一个另外的值,参数为映射的规则(可以为lambda表达式)。

package book.chapter2.kt06_lambda

// TODO:将所有的水果大写打印
fun main() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "Watermelon")
    val newList = list.map { it.uppercase() }

    for (fruit in newList) {
        println(fruit)
    }
}
  • filter()函数

用于过滤集合中的数据

package book.chapter2.kt06_lambda

// TODO:筛选名字长度大于5的水果进行打印
fun main() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "Watermelon")
    val newList = list.filter { it.length >= 6 }

    for (fruit in newList.map { it.uppercase() }) {
        println(fruit)
    }
}
  • any()函数和all()函数
package book.chapter2.kt06_lambda

// TODO:判断是否有任意水果名字长度大于6 和是否所有的水果名字长度都大于6
fun main() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "Watermelon")
    val anyResult = list.any {it.length >= 6}
    val allResult = list.all {it.length >= 6}

    println("any result is $anyResult, all result is $allResult")
}

6.3 Java函数式API的使用

在Kotlin中调用Java方法时也可以使用函数式API,但有限制。具体为,在Kotlin代码中调用了一个Java方法,并且该方法接受了一个Java单抽象方法接口的参数,就可以使用函数式API。Java单抽象方法接口指的是接口中只有一个待实现的方法,如果接口中有多个待实现的方法,则无法使用函数式API。

  • Java中的Runnable接口

Runnable接口是一个Java的单抽象方法接口,里面只有一个run()函数待实现

public interface Runnable {
    void run()
}
  • Thread类

对于任何一个Java方法,只要它接收Runnable参数,就可以使用函数式API,Java的线程类Thread就接收Runnable参数。创建了一个Runnable接口的匿名对象,并将它传给Thread类的构造方法,最后调用Thread类的start()方法执行这个线程。

package book.chapter2.kt06_lambda

fun main() {
    Thread(object : Runnable {
        override fun run() {
            println("Thread is running")
        }
    }).start()
}
  • 结合函数式API的结构进行简化
package book.chapter2.kt06_lambda

fun main() {
    Thread {
        println("Thread is running")
    }.start()
}
  • 因为Runnable接口下只有一个待实现方法,所有不用显示的重写run()方法override fun run(),直接在lambda表达式中给出返回值。
  • lambda作为Thread()括号中的最后一个元素和唯一一个元素时,可将lambda表达式移动到括号外面和去掉括号。

注意:为什么要学习Java函数式API的调用

Andriod SDK 是使用Java语言编写的,当我们在Kotlin中调用这些SDK接口时,就可能会用到这种Java函数式API的写法,如有一个button对象调用OnclickListenner接口的onClick()函数。

  • OnclickListenner接口java
// 满足函数式API接口的条件
public interface OnclickListenner {
    void onClick(View, v);
}
  • 对button对象注册按钮点击事件
button.OnclickListenner{    
}

Java函数式API的使用限定于从Kotlin中调用Java的单抽象方法,接口也必须是用Java定义的。因为Kotlin中有专门的高阶函数来实现更加强大的自定义函数式API功能。

7. 空指针检查

空指针异常(NullPointerException)是Android系统上崩溃率最高的异常类型,空指针是一种不受编程语言检查的运行时异常,只能由程序员通过逻辑判断来避免,即使是最出色的程序员,也不可能将所有潜在额空指针异常全部考虑到。

  • Java通过逻辑判断处理空指针

在Java中如果没有if 判断是否为空,当传入一个空类型时,如study == null,代码会报错空指针异常。稳妥的做法是在调用参数的方法之前进行判空处理,以保证不管传入的参数是什么代码始终都是安全的。java

public void doStudy(Study study) {
    // 调用参数的方法之前进行判空处理
    if (study != null) {
    	study.readBooks();
    	study.doHomework();        
    }
}
  • Kotlin的解决方案

利用编译时判空检查的机制几乎完全杜绝了空指针异常。虽然编译时判空检查的机制有时候会导致代码比较难写,但Kotlin提供了各种辅助工具,让我们能轻松的处理各种判空的情况。

  • Kotlin默认所有参数和变量都不能为空

以下代码在Kotlin中是绝对安全的,因为Kotlin默认所有参数和变量都不能为空,当给study对象赋空值时,会报错kotlin

package book.chapter2.kt06_lambda

fun main() {
    // 报错 Null can not be a value of a non-null type Study
    doStudy(null)
}

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

7.1 可空类型系统

在特定的业务逻辑中,我们需要某个参数或某个变量可以为空,就使用Kotlin另一套可为空的类型系统。但是,使用可为空的类型系统时,我们需要在编译时期就把所有潜在为空的指针都处理掉,否则代码将无法编译通过。

语法: 对可能为空的变量或参数后加?,如Study?

package book.chapter2.kt06_lambda

fun main() {    
    doStudy(null)
}

// 参数或变量后面加? 表示Study可以取空值
fun doStudy(study: Study?) {
    // Study为空时 调用study对象的方法会报错,因此还是需要逻辑判断
    if (study != null) {
        study.doHomework()
        study.readBooks()
    }
}

7.2 判空辅助工具

Kotlin提供了各种操作符,用于对空类型进行处理,而不用使用if 进行逻辑判断,以提高代码的编写效率。

  • ?. :当对象不为空时正常调用对应的方法,当对象为空时什么也不做
package book.chapter2.kt06_lambda

fun main() {
    doStudy(null)
}

// 参数或变量后面加? 表示Study可以取空值
fun doStudy(study: Study?) {
    // 使用?. 操作符 代理if逻辑判断
        study?.doHomework()
        study?.readBooks()
}
  • ?: :左右两边都为表达式,如果左边表达式结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。
// a不为空就返回左边,否则返回右边
val c = a ?: b

编写一个计算文本长度的函数

package book.chapter2.kt06_lambda

fun main() {
    println(getTxtLength("看啥啥"))
    println(getTxtLength(null)) 
}

fun getTxtLength(text: String?) = text?.length ?: 0
  • getTxtLength(text: String?):表示允许text参数为空
  • text?.length:text不为空正常正常执行length,text为空就什么都不做
  • text?.length ?: 0:text?.length不为空返回text?.length,text?.length不为空返回0
  • !! :非空断言工具

Kotlin的空指针检测机制并不是那么智能,在编译getUpperCase()函数时,Kotlin并不知道主函数执行前对content对象是否为空进行了判断,当content对象非空时才调用getUpperCase()函数。因此对content对象使用uppercase()函数会报错。解决方法有两种:

  • content?.uppercase():使用?.操作符,进行处理
  • content!!.uppercase():使用!!操作符,断言content对象不可能为空。虽然这样编写代码代码确实可以通过编译,但其实告诉了Kotlin我非常确定这个对象不为空,不用你来帮我做空指针检测,如果出现问题,你可以直接给我抛出空指针异常,后果由我自己承担。
package book.chapter2.kt06_lambda

var content: String? = "hello"

fun main() {
    if (content != null) {
        getUpperCase()
    }
}

fun getUpperCase() {
//    // content.uppercase()报错 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
//    val upperCase = content.uppercase()
    val upperCase = content?.uppercase()
    val upperCase1 = content!!.uppercase()
    
    println(upperCase)
}
  • let():let是一个特殊的辅助工具,它既不是操作符,也不是关键字而是一个函数,同时还提供了函数式API的编程接口。

let()函数将原调用对象作为参数传递到lambda表达式中,obj对象调用了let()方法后,Lambda表达式中的代码会立即执行,并将obj对象传入作为参数。kotlin

obj.let {obj
    // 编写具体的业务逻辑
}

使用?.的方式存在问题

fun doStudy(study: Study?) {
    // 使用?. 操作符 虽然正常编译通过但是比较啰嗦
        study?.doHomework()
        study?.readBooks()
}

// 上面代码等价于 原本使用一次if条件判断就能完成的操作,进行了两次if条件判断
fun doStudy(study: Study?) {
    if (study != null) {
        study.doHomework()
    }
    if (study != null) {
        study.readBooks()
    }        
}

使用let()加?.进一步优化doStudy函数kotlin

package book.chapter2.kt06_lambda

var study: Study? = null

fun doStudy() {
    if (study != null) {
        // 报错:Smart cast to 'Study' is impossible, because 'study' is a mutable property that could have been changed by this time
        study.readBooks()
        study.doHomework()
    }
}

注意:let()函数还可以处理全局变量的判空问题

  • if 判断不支持

之所以出现报错是因为全局变量的值随时都有可能被其它线程所修改,即使做了判空处理,任然无法保证if语句中的study变量没有空指针风险。

package book.chapter2.kt06_lambda

var study: Study? = null

fun doStudy() {
    if (study != null) {
        // 报错:Smart cast to 'Study' is impossible, because 'study' is a mutable property that could have been changed by this time
        study.readBooks()
        study.doHomework()
    }
}
  • let()函数支持
var study: Study? = null
  
fun doStudy() {
    study?.let {
        it.readBooks()
        it.doHomework()
    }
}

8. Kotlin中的小魔术

8.1 字符串内嵌表达式

Kotlin中允许在字符串嵌入像${}这种语法的表达式,同时当表达式中只有一个变量时还可以省略{}

"hello, ${obj.name}. nice to meet you!"

8.2 函数的参数默认值

8.3.1 默认参数的概念

Kotlin还可以在定义参数时给参数设置默认值,作用和Python的默认参数差不多

package book.chapter2.kt07_magic

fun main() {
    // 三种调用方式
    // 使用默认参数
    printparams()
    // 按顺序传入两个参数
    printparams(60, "goodman")
    // 按关键字进行传参
    printparams(str = "hello", num = 43)
    
}

fun printparams(num: Int = 100, str: String = "Alex") {
    println("num is $num, str is $str")
}

8.3.2 默认参数的作用

默认参数的加入,很大程度的替代了次构造函数的作用,可以回想前面的次构造函数,同样是实现输入不同个数的参数对函数进行调用,使用默认参数的方法要简单很多。

  • 使用次构造函数实现kotlin
package book.chapter2.kt05_classObject

fun main() {
    // 使用第二个次构造函数实例化对象
    val s31 = Student3()  // 输出:primary、second1、second2
    // 使用第一个次构造函数实例化对象
    val s32 = Student3("Alex", 32) // 输出:primary、second1
    // 使用主构造函数实例化对象
    val s33 = Student3("202233", 3, "Alex", 33)  // 输出:primary
}

open class Person2(val name: String, val age: Int) {
}

class Student3(val sno: String, val grade: Int, name: String, age: Int):
    Person1(name, age){
    init {
        println("i'm primary constructor!")
    }

    constructor(name: String, age: Int): this("", 0, name, age){
        println("i'm second1 constructor!")
    }

    constructor(): this("", 0){
        println("i'm second2 constructor!")
    }

}
  • 使用默认参数实现
class Student3(val sno: String = “”, val grade = 0: Int = 0, name: String = “”, age: Int = 0):
Person1(name, age){
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值