Kotlin学习 1 -- 快速入门Kotlin

本篇文章主要介绍以下几个知识点:

SUMMER DAY (图片来源于网络)

1. Kotlin 语言简介

编程语言大致可分为两类:编译型语言和解释型语言。
编译型:编译器将编写的源代码一次性地编译成计算机可识别的二进制文件,然后计算机直接执行,如C、C++。
解释型:程序运行时,解释器会一行行读取编写的源代码,然后实时地将这些源代码解释成计算机可识别的二进制数据后再执行,如Python、JavaScript(解释型语言效率会差些)。

Java先编译再运行,但Java代码编译之后生成的是class文件,只有Java虚拟机才能识别,Java虚拟机将编译后的class文件解释成二进制数据后再执行,因而Java属于解释型语言。

Kotlin也是通过编译器编译成class文件,从而Java虚拟机可以识别。

2. 变量和函数

2.1 变量

Kotlin 定义一个变量,只允许在变量前声明两种关键字:val 和 var。

val (value的简写)声明不可变的变量,对应java中的final变量。

var (variable的简写)声明可变的变量,对应java中的非final变量。

使用技巧:编程时优先使用val来声明变量,当val无法满足需求时再使用var。

2.2 函数

fun (function的简写) 是定义函数的关键字

如定义个 返回两个数中较大的数 的函数如下:

fun main() {
    val a = 10
    val b = 20
    val value = largerNum(a, b)
    print("large number is $value")
}

fun largerNum(num1: Int, num2: Int): Int {
    return max(num1, num2)
}

3. 程序的逻辑控制

程序的执行语句主要分3中:顺序、条件和循环语句。

kotlin中的条件语句主要用 if 和 when 语句,循环语句主要用 while 和 for 循环。

3.1 if 条件语句

Kotlin 中的if语句和java中if语句没啥区别,以上述函数为例修改如下:

fun largerNum(num1: Int, num2: Int): Int {
    var value = 0
    if (num1 > num2) {
        value = num1
    } else {
        value = num2
    }
    return value
}

不过Kotlin中if语句可以有返回值,返回值是if语句每一个条件中最后一行代码的返回值。上述函数可以简化如下:

fun largerNum(num1: Int, num2: Int): Int {
   val value = if (num1 > num2) {
       num1
   } else {
       num2
   }
   return value
}

将if语句直接返回,继续简化:

fun largerNum(num1: Int, num2: Int): Int {
    return if (num1 > num2) {
        num1
    } else {
        num2
    }
}

当一个函数只有一行代码时,可以省略函数体部分,直接将这一行代码使用等号串连在函数定义的尾部。上述函数和一行代码的作用是相同的,从而可以进一步精简:

fun largerNum(num1: Int, num2: Int): Int = if (num1 > num2) {
    num1
} else {
    num2
}

当然也可以直接压缩成一行代码:

fun largerNum(num1: Int, num2: Int): Int = if (num1 > num2) num1 else num2

3.2 when 条件语句

Kotlin中的when语句有点类似于java中的switch语句,但强大得多。

用if语句实现个 输入学生名字返回该学生的分数 的函数如下:

fun getScore(name: String) = if (name == "Wonderful") {
    100
} else if (name == "Tome") {
    86
} else if (name == "Jack") {
    60
} else {
    0
}

when 语句允许传入一个任意类型的参数,然后可以在when的结构体中定义一系列的条件,格式是:

匹配值 -> { 执行逻辑 }

当执行逻辑只有一行代码时, { } 可以省略。

用when语句实现上述方法如下:

fun getScore(name: String) = when (name) {
    "Wonderful" -> 100
    "Tome" -> 86
    "Jack" -> 60
    else -> 0
}

在某些场景,比如 所有名字以Won开头的学生分数都是100分,则上述函数可以用不带参数的when语句实现:

fun getScore(name: String) = when {
    name.startsWith("Won") -> 100
    name == "Tome" -> 86
    name == "Jack" -> 60
    else -> 0
}

注:when语句不带参数的用法不太常用

除此之外,when 语句还可以进行类型匹配,如:

fun checkNumber(num: Number) {
   when (num) {
       is Int -> print("整数") // is 关键字相当于 Java 中的 instanceof 关键字
       is Double -> print("Double")
       else -> print("number not support")
   }
}

3.3 循环语句

Kotlin 中的 while 循环语句和在 Java 中的使用没有区别,而 for 循环在 Kotlin 中做了很大幅度的修改。

Java 中常用的 for-i 循环在 Kotlin 中被舍弃了,Java 中的 for-each 循环在 Kotlin 中变成了 for-in 循环。

Kotlin 用 .. 创建闭区间,用 until 关键字创建左闭右开的区间,如:

val range = 0..10 // 数学中的[0, 10]
val range = 0 until 10 // 数学中的[0, 10)

Kotlin 中 for 循环用法如下:

fun main() {
    // 遍历[0, 10]中的每一个元素
    for (i in 0..10){
        println(i)
    }
    // 遍历[0, 10)的时候,每次循环会在区间范围内递增2,相当于 for-i 中的 i = i + 2 效果
    // step 关键字可以跳过其中一些元素
    for (i in 0 until 10 step 2){
        println(i)
    }
    // 降序遍历[0, 10]中的每一个元素
    // downTo 关键字用来创建降序的空间
    for (i in 10 downTo 1){
        println(i)
    }
}

4. 面向对象编程

不同于面向过程的语言(如C语言),面向对象的语言是可以创建类的。类是对事物的一种封装,而面向对象编程最基本的思想就是通过这种类的封装,在适当的时候创建该类的对象,然后调用对象中的字段和函数来满足实际编程的需求。

建立在基本思想之上,面向对象编程还有其他特性如继承、多态等。

4.1 类与对象

在Kotlin中,用 class 关键字来声明一个类,比如创建一个 Person 类如下:

// 定义一个Person类,包含name和age字段,一个eat() 函数
class Person {
    var name = ""
    var age = 0
    
    fun eat(){
        println("$name is eating. He is $age years old")
    }
}

定义好类后,类的实例化方式和 Java 是基本类似的,但不需要new关键字,只需val p = Person(),如下:

fun main() {
    val p = Person()
    p.name = "Wonderful"
    p.age = 18
    p.eat()
}

4.2 继承与构造函数

现创建一个Student类如下:

class Student {
    var sno = ""  // 学号
    var grade = 0 // 年级
}

如果要让 Student 类继承 Person 类,需要做以下两件事:

  • 使 Person类可以被继承(注:Kotlin 中任何一个非抽象类默认是不可被继承的),在 Person 类前面加上关键字 open 就可以了:
open class Person {
    var name = ""
    var age = 0

    fun eat(){
        println("$name is eating. He is $age years old")
    }
}
  • 让 Student 类继承 Person 类,Kotlin 中统一用冒号 : 继承类或实现接口,如下:
class Student : Person() {
    var sno = ""  // 学号
    var grade = 0 // 年级
}

上面继承代码中 Person 类的后面要加一对(),表示 Student 类的主构造函数在初始化时会调用 Person 类的无参构造函数,即使在无参情况下也不能取消括号。


Kotlin 的构造函数有两种:主构造函数次构造函数

主构造函数没有函数体,每个类默认会有一个不带参数的主构造函数,也可以在类名后面直接定义来显式指明参数,如:

class Student(val sno: String, val grade: Int) : Person() { }

这样,实例化 Student 类时需要传入构造函数中的参数:

val student = Student("no123", 6) // 学号 no123,年级 6

如果想在实例化类时在主构造函数中实现一些逻辑,则可以将逻辑写在 Kotlin 提供的 init结构体中:

class Student(val sno: String, val grade: Int) : Person() {
    init {
        // 实例化时打印学号和年级
        println("sno is $sno")
        println("grade is $grade")
    }
}

子类的主构造函数调用父类中的哪个构造函数,在继承时通过括号来指定。

如果把 Person 类的姓名和年龄放到主构造函数中,如下:

open class Person(val name: String, val age: Int) {
    fun eat() {
        println("$name is eating. He is $age years old")
    }
}

此时,Person 类已经没有无参构造函数了,Student 类要继承 Person 类也要在主构造函数中加上姓名和年龄这两个参数,如下:

// 这边增加的 name 和 age 字段不能声明成 val,因为在主构造函数中声明 val 或 var 的参数会将自动
// 成为该类的字段,这会导致和父类中同名的字段冲突
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {
    init {
        println("sno is $sno")
        println("grade is $grade")
    }
}

任何一个类只能有一个主构造函数,但可以有多个次构造函数。次构造函数也可用于实例化一个类,它是有函数体的。

Kotlin 规定,当一个类既有主构造函数也有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。

次构造函数是通过 constructor 关键字来定义的,如定义 Student 类的次构造函数如下:

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

此时就可以有3种方式来实例化 Student 类:

val student1 = Student()
val student2 = Student("Wonderful", 18)
val student3 = Student("no123", 6, "Wonderful", 18)

还有种特殊情况,类中只有次构造函数,没有主构造函数(当一个类没有显式定义主构造函数且定义了次构造函数时,它就是没有主构造函数的),此时继承类时就不需要再加上括号了,如下:

class SpecialStudent : Person {
    constructor(name: String, age: Int) : super(name, age) { }
}

4.3 接口

接口是用于实现多态编程的重要组成部分,Kotlin 和 Java 一样也是一个类只能继承一个父类,却可以实现多个接口。

定义个 Study 接口,接口中的函数不要求有函数体,如下:

interface Study {
    fun readBooks()
    fun doHomework()
}

在 Kotlin 中,统一用冒号,中间用逗号分隔,来继承类或实现接口,如在 Student 类中实现 Study 接口:

class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age), Study {
    init {
        println("sno is $sno")
        println("grade is $grade")
    }

    override fun readBooks() {
        println("$name is reading")
    }

    override fun doHomework() {
        println("$name is doing homework")
    }

    constructor(name: String, age: Int) : this("", 0, name, age){ }

    constructor() : this("", 0){ }
}

在mian() 中调用这两个接口函数如下:

fun main() {
    val student = Student("no123", 6, "Wonderful", 18)
    doStudy(student)
}

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

上面由于 Student 类实现了 Study 接口,从而可以把 Student 类的实例传递给 doStudy 函数,这种面向接口编程也可以称为多态。

Kotlin 还允许对接口中定义的函数进行默认实现,如:

// 当一个类实现 Sduty 接口时,只会强制要求实现 readBooks() 函数,
// 而 doHomework() 函数可以自由选择是否实现
interface Study {
    fun readBooks()
    fun doHomework() {
        println("do homework default implementation")
    }
}

Kotlin 和 Java 中函数的可见性修饰符比较如下:


Java 和 Kotlin 函数可见性修饰符对照表

4.4 数据类与单例类

在 Java 中数据类通常需要重写 equals()hashCode()toString() 几个方法,如用 Java 构建一个手机数据类如下:

public class CellPhone {
    String brand; // 品牌
    double price; // 价格

    public CellPhone(String brand, double price) {
        this.brand = brand;
        this.price = price;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CellPhone cellPhone = (CellPhone) o;
        return Double.compare(cellPhone.price, price) == 0 &&
                brand.equals(cellPhone.brand);
    }

    @Override
    public int hashCode() {
        return brand.hashCode() + (int) price;
    }

    @Override
    public String toString() {
        return "CellPhone{" +
                "brand='" + brand + '\'' +
                ", price=" + price +
                '}';
    }
}

如果用 Kotlin 只需在数据类前面声明关键字 data 就可以了,如下:

data class CellPhone(val brand: String, val price: Double)

在 Java 中常见的单例模式写法如下:

public class Singleton {
    private static Singleton instance;

    private Singleton() { }

    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

如果用 Kotlin 只需把 class 关键字改成 object 就可以了,如下:

object Singleton { }

5. Lambda 编程

5.1 集合的创建与遍历

一般的集合主要就是 List 、Set 和 Map,List 的主要实现类是 ArrayList 和 LinkedList,Set 的主要实现类是 HashSet,Map 的主要实现类是 HashMap。

创建一个包含许多水果名称的集合,传统的写法如下:

val list = ArrayList<String>()
list.add("apple")
list.add("orange")
list.add("pear")

上面这种方式比较繁琐,Kotlin 专门提供了一个内置的 listOf() 函数来简化初始化集合的写法,如下:

val list = listOf("apple", "orange", "pear")

不过 listOf() 函数创建的是一个不可变集合,创建可变集合用 mutableListOf() 函数。

Set 集合的用法也差不多,将创建集合的方式变成 setOf()mutableSetOf() 函数而已。

注:和 List 集合不同的是,Set 集合底层使用 hash 映射机制来存放数据,因而集合中的元素无法保证有序。

用 Map 集合创建一个包含许多水果名称和对应编号的集合,传统的写法如下:

val map = HashMap<String, Int>()
map.put("apple", 1)
map.put("orange", 2)
map.put("pear", 3)

但在 Kotlin 中不建议用 put()get() 方法来对 Map 进行数据操作,而推荐使用一种类似于数组下标的语法结构,如添加 map["apple] = 1,读取 val number = map["apple"],因此上面代码可改为:

val map = HashMap<String, Int>()
map["apple"] = 1
map["orange"] = 2
map["pear"] = 3

或者使用 mapOf()mutableMapOf() 来简化:

val map = mapOf("apple" to 1, "orange" to 2, "pear" to 3)

5.2 集合的函数式 API

要在一个水果集合里找到单词最长的那个水果,可以用如下代码实现:

val list = listOf("apple", "orange", "pear")
var maxLengthFruit = ""
for (fruit in list){
    if (fruit.length > maxLengthFruit.length){
        maxLengthFruit = fruit
    }
}
println("max length fruit is $maxLengthFruit")

但如果使用集合的函数式API,就可以简化为:

val list = listOf("apple", "orange", "pear")
val maxLengthFruit = list.maxBy { it.length }
println("max length fruit is $maxLengthFruit")

上面代码使用了 Lambda 表达式的语法结构,只需一行代码就能找到集合中单词最长的水果。

Lambda 就是一小段可以作为参数传递的代码,它的语法结构如下:

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

最外层是一对大括号,若有参数传入到 Lambda 表达式,需要声明参数列表,参数列表结尾用符号 -> 表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码,并且最后一行代码会自动作为返回值。

当然,多数情况下我们写的更多的是简化的写法,以上面例子为例,maxby 就是一个普通的函数,接收了一个 Lambda 类型的参数,若刚开始套用 Lambda 表达式的语法结构,可变成如下:

val list = listOf("apple", "orange", "pear")
val lambda = { fruit: String -> fruit.length }
val maxLengthFruit = list.maxBy(lambda) // maxBy 函数实质上是接收了一个 Lambda 参数

由于可以直接将 lambda 表达式传入 maxBy 函数中,因此可简化为:

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

Kotlin 规定,当 Lambda 参数是函数的最后一个参数时,可将 Lambda 表达式移到函数括号外面,如下:

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

如果 Lambda 参数是函数的唯一一个参数的话,可将函数的括号省略:

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

由于 Kotlin 拥有类型推导机制,Lambda 表达式中的参数列表大多数情况下可不必声明参数类型,从而进一步简化为:

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

最后,当 Lambda 表达式的参数列表只有一个参数时,也不必声明参数名,可用 it 关键字代替:

val maxLengthFruit = list.maxBy { it.length }

接下来介绍几个集合中比较常用的函数式 API:

  • map 函数

集合中的 map 函数用于将集合中的每个元素都映射成一个另外的值,映射的规则在 Lambda 表达式中指定,最终生成一个新的集合。

如把所有水果名变成大写:

val list = listOf("apple", "orange", "pear")
val newList = list.map { it.toUpperCase(Locale.ROOT) } // 新的列表水果名都是大写的
  • filter 函数

filter 函数是用来过滤集合中的数据的,可单独使用,也可配合 map 一起使用。

如只保留5个字母以内的水果且所有水果名大写:

val list = listOf("apple", "orange", "pear")
val newList = list.filter { it.length <= 5 }.map { it.toUpperCase(Locale.ROOT) }

注:上面若改成先调用 map 再调用 filter 函数,效率会差很多,因为这相当于对集合的所有元素进行一次映射转换后再过滤。

  • any 和 all 函数

any 函数用于判断集合中是否至少存在一个元素满足指定条件。

all 函数用于判断集合中是否所有元素都满足指定条件。

用法如下:

val list = listOf("apple", "orange", "pear")
val anyResult = list.any { it.length <= 5 } // 集合中是否存在5个字母以内的单词,返回 true
val allResult = list.all { it.length <= 5 } // 集合中是否所有单词都在5个字母内,返回 false
println("anyResult is $anyResult , allResult is $allResult")

5.3 Java 函数式 API 使用

在 Kotlin 代码中调用 Java 方法,若该方法接收一个 Java 单抽象方法接口(接口中只有一个待实现方法)参数,就可以使用函数式 API。

如 Java 原生 API 中的 Runnable 接口就是一个单抽象方法接口:

public interface Runnable {
    // 这个接口中只有一个待实现的 run() 方法
    void run();
}

以 Java 的线程类 Thread 为例,Thread 类的构造方法中接收一个 Runnable 参数,Java 代码创建并执行一个子线程:

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

上面代码用 Kotlin 实现如下:

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

上面 Thread 类的构造方法是符合 Java 函数式 API 使用条件的,因此可简化为:

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

若一个 Java 方法的参数列表只有唯一一个单抽象方法接口参数,可把接口名省略:

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

当 Lambda 表达式是方法的最后一个参数时,可把它移到方法括号外面,同时如果它还是方法的唯一一个参数,可把方法的括号省略:

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

注:以上 Java 函数式 API 的使用都限定与从 Kotlin 中调用 Java 方法,并且单抽象方法接口也必须是 Java 语言定义的。

6. 空指针检查

先看一段简单的 Java 代码:

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

若向 doStudy() 方法传入一个 null 参数,那么上面代码就会报空指针异常,更加稳妥的做法是做判空处理:

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

若用 Kotlin 实现上面 doStudy() 函数,如下:

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

它和 Java 版本没啥区别,但它是没有空指针风险的,因为 Kotlin 默认所有参数和变量都不可空,当你传一个 null 参数时,编译器会提示错误:

向 doStudy() 方法传入 null 参数

Kotlin 把空指针异常的检查提前到了编译时期,程序若存在空指针异常的风险,那么在编译时会直接报错。

如果希望传入的参数可为空,Kotlin 中在类名后面加一个问号就可以了,比如 Int 表示不可为空的整型,而 Int? 就表示可为空的整形。

把上面代码中参数的类型由 Study 变为 Study?,如下:

允许 Study 参数为空

发现调用 doStudy() 函数时可以传入 null 参数了,但调用参数的两个方法时,会出现红色的错误提示,这是因为把参数改成了可空的 Study? 类型,此时调用参数的 readBooks()doHomework() 方法可能造成空指针异常。

还需要做个判空处理,就不会出现错误了:

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

当然,用一些判空辅助工具会更加简单进行判空处理,下面介绍几个 Kotlin 的判空辅助工具:

  • 操作符 ?.

操作符 ?. 的作用是当对象不为空时正常调用相应的方法,为空时则什么都不做。

用操作符 ?. 上述代码可改为:

fun doStudy(study: Study?) {
    study?.readBooks()
    study?.doHomework()
}
  • 操作符 ?:

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

比如把如下代码:

val c = if (a != null) {
    a
} else {
    b
}

用操作符 ?: 就可简化为:

val c = a ?: b
  • 操作符 !!

操作符 !! 的作用是告诉 Kotlin 非常确信对象不会为空,不需要 Kotlin 帮忙做空指针检查,若出现问题再直接抛出空指针异常。

比如以下代码:


做判空处理还是编译失败

上面代码中 printUpperCase() 函数并不知道外部已经对 content 进行了非空检查,在调用 toUpperCase() 方法时,还认为存在空指针风险,从而编译不通过。这种情况想要强制通过编译,可在对象的后面加上!!,如下:

fun printUpperCase(){
    val upperCase = content!!.toUpperCase(Locale.ROOT)
    println(upperCase)
}
  • let 函数

let 函数提供了函数式 API 的编程接口,并将原始调用对象作为参数传递到 Lambda 表达式中,如下:

obj.let { obj2 ->  // 这里的 obj2 和 obj 是同一个对象
    // 编写具体的业务逻辑
}

let 函数属于 Kotlin 中的标准函数,可以处理全局变量的判空问题(if 判断语句无法做到这一点),它配合操作符 ?. 可以在做空指针检查时起到很大作用。

如上述的 doStudy() 函数代码可用 let 函数进行优化,如下:

fun doStudy(study: 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()
    }
}

7. Kotlin 中的小技巧

7.1 字符串内嵌表达式

Kotlin 允许在字符串里嵌入 ${} 这种语法结构的表达式,并在运行时使用表达式执行的结果替代这一部分内容,大大提升了易读性和易用性:

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

当表达式仅有一个变量时,可将两边的大括号省略:

"hello, $name, nice to meet you"

举个例子:

val name = "Wonderful"
val age = 18
println("My name is " + name + ", " + age + "years old")

用字符串内嵌表达式的写法可简化为:

val name = "Wonderful"
val age = 18
println("My name is $name, $age years old")

7.2 函数的参数默认值

Kotlin 中,定义函数时给任意参数设定一个默认值,调用时就不会强制为此参数传值,在此参数没传值的情况下使用设定的默认值。如:

// 这里给第二个参数 str 设定了个默认值 “hello”
fun printParams(num: Int, str: String = "hello"){
    println("num is $num , str is $str")
}

这样调用时可不用给第二个参数传值,如printParams(123)。但如果改成给第一个参数设定默认值的话:

// 这里给第一个参数 num 设定了个默认值 100
fun printParams(num: Int = 100, str: String){
    println("num is $num , str is $str")
}

此时再调用诸如 printParams("world") 就会报类型匹配错误了,这时需要通过键值对的方式来传参,从而不必按照参数定义的顺序来传参,如 printParams(str = "world")

给函数设定参数默认值这个功能,使得主构造函数很大程度上替代了次构造函数,从而次构造函数比较少使用到。

本篇文章就介绍到这。

推荐阅读更多精彩内容