三篇文章带你快速入门Kotlin(上)

374 阅读16分钟

三篇文章带你快速入门Kotlin(上)

Kotlin的发展历程

2011年,JetBrains发布了Kotlin的第一个版本,并在2012年将其开源。

2016年Kotlin发布了1.0正式版,代表着Kotlin语言已经足够成熟和稳定了,并且JetBrains也在自家的旗舰IDE开发工具IntelliJ IDEA中加入了Kotlin的支持。

2017年Google宣布Kotlin正式成为Android开发一级语言,并且Android Studio也加入了对Kotlin的支持。

2019年Google正式宣布了Kotlin First,未来提供的官方API也将会以Kotlin版本为主。

Kotlin相比于Java的优势

语法更加简洁,对于同样的功能,使用Kotlin开发的代码量可能会比使用Java开发的减少50%甚至更多。

语法更加高级,相比于Java比较老旧的语法,Kotlin增加了很多现代高级语言的语法特性,使得开发效率大大提升。

语言更加安全,Kotlin几乎杜绝了空指针这个全球崩溃率最高的异常。

和Java是100%兼容的,Kotlin可以直接调用使用Java编写的代码,也可以无缝使用Java第三方的开源库。这使得Kotlin在加入了诸多新特性的同时,还继承了Java的全部财富。

Kotlin的工作原理

Kotlin可以做到和Java 100%兼容,这主要是得益于Java虚拟机的工作机制。

其实Java虚拟机并不会直接和你编写的Java代码打交道,而是和编译之后生成的class文件打交道。

而Kotlin也有一个自己的编译器,它可以将Kotlin代码也编译成同样规格的class文件。

Java虚拟机不会关心class文件是从Java编译来的,还是从Kotlin编译来的,只要是符合规格的class文件,它都能识别。

也正是这个原因,JetBrains才能以一个第三方公司的身份设计出一门用来开发Android应用程序的编程语言。

函数和变量

变量

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

  • val(value的简写的简写)用来声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋值,对应Java中的final变量。

  • var(variable的简写的简写)用来声明一个可变的变量,这种变量在初始赋值之后仍然可以再被重新赋值复制,对应Java中的非final变量。

    fun main() {
        val a = 10
        var b = 5
        b = b + 3
        println("a = " + a)
        println("b = " + b)
    }
    

内嵌表达式

在Kotlin中,我们可以直接将表达式写在字符串里面,即使是构建非常复杂的字符串,也会变得轻而易举。

Kotlin中字符串内嵌表达式的语法规则如下:

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

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

"hello, $name. nice to meet you!"

函数

定义一个函数的语法规则如下:

fun methodName(param1: Int, param2: Int): Int {
      return 0
}

当一个函数的函数体中只有一行代码时,可以使用单行代码函数的语法糖:

fun methodName(param1: Int, param2: Int) = 0

使用这种写法,可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可

return关键字也可以省略,等号足以表达返回值的意思。

Kotlin还拥有出色的类型推导机制,可以自动推导出返回值的类型。

逻辑控制

if条件语句

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

Kotlin中的if语句相比于Java有一个额外的功能:它是可以有返回值的,返回值就是if语句每一个条件中最后一行代码的返回值。

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

仔细观察上述代码,你会发现value其实是一个多余的变量,我们可以直接将if语句返回,这样代码将会变得更加精简,如下所示:

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

当一个函数只有一行代码时,可以省略函数体部分,直接将这一行代码使用等号串连在函数定义的尾部。虽然largerNumber()函数不止只有一行代码,但是它和只有一行代码的作用是相同的,只是return了一下if语句的返回值而已,符合该语法糖的使用条件。那么我们就可以将代码进一步精简:

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

最后,还可以将上述代码再精简一下,直接压缩成一行代码:

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

when条件语句

当需要判断的条件非常多的时候,可以考虑使用when语句来替代if语句。

类似与switch语句又远比switch语句强大的多

分支可以时表达式,也可以时具体数值。

fun getScore1(name: String) = when (name) {
    "Tom" -> 86
    "Jack" -> 77
    "Jim" -> 33
    "Lily" -> 44
    else -> 0
}

高级用法:when不带参数写法,不常用,但是扩展性很强

fun getScore2(name: String) = when {
    name == "Tom" -> 86
    name.equals("Jack") -> 77
    name.endsWith("Jim") -> 33
    name.startsWith("Lily") -> 44
    else -> 0
}

when不带参数,多参数

fun getScore3(name: String, name1: String) = when {
    name == "Tom" -> 86
    name.equals("Jack") -> 77
    name1.endsWith("Jim") -> 33
    name1.startsWith("Lily") -> 44
    else -> 0
}

for-in循环语句

我们可以使用如下Kotlin代码来表示一个区间:

val range = 0..10

上述代码表示创建了一个0到10的区间,并且两端都是闭区间,这意味着0到10这两个端点都是包含在区间中的,用数学的方式表达出来就是[0, 10]。

也可以使用until关键字来创建一个左闭右开的区间:

val range = 0 until 10

如果你想跳过其中的一些元素,可以使用step关键字:

fun main() {
    for (i in 0 until 10 step 2) {
        println(i)
    }
}

如果你想创建一个降序的区间,可以使用downTo关键字:

fun main() {
    for (i in 10 downTo 1) {
        println(i)
    }
}

控制循环语句Break

退出当前循环

@ 标签可以指定退出外层循环

fun for(){
    out@ for (i in 0..5) {
        println("i$i")
        for (j in 0..3) {
            println("j$j")
            if (j == 1) {
                break@out
            }
        }
    }
}

控制循环语句Continue

退出本次循环

fun for(){
    out@ for (i in 0..5) {
        println("i$i")
        for (j in 0..3) {
            println("j$j")
            if (j == 1) {
                continue@out
            }
        }
    }
}

面向对象编程

类与对象

可以使用如下代码定义一个类,以及声明它所拥有的字段和函数:

class Person {
    var name = ""
    var age = 0
    fun eat() {
        println("name:" + name + " age:" + age)
    }
}

然后使用如下代码创建对象,并对对象进行操作:

fun main() {
    val p = Person()
    p.name = "yx"
    p.age = 66
    p.eat()
}

继承

Kotlin中一个类默认是不可以被继承的,如果想要让一个类可以被继承,需要主动声明open关键字:

open class Person {
    …
}

要让另一个类去继承Person类,则需要使用冒号关键字:

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

现在Student类中就自动拥有了Person类中的字段和函数,还可以定义自己独有的字段和函数。

构造器

  • 主构造器

constructor 关键字如果没有任何注解或修饰可以省略

如果没有为非抽象类定义任何主次构造器,系统会提供一个无参数的主构造器。默认修饰为public

一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器

  • 次构造器

任何一个类只能有一个主构造函数,但可以有多个次构造函数

如果定义了主构造器,那么次构造器必须委托主构造器

如果没有定义柱构造器则不用委托主构造器

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

init 关键字为初始化时回调的逻辑块

接口

Kotlin中定义接口的关键字和Java中是相同的,都是使用的interface:

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

而Kotlin中实现接口的关键字变量了冒号,和继承使用的是同样的关键字:

class Artist(var name: String) : Study {
    override fun readBook() {
        println("name:$name readBook")
    }

    override fun doHomework() {
        println("name:$name doHomework")
    }
}

数据类

Kotlin中使用data关键字可以定义一个数据类:

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

Kotlin会根据数据类的主构造函数中的参数将equals()、hashCode()、toString()等固定且无实际逻辑意义的方法自动生成,从而大大简少了开发的工作量。

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

class Cellphone1(val brand: String, val price: Double)

fun main() {

    val str = "{'userCode':'demo','userName':'Demo Group'}"
    val a = EncryptUtils.decryptBase64AES(
        str.toByteArray(),
        "PHA".toByteArray(),
        "CBC",
        "".toByteArray()
    )
    println(a)


    val cellPhone1 = CellPhone("apple", 1000.0)
    val cellPhone2 = CellPhone("apple", 1000.0)
    println(cellPhone1)
    println(cellPhone1 == cellPhone2)

    val cellPhone3 = Cellphone1("apple", 1000.0)
    val cellPhone4 = Cellphone1("apple", 1000.0)
    println(cellPhone3)
    println(cellPhone3 == cellPhone4)
}

单例类

Kotlin中使用object关键字可以定义一个单例类:

object Singleton {
    fun singletonTest() {
        println("singletonTest is called.")
    }
}

而调用单例类中的函数比较类似于Java中静态方法的调用方式:

Singleton.singletonTest()

这种写法虽然看上去像是静态方法的调用,但其实Kotlin在背后自动帮我们创建了一个Singleton类的实例,并且保证全局只会存在一个Singleton实例。

集合

集合的创建

使用如下代码可以初始化一个List集合:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")

使用如下代码可以初始化一个Set集合:

val set = setOf("Apple", "Banana", "Orange", "Pear", "Grape")

使用如下代码可以初始化一个Map集合

val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)

集合的取值

fun mutableList() {
    val list = mutableListOf("apple", "banana", "orange", "pear", "grape")
    list.add("other")
    for (fruit in list) {
        println(fruit)
    }
}

SetOf集合,去重的集合,底层是map 所以可以去重

fun setList() {
    val list = setOf("apple", "banana", "orange", "pear", "grape")

    for (fruit in list) {
        println(fruit)
    }
}

map 集合 java写法:键值对一对一

fun mapList() {
    val map = HashMap<String, Int>()
    map.put("apple", 1)
    map.put("banana", 2)
}

kotlin 可以用[]

fun mapListKotlin() {
    val map = HashMap<String, Int>()
    map["apple"] = 2
    map["banana"] = 1
    val number = map["apple"]
}

最简洁的写法 mapOf不可变,mutableMapOf可变

fun mapListKotlin1() {
    val map = mapOf("apple" to 1, "banana" to 2, "orange" to 3)

    val map1 = mutableMapOf("apple" to 1, "banana" to 2, "orange" to 3)
    map1["watermelon"] = 6
    for ((fruit, number) in map1) {
        println("mutable" + fruit + number)
    }
}

Lambda编程

Lambda就是一小段可以作为参数传递的代码。正常情况下,我们向某个函数传参时只能传入变量,而借助Lambda却允许传入一小段代码。

我们来看一下Lambda表达式的语法结构:

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

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

集合的函数式API

集合中的map函数是最常用的一种函数式API,它用于将集合中的每个元素都映射成一个另外的值,映射的规则在Lambda表达式中指定,最终生成一个新的集合。比如,这里我们希望让所有的水果名都变成大写模式,就可以这样写:

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
    val newList = list.map({ fruit: String -> fruit.toUpperCase() })
    for (fruit in newList) {
        println(fruit)
    }
}
  • 当Lambda参数是函数的最后一个参数时,可以将Lambda表达式移到函数括号的外面。
  • 如果Lambda参数是函数的唯一一个参数的话,还可以将函数的括号省略。
  • 由于Kotlin拥有出色的类型推导机制,Lambda表达式中的参数列表其实在大多数情况下也不必声明参数类型。
  • 当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替。

因此,Lambda表达式的写法可以进一步简化成如下方式:

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

推导过程

fun lambda1() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
    val max = list.maxByOrNull { it.length }
    println(max)
}

maxByOrNul实际上它一个普通函数,只不过它接受的是一个Lambda类型参数,每次集合遍历时把遍历的值作为参数传递给lambda表达式,工作原理就是找到最长值并返回

fun lambda2() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
    val lambda = { fruit: String -> fruit.length }
    val max = list.maxByOrNull(lambda)
    println(max)
}

可直接将lambda表达式传入maxByOrNull函数中

fun lambda3() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "other")

    val max = list.maxByOrNull({ fruit: String -> fruit.length })
    println(max)
}

Kotlin规定,当lambda参数是函数的最后一个参数时,可以将Lambda表达式移到函数括号外边

fun lambda4() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "other")

    val max = list.maxByOrNull() { fruit: String -> fruit.length }
    println(max)
}

Kotlin规定,如果Lambda参数是函数的唯一一个参数的话,还可以讲函数的括号省略

fun lambda5() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "other")

    val max = list.maxByOrNull { fruit: String -> fruit.length }
    println(max)
}

Kotlin有出色的类推导机制,大多数的情况下不必声明参数的类型

fun lambda6() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "other")

    val max = list.maxByOrNull { fruit -> fruit.length }
    println(max)
}

Kotlin规定,当lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替

fun lambda7() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "other")

    val max = list.maxByOrNull { it.length }
    println(max)
}

其他集合函数API:

使用map函数转换为大写

fun lamdabMap() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
    var newList = list.map { it.toUpperCase() }
}

filter函数过滤集合中的元素,这里注意调用顺序 先map 在filter 效率会低

fun lamdabMapFilter() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
    var newList = list.filter { it.length <= 5 }.map { it.toUpperCase() }
}

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

all函数判断集合中所有元素满足条件

这样是不是比每次循环方便多了

fun lamdabAnyAll() {
    val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
    val any = list.any { it.length <= 5 }
    val all = list.all { it.length <= 5 }
}

Java函数式API

如果我们在Kotlin代码中调用了一个Java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。Java单抽象方法接口指的是接口中只有一个待实现方法,如果接口中有多个待实现方法,则无法使用函数式API。

举个例子,Android中有一个极为常用的点击事件接口OnClickListener,其定义如下:

public interface OnClickListener {
     void onClick(View v);
}

可以看到,这是一个单抽象方法接口。假设现在我们拥有一个按钮button的实例,就可以使用函数式API的写法来注册这个按钮的点击事件:

button.setOnClickListener { v ->
}

推导过程

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

Runnable只有一个待实现方法,这里没有显示地重写run方法kotlin自动明白runnable后面的lambda表达式就是在run中的实现内容

fun lmClass1() {
    Thread(Runnable {
        println("Thead is run")
    }).start()
}

java方法的参数列表不存在一个以上java单抽象方法接口参数,还可以将接口名省略

fun lmClass2() {
    Thread({
        println("Thead is run")
    }).start()
}

当lambda表达式是方法的最后一个参数时,可以将lambda表达式移到方法括号外面


fun lmClass3() {
    Thread() {
        println("Thead is run")
    }.start()
}

同时lambda表大会还是方法的唯一一个参数,还可以方法的括号省略


fun lmClass4() {
    Thread {
        println("Thead is run")
    }.start()
}

空指针检查

空指针是一种不受编程语言检查的运行时异常,只能由程序员主动通过逻辑判断来避免,但即使是最出色的程序员,也不可能将所有潜在的空指针异常全部考虑到。

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

这段Java代码安全吗?不一定,因为这要取决于调用方传入的参数是什么,如果我们向doStudy()方法传入了一个null参数,那么毫无疑问这里就会发生空指针异常。因此,更加稳妥的做法是在调用参数的方法之前先进行一个判空处理,如下所示:

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

可空类型系统

Kotlin中引入了一个可空类型系统的概念,它利用编译时判空检查的机制几乎杜绝了空指针异常。

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

这段代码看上去和刚才的Java版本并没有什么区别,但实际上它是没有空指针风险的,因为Kotlin默认所有的参数和变量都不可为空,所以这里传入的Study参数也一定不会为空,可以放心地调用它的任何函数。

Kotlin提供了另外一套可为空的类型系统,就是在类名的后面加上一个问号。比如,Int表示不可为空的整型,而Int?就表示可为空的整型;String表示不可为空的字符串,而String?就表示可为空的字符串。

使用可为空的类型系统时,需要在编译时期就把所有的空指针异常都处理掉才行。

判空辅助工具

Kotlin提供了一系列的辅助工具,使开发者能够更轻松地进行判空处理。

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

if (a != null) {
    a.doSomething()
}

这段代码使用?.操作符就可以简化成:

a?.doSomething()

?: 操作符表示如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。比如:

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

这段代码的逻辑使用?:操作符就可以简化成:

val c = a ?: b

kotlin并非总是那么智能如下

我们发现一个可空的全局变量,在contextNull已经判断为空却报错kotlin识别不了

!!:我们使用强制判断不为空!!非空断言工具

var content: String? = "hello"
fun contextNull() {
    if (content != null) {
        upCase()
    }
}
fun upCase() {
    val upcase = content!!.toUpperCase()
    println(upcase)
}

结合使用?.操作符和let函数也可以对多次重复调用的某个变量统一进行判空处理

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

取文本长度的函数

fun getTextLength(text: String?): Int {
    if (text != null) {
        return text.length
    }
    return 0
}

简写为:由于text可为空?.判断返回null借助?:返回0

fun getTextLength1(text: String?) = text?.length ?: 0

函数默认值

Kotlin允许在定义函数的时候给任意参数设定一个默认值,这样当调用此函数时就不会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。语法格式如下:

fun printParams(num: Int, str: String = "hello") {
    println("num is $num , str is $str")
}

这里给printParams()函数的第二个参数设定了一个默认值,这样当调用printParams()函数时,可以选择给第二个参数传值,也可以选择不传,在不传的情况下就会自动使用默认值。

示例:

fun params(num: Int, str: String = "Hello") {
    println("num $num str $str")
}

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

fun params2(num: Int = 100, str: String = "Hello") {
    println("num $num str $str")
}

给构造函数传入默认入参,可以默认不传入参数


class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0) :
    Person3(name, age) {
    init {
        println("sno:$sno")
        println("grade:$grade")
    }

}

fun main() {
    params(123)
    //params1(123)
    params1(str = "wordla")
    params2(str = "11", num = 55)
    val s = Student()
}