Kotlin系列教程--持续更新

531 阅读4分钟

文章主要参考链接

Kotlin系列教程——史上最全面、最详细的学习教程,持续更新中....

Android 进阶:基于 Kotlin 的 Android App 开发实践

Kotlin协程它不香吗?

Kotlin 的协程用力瞥一眼

var 与 val

// 定义一个不可为空的变量,用var修饰的变量可以被重新赋值,用val修饰的变量则不能,但是不能赋值为null
var a : Int = 12

/*
    定义可空类型的变量,即变量可以被赋值为null
    定义格式为: 修饰符 变量名 : 类型? = 值
*/
var nullA : Int? = 12
nullA = null

Elvis操作符

  • ?.该符号的用法为:可空类型变量?.属性/方法。如果可空类型变量为null时,返回null
var str : String? = "123456"
str = null

println(str?.length)   // 当变量str为null时,会返回空(null)

  • ?:这个操作符表示在判断一个可空类型时,会返回一个我们自己设定好的默认值.
val testStr : String? = null

var length = 0

// 例: 当testStr不为空时,输出其长度,反之输出-1

// ?: 写法
length = testStr?.length ?: -1

println(length)// -1

函数

定义一个返回类型为Int的函数

fun returnFun() : Int{
    return 2
}

单表达式函数

当函数返回单个表达式时,可以省略函数体的花括号

fun sum(x: Int=0, y: Int): Int {
    return x + y
}
等价于:

fun sum(x: Int=0, y: Int): Int =  x + y

它还等价于:因为可以通过编译器来推断该函数的返回类型。

fun sum(x: Int=0, y: Int) =  x + y

嵌套类(Nested Class)

Kotlin 的嵌套类是指定义在某一个类内部的类,嵌套类不能够访问外部类的成员。除非嵌套类变成内部类。

class Outter1 {

    val str:String = "this property is from outter1 class"

    class Nested {

        fun foo() = println("")
    }
}

fun main(args: Array<String>) {
    Outter1.Nested().foo()
}

内部类(Inner Class)

Kotlin 的内部类使用inner关键字标识,内部类能够访问外部类的成员。

class Outter2 {

    val str:String = "this property is from outter2 class"

    inner class Inner {

        fun foo() = println("$str")
    }
}

fun main(args: Array<String>) {
    Outter2().Inner().foo()
}

小结一下嵌套类和内部类:

  • 默认的是嵌套类
  • 嵌套类不持有外部类的引用,内部类持有外部类的引用
  • 嵌套类的创建方式:外部类.嵌套类()
  • 内部类的创建方式:外部类().内部类()

类的构造函数

  • 在Kotlin中,允许有一个主构造函数和多个二级构造函数(辅助构造函数、次构造函数)。其中主构造函数是类头的一部分。
  • 关键字或者构造函数名:constructor(参数)

主构造函数

主构造函数是类头的一部分,类名的后面跟上构造函数的关键字以及类型参数。

class Test constructor(num : Int){
     ...
}
或者
/*
     因为是默认的可见性修饰符且不存在任何的注释符
     故而主构造函数constructor关键字可以省略
*/
class Test(num: Int){
      ...
}

构造函数中的初始化代码块init init{...}中能使用构造函数中的参数

fun main(args: Array<String>) {
    // 类的实例化,会在下面讲解到,这里只是作为例子讲解打印结果
    var test = Test(1)
}

class Test constructor(var num : Int){
    init {
        num = 5
        println("num = $num")// 5
    }
}

二级构造函数

Kotlin中支持二级构造函数。它们以constructor关键字作为前缀。

class Test{
    constructor(参数列表){

    }
}

同时存在主构造函数和二级构造函数时的情况

如果类具有主构造函数,则每个辅助构造函数需要通过另一个辅助构造函数直接或间接地委派给主构造函数。 使用this关键字对同一类的另一个构造函数进行委派:

fun main(args: Array<String>) {
    var test1 = Test(1)
    var test2 = Test(1,2)
}

// 这里是为了代码清晰,故而没有隐藏constructor关键字
class Test constructor(num: Int){

    init {
        println("num = $num")
    }

    constructor(num : Int, num2: Int) : this(num) {
        println(num + num2)
    }
}

说明:二级构造函数中的参数1(num),是委托了主构造函数的参数num。

可以看出,当实例化类的时候只传1个参数的时候,只会执行init代码块中的代码。当传2个参数的时候,除了执行了init代码块中代码外,还执行了二级构造函数中的代码。

输出结果为:

num = 1
num = 1
3

继承类的基础使用

定义继承类的关键字为:open。不管是类、还是成员都需要使用open关键字。


open class 类名{
     ...
     open var/val 属性名 = 属性值
     ...
     open fun 函数名()
     ...
 }

这里定义一个继承类Demo,并实现两个属性与方法,并且定义一个DemoTest去继承自Demo

open class Demo{

    open var num = 3

    open fun foo() = "foo"

    open fun bar() = "bar"

}

class DemoTest : Demo(){
    // 这里值得注意的是:Kotlin使用继承是使用`:`符号,而Java是使用extends关键字
}

fun main(args: Array<String>) {

    println(DemoTest().num)
    DemoTest().foo()
    DemoTest().bar()

}

输出

3
foo
bar

继承类的构造函数

  • 无主构造函数

当实现类无主构造函数时,则每个二级构造函数必须使用super关键字初始化基类型,或者委托给另一个构造函数。 请注意,在这种情况下,不同的辅助构造函数可以调用基类型的不同构造函数

例:这里举例在Android中常见的自定义View实现,我们熟知,当我们指定一个
组件是,一般实现继承类(基类型)的三个构造函数。

class MyView : View(){

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
        : super(context, attrs, defStyleAttr)
}
  • 存在主构造函数

当存在主构造函数时,主构造函数一般实现基类型中参数最多的构造函数,参数少的构造函数则用this关键字引用即可了

// 同样以自定义组件为例子

class MyView(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
    : View(context, attrs, defStyleAttr) {

    constructor(context: Context?) : this(context,null,0)
    
    constructor(context: Context?,attrs: AttributeSet?) : this(context,attrs,0)
}

重写

当基类中的函数,没有用open修饰符修饰的时候,实现类中出现的函数的函数名不能与基类中没有用open修饰符修饰的函数的函数名相同,不管实现类中的该函数有无override修饰符修饰。

读着有点绕,直接看例子你就明白了。

open class Demo{
    fun test(){}   // 注意,这个函数没有用open修饰符修饰
}

class DemoTest : Demo(){
    
    // 这里声明一个和基类型无open修饰符修饰的函数,且函数名一致的函数
    // fun test(){}   编辑器直接报红,根本无法运行程序
    // override fun test(){}   同样报红
}

方法重载

在文章的开头提到了多态这个特性,方法的重载其实主要体现在这个地方。即函数 名相同,函数的参数不同的情况。这一点和Java是相同的

open class Demo{
    open fun foo() = "foo"
}

class DemoTest : Demo(){

    fun foo(str: String) : String{
        return str
    }

    override fun foo(): String {
        return super.foo()
    }
}    

fun main(args: Array<String>) {
    println(DemoTest().foo())
    DemoTest().foo("foo的重载函数")
}

输出:

foo
foo的重载函数

覆盖规则

这里的覆盖规则,是指实现类继承了一个基类,并且实现了一个接口类,当我的基类中的方法、属性和接口类中的函数重名的情况下,怎样去区分实现类到底实现哪一个中的属性或属性。 这一点和一个类同时实现两个接口类,而两个接口都用同样的属性或者函数的时候是一样的。

open class A{
    open fun test1(){ println("基类A中的函数test1()") }

    open fun test2(){println("基类A中的函数test2()")}
}

interface B{
    fun test1(){ println("接口类B中的函数test1()") }

    fun test2(){println("接口类B中的函数test2()")}
}

class C : A(),B{
    override fun test1() {
        super<A>.test1()
        super<B>.test1()
    }

    override fun test2() {
        super<A>.test2()
        super<B>.test2()
    }
}

接口

fun main(args: Array<String>) {

   // 类的初始化
   var demo = Demo1()

   demo.fun1()
}

/**
 * 我定义的接口
 */
interface Demo1Interface{

    // 定义的方法
    fun fun1()
}

/**
 * 接口的实现类
 */
class Demo1 : Demo1Interface{
    override fun fun1() {
        println("我是接口中的fun1方法")
    }
}

数据类

在Java中,或者在我们平时的Android开发中,为了解析后台人员给我们提供的接口返回的Json字符串,我们会根据这个字符串去创建一个类或者实例对象,在这个类中,只包含了一些我们需要的数据,以及为了处理这些数据而所编写的方法。这样的类,在Kotlin中就被称为数据类。

  • data为声明数据类的关键字,必须书写在class关键字之前。
  • 在没有结构体的时候,大括号{}可省略。
  • 构造函数中必须存在至少一个参数,并且必须使用val或var修饰。这一点在下面数据类特性中会详细讲解。
  • 参数的默认值可有可无。(若要实例一个无参数的数据类,则就要用到默认值)

使用数据类

声明数据类的关键字为:data

data class 类名(var param1 :数据类型,...){}
或者
data class 类名 可见性修饰符 constructor(var param1 : 数据类型 = 默认值,...)

// 定义一个名为User的数据类
data class User(val name : String, val pwd : String)

修改数据类属性

Koltin要修改数据类的属性,则使用其独有的copy()函数。其作用就是:修改部分属性,但是保持其他不变

val mUser = User("kotlin","123456")
println(mUser)
val mNewUser = mUser.copy(name = "new Kotlin")
println(mNewUser)

输出结果

User(name=kotlin, pwd=123456)
User(name=new Kotlin, pwd=123456)

解构声明

val mUser = User("kotlin","123456")
val (name,pwd) = mUser
println("name = $name\tpwd = $pwd")

输出结果

name = kotlin	pwd = 123456

抽象类

使用抽象类

关键字:abstract

abstract class Lanauage{
    val TAG = this.javaClass.simpleName  // 自身的属性
    
    // 自身的函数
    fun test() : Unit{
        // exp
    }
    abstract var name : String           // 抽象属性
    abstract fun init()                  // 抽象方法
}

/**
 * 抽象类Lanauage的实现类TestAbstarctA
 */
class TestAbstarctA : Lanauage(){

    override var name: String
        get() = "Kotlin"
        set(value) {}

    override fun init() {
        println("我是$name")
    }
}

/**
 * 抽象类Lanauage的实现类TestAbstarctB
 */
class TestAbstarctB : Lanauage(){
    override var name: String
        get() = "Java"
        set(value) {}

    override fun init() {
        println("我是$name")
    }
}

fun main(args: Array<String>) {
    
    // val lanauage = Lanauage() 是错误的,因为抽象类不能直接被实例化
    
    val mTestAbstarctA = TestAbstarctA()
    val mTestAbstarctB = TestAbstarctB()

    println(mTestAbstarctA.name)
    mTestAbstarctA.init()

    println(mTestAbstarctB.name)
    mTestAbstarctB.init()
}

内部类

使用内部类

关键字 inner

class Other{            // 外部类
    val numOther = 1

    inner class InnerClass{     // 嵌套内部类
        val name = "InnerClass"
        fun init(){
            println("我是内部类")
        }
    }
}

fun main(args: Array<String>) {
   Other().InnerClass().init()  // 调用格式为:外部类().内部类().内部类方法/属性
}

Lambda

Lambda介绍

无参数的情况

val/var 变量名 = { 操作的代码 }

// 源代码
fun test(){ println("无参数") }
  
// lambda代码
val test = { println("无参数") }

// 调用
test()  => 结果为:无参数

有参数的情况

val/var 变量名 : (参数的类型,参数类型,...) -> 返回值类型 = {参数1,参数2,... -> 操作参数的代码 } 等价于👇 // 此种写法:即表达式的返回值类型会根据操作的代码自推导出来。 val/var 变量名 = { 参数1 : 类型,参数2 : 类型, ... -> 操作参数的代码 }

// 源代码
fun test(a : Int , b : Int) : Int{
    return a + b
}

// lambda
val test : (Int , Int) -> Int = {a , b -> a + b}
// 或者
val test = {a : Int , b : Int -> a + b}

// 调用
test(3,5) => 结果为:8

lambda表达式作为函数中的参数的时候

这里举一个例子: fun test(a : Int, 参数名 : (参数1 : 类型,参数2 : 类型, ... ) -> 表达式返回类型){ ... }

// 源代码
fun test(a : Int , b : Int) : Int{
    return a + b
}

fun sum(num1 : Int , num2 : Int) : Int{
    return num1 + num2
}

// 调用
test(10,sum(3,5)) // 结果为:18

// lambda
fun test(a : Int , b : (num1 : Int , num2 : Int) -> Int) : Int{
    return a + b.invoke(3,5)
}

// 调用
test(10,{ num1: Int, num2: Int ->  num1 + num2 })  // 结果为:18

如果 Lambda 是函数的最后一个参数,你可以把 Lambda 写在括号的外面

view.setOnClickListener({ v: View ->
  switchToNextPage()
})

// 等价于
view.setOnClickListener() { v: View ->
  switchToNextPage()
}
// 而如果 Lambda 是函数唯一的参数,你还可以直接把括号去了:
view.setOnClickListener { v: View ->
  switchToNextPage()
}
// 另外,如果这个 Lambda 是单参数的,它的这个参数也省略掉不写:
view.setOnClickListener {
  switchToNextPage()
}
/*哎,不错,单参数的时候只要不用这个参数就可以直接不写了。
其实就算用,也可以不写,
因为 Kotlin 的 Lambda 对于省略的唯一参数有默认的名字:it
*/


view.setOnClickListener {
  switchToNextPage()
  it.setVisibility(GONE)
}

it

  • it并不是Kotlin中的一个关键字。
  • it是在当一个高阶函数中Lambda表达式的参数只有一个的时候可以使用it来使用此参数。it可表示为单个参数的隐式名称,是Kotlin语言约定的。
// 这里举例一个语言自带的一个高阶函数filter,此函数的作用是过滤掉不满足条件的值。
val arr = arrayOf(1,3,5,7,9)
// 过滤掉数组中元素小于2的元素,取其第一个打印。这里的it就表示每一个元素。
println(arr.filter { it < 5 }.component1())   

集合

List

  • 声明并初始化List的集合:使用listOf(..)函数
  • 声明并初始化MutableList的集合:使用mutableListOf(..)函数

Set

Set类型集合会把重复的元素去除掉。

  • 声明并初始化Set的集合:使用setOf(..)函数
  • 声明并初始化MutableSet的集合:使用mutableSetOf(..)函数

Map

当我们的键存在重复时,集合会过滤掉之前重复的元素。

  • 不可变的Map类型集合的初始化使用:mapOf()函数
  • 可变的Map类型集合的初始化使用:mutableMapOf()函数

扩展函数

class Extension2 {
    fun test() = println("this is from test()")
}

fun Extension2.test() = println("this is from extension function") // 扩展函数 test

fun main(args: Array<String>) {
    var extension2 = Extension2()
    extension2.test()
}

输出

this is from test()

当扩展函数跟原先的函数重名,并且参数都相同时,扩展函数就会失效,调用的是原先类的函数

对象表达式与对象声明

创建一个对当前类有一点小修改的对象,但不想重新声明一个子类。Java匿名内部类实现,kotlin使用对象表达式和对象声明实现

对象表达式

要创建一个对象表达式,一般是创建一个继承自某一类的匿名类的对象,使用关键字object,如果超类有构造函数,则必须传递适当的构造参数,多个超类用逗号隔开。

open class A(x: Int) {
    public open val y: Int = x
}

interface B { /*……*/ }

val ab: A = object : A(1), B {
    override val y = 15
}

请注意,匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的 返回类型或者用作公有属性的类型,那么该函数或属性的实际类型 会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any。在匿名对象 中添加的成员将无法访问

class C {
    // 私有函数,所以其返回类型是匿名对象类型
    private fun foo() = object {
        val x: String = "x"
    }

    // 公有函数,所以其返回类型是 Any
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x        // 没问题
        val x2 = publicFoo().x  // 错误:未能解析的引用“x”
    }
}

对象声明

所谓的对象声明,我们可以理解为 java 中的单例模式。 单例与伴随对象

日常开发中写一个单例类是很常见的行为,Kotlin中直接将这种设计模式提升到语言级别,使用关键词object定义单例类。这里需要注意,是全小写。Kotlin中区分大小写,Java中原本指所有类的父类Object已弃用。单例类访问直接使用类名,无构造函数。

object Shop(name: String) {
    fun buySomething() {
        println("Bought it")
    }
}
Shop.buysomething()

伴生对象

Java中使用static标识一个类里的静态属性或方法,可以被这个类的所以实现使用。Kotlin改为使用伴随对象,用companion修饰单例类object,来实现静态属性或方法功能。

class Mall(name: String) {
    companion object Shop {
        val SHOP_NAME: String = "McDonald" // 等同于Java中写public static String
        fun buySomething() { // 等同于Java中写public static void
            println("Bought it")
        }
    }
}
Mall.buySomething()

对象表达式和对象声明之间有一个重要的语义差别:

  • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

This 表达式 (Expression)

inner 关键字定义内部类 在内部类当中访问外部类,需要显示使用 this@OutterClass.fun() 的语法

class A {

    fun testA(){	}

    inner class B { // 在 class A 定义内部类 B

        fun testB(){	}

        fun foo() {
            this.testB() // ok
            this.testA() // 编译错误
            this@A.testA() // ok
            this@B.testB() // ok
        }
    }
}

kotlin 协程

参考链接

【码上开学】Kotlin 的协程用力瞥一眼

【码上开学】Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了

【码上开学】到底什么是「非阻塞式」挂起?协程真的更轻量级吗? Kotlin协程它不香吗?

Kotlin协程是什么:

Kotlin协程就是 Kotlin 提供的一套线程封装的 API,它可以用看起来同步的方式写出异步的代码

基本使用

launch 函数不是顶层函数,是不能直接用的,可以使用下面三种方法来创建协程:

🏝️
// 方法一,使用 runBlocking 顶层函数
runBlocking {
    getImage(imageId)
}

// 方法二,使用 GlobalScope 单例对象
//            👇 可以直接调用 launch 开启协程
GlobalScope.launch {
    getImage(imageId)
}

// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
//                                    👇 需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    getImage(imageId)
}

  • 方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。

  • 方法二和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会和 app 一致,且不能取消(什么是协程的取消后面的文章会讲)。

  • 方法三是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。

使用协程

如果只是使用 launch函数,协程并不能比线程做更多的事。不过协程中却有一个很实用的函数:withContext 。这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。

🏝️
launch(Dispachers.Main) {              // 👈 在 UI 线程开始
    val image = getImage(imageId)
    avatarIv.setImageBitmap(image)     // 👈 执行结束后,自动切换回 UI 线程
}

// suspend:👈 挂起      
// withContext(Dispatchers.IO): 👈 切换到 IO 线程,并在执行完成后切回 UI 线程                    
suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {// 👈 getImage代码将会运行在 IO 线程
    ...
}

注意点: 协程的挂起主要是切线程,只是它能在挂起之后自动的切回来。

suspend 的意义

这个 suspend 关键字,既然==它并不是真正实现挂起==,那它的作用是什么?

它其实是一个==提醒==。 函数的创建者对函数的使用者的提醒:我是一个耗时函数,我被我的创建者用挂起的方式放在后台运行,所以请在协程里调用我。 为什么 suspend 关键字并没有实际去操作挂起,但 Kotlin 却把它提供出来? 因为它本来就不是用来操作挂起的。 挂起的操作 —— 也就是切线程,依赖的是挂起函数里面的实际代码,而不是这个关键字。 supend它并没有做挂起操作的功能,==真正做挂起的是这个函数里面挂起函数,比如我们这里用的withContext这个自带的挂起函数==。

所以supend这个关键字,只是一个==提醒==。

🏝️
// 👇 redundant suspend modifier
suspend fun suspendingPrint() {
  println("Thread: ${Thread.currentThread().name}")
}

如果你创建一个 suspend 函数但它内部不包含真正的挂起逻辑,编译器会给你一个提醒:redundant suspend modifier,告诉你这个 suspend 是多余的。 因为你这个函数实质上并没有发生挂起,那你这个 suspend 关键字只有一个效果:就是限制这个函数只能在协程里被调用,如果在非协程的代码中调用,就会编译不通过。

自定义 suspend 函数

给函数加上 suspend 关键字,然后在 withContext 把函数的内容包住就可以了。

suspend fun getImage(imageId: Int) = 
       withContext(Dispatchers.IO) {
  }

常用的 Dispatchers

  • Dispatchers.Main:Android 中的主线程
  • Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
  • Dispatchers.Default:适合 CPU 密集型的任务,比如计算

如果使用协程,可以直接把两个并行请求写成上下两行,最后再把结果进行合并即可:

🏝️
coroutineScope.launch(Dispatchers.Main) {
    //            👇  async 函数之后再讲
    val avatar = async { api.getAvatar(user) }    // 获取用户头像
    val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
    val merged = suspendingMerge(avatar, logo)    // 合并结果
    //                  👆
    show(merged) // 更新 UI
}

可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码。需要注意的是 suspendingMerge 并不是协程 API 中提供的方法,而是我们自定义的一个可「挂起」的结果合并方法.