Skip to content

此文是在阅读《Kotlin进阶实战》中所写的一些笔记记录,其中一些内容之前学习过故会引出连接。

函数与类

函数

基本格式就是:

kotlin
fun <函数名>(): <返回值> {
    ...
}

这些基本的与Java差不多的就不再赘述了。

返回Unit的函数

在Kotlin中,没有void,函数总会返回一个值,如果该函数不返回任何类型的对象,那么就会返回Unit类型。

kotlin
fun printHello(): Unit {
    println("Hello World")
}

其中Unit可以省略:

kotlin
fun printHello() {
    println("Hello World")
}

返回Nothing的函数

Unit相比,Nothing的区别是在Nothing后面执行的代码,均不能执行。

kotlin
fun doForever(): Nothing {
    while(true) {
        println("do something")
    }
}

fun main() {
    doForever()
    println("done") //IDE上会提示"Unreachable code"
}

单表达式函数

当函数只是返回一个表达式时括号可省略:

kotlin
fun sum(x: Int, y: Int): Int {
    return x + y
}

等同于:

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

返回的类型Int也被省略的原因是Kotlin自身的推导机制。

局部函数

局部函数,指在一个函数中定义另一个函数,类似于内部类。在局部函数中,可以直接访问外部函数的局部变量。

kotlin
fun validate(username: String): Boolean {
    fun validateInput(input: String?) {
        if(input == null || input.isEmpty()) {
            throw IllegalArgumentException("must not be empty")
        }
    }
    validateInput(username)
    return true
}

尾递归函数

尾调用指一个函数最后一个动作是一个函数调用的情况,即这个调用的返回值直接被当前函数返回。这种情况下称该调用位置为尾位置。若这个函数在尾位置调用本身(或调用本身的其他函数等),则称为尾递归,是递归的一种特殊情形。

尾调用不一定是递归调用(因为可以调用其他函数),但尾递归特别有用

kotlin
fun sum(n: Int, result: Int): Int = if(n <= 0) result else sum(n - 1, result + n)
fun main() {
    println(sum(100000, 0))
}

这时会抛出Exception in thread "main" java.lang.StackOverflowError的异常,因为在Kotlin中使用尾递归有两个条件:

  • 使用tailrec关键词修饰函数
  • 在函数最后进行递归调用

加上后tailrec关键字后:

kotlin
tailrec fun sum(n: Int, result: Int): Int = if(n <= 0) result else sum(n - 1, result + n)

再运行就能编译成功了。

与Java不同,Kotlin的类默认都是final的,如果某一个类需要被其他类继承,就需要使用open修饰。Kotlin的类有一个共同的超类Any

构造函数和初始化块

一个类可以包括一个主构造函数和N个次构造函数。

Kotlin进阶学习这篇文章中,提到过不使用次构造函数而是使用函数默认参数值的方法。

主构造函数

Kotlin的主构造函数可以借助初始化块对代码进行初始化。Kotlin使用init关键字作为初始化前缀:

kotlin
class <类名> {
    init { //初始化块
        ...
    }
}

初始化块可以有多个,调用主构造函数时会按照初始化块的顺序执行

上述init代码实际上等价于使用constructor关键字作为构造函数的函数名,只不过可以省略:

kotlin
class <类名> constructor{
    init { //初始化块
        ...
    }
}
次构造函数

Kotlin的次构造函数同样使用constructor作为函数名,但不能省略函数名。次构造函数可以包含代码,调用次构造函数时必须调用主构造函数。

kotlin
//次构造函数
class Constructor(str: String) {
    init {
        println(str)
    }
    constructor(str1: String, str2: String):this(str1) {
        println("$str1 $str2")
    }
    fun foo() = println("this is foo function")
}

fun main() {
    val obj = Constructor("hello", "world")
    obj.foo()
}

特性:

  • 类可以有多个次构造函数
  • 主构造函数的属性可以用var,val修饰,而次构造函数则不能进行修饰。
  • 每个次构造函数需要委托给主构造函数,调用次构造函数时会先调用主构造函数以及初始化块

属性

声明属性的完整语法:

kotlin
var <propertyName> [: <PropertyType>] [= <property_initializer>]
	[<getter>]
	[<setter>]

var可以有gettersetter两种方法,而val只有getter方法。比如:

kotlin
data class HttpResponse<T>(
    var code: Int = -1,
    var message: String? = null,
    var data: T? = null
) : Serializable {
    val isOkStatus: Boolean
        get() = code == 0
}

幕后字段(backing field)

这是Kotlin属性自动生成的字段,只能在当前属性的访问器内部使用,且拓展属性也不能使用backing field。如:

kotlin
var paramValue: Int = 0
	get() = paramValue
	set(value) {
        this.paramValue = value
    }

当我们尝试获取值时就会递归调用getter。Kotlin为每个属性提供了自动的backing field,可以使用field访问,便于在使用getter(),setter()时替换变量。

kotlin
var paramValue: Int = 0
	get() = field
	set(value) {
        field = value
    }

抽象类

与Java相同,不多赘述。

嵌套类和内部类

嵌套类

Kotlin嵌套类是指在某一个类内部的类,不能访问外部类成员。

Kotlin
Class Outter {
    val str:String = "Hello world"
    class Nested {
        fun foo() = println("")
    }
}

这时候去调用Outter1.Nested().foo()就会报错。

内部类

我们如果把它声明成内部类,就可以了:

kotlin
Class Outter {
    val str:String = "Hello world"
    inner class Nested {
        fun foo() = println("")
    }
}

枚举类

Kotlin中的枚举类需要用enumclass两个关键字修饰:

kotlin
enum class Color constructor(var colorName: String, var value: Int) {
    RED("红色", 1), GREEN("绿色", 2), BLUE("蓝色", 3)
}

对象声明和对象表达式

对象声明、对象表达式、和伴生对象都用到了object关键字。

对象声明

object关键字后面指定对象名称,可以实现单例模式。

kotlin
object Singleton1 {
    fun printlnHelloWorld() = println("hello world")
}
对象表达式

类似于Java匿名内部类,比如在网络请求接口回调时,Callback接口经常写成对象表达式:

kotlin
    override fun onLogin(username: String, password: String) {
        model.login(username, password, object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                 view.showError(e.message.toString())
            }

            override fun onResponse(call: Call, response: Response) {
                ...
            }

        })
    }

伴生对象

伴生对象相当于Java的静态代码块,因为Kotlin本身没有static关键字。

kotlin
class Student {
    companion object {
        ...
    }
}

数据类

Kotlin中用data关键字修饰类:

kotlin
data class User(var name: String, var password: String)

数据类不能被继承,那如何在同一超类型的多个数据类之间共享属性、方法呢?可以用抽象类或接口,在父类中共享一个属性使用abstract修饰,然后在子类里面覆盖

密封类

Kotlin中的密封类用sealed关键字修饰。

kotlin
sealed class Mammal(val name: String)
class Dog(dogName: String): Mammal(dogName)
class Horse(horseName: String) : Mammal(horseName)
class Human(humanName: String, val job: String): Mammal(humanName)
fun greetMammal(mammal: Mammal) = when(mammal) {
    is Dog -> "Hello ${mammal.name}"
    is Horse -> "Hello ${mammal.name}"
    is Human -> "Hello ${mammal.name}, You're working as a ${mammal.job}"
}

fun main() {
    println(greetMammal(Dog("wang")))
    println(greetMammal(Horse("chitu")))
    println(greetMammal(Human("tony", "coder")))
}

密封类特点:

  • 密封类是一个抽象类
  • when表达式配合使用时,如果能覆盖所有情况,则无需添加else

Kotlin函数式编程

函数式编程与高阶函数

在函数式编程中,函数是头等对象头等函数,这意味着一个函数,既可以作为其它函数的输入参数值,也可以从函数中返回值,被修改或者被分配给一个变量。λ演算是这种范型最重要的基础,λ演算的函数可以接受函数作为输入参数和输出返回值。

比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

高阶函数曾经对高阶函数有过记录,故在这儿放个链接。

lambda表达式

lambda函数式编程lambda表达式同样也记录过

集合,序列和Java中的流

map

对集合执行一个操作并获取其上下文:

kotlin
listOf("java","kotlin","scala","groovy")
	.map{it.toUpperCase()}
	.foreach(::println)

flatmap

遍历所有元素,为每一个创建一个集合,最后将集合放在一个集合中。

Sequence

序列是另一种容器类型,类似于集合将集合转成序列只需要listOf().asSequence(),序列与集合有着相同的函数API。

使用Sequence有助于避免不必要的临时分配开销,可以显著提高复杂处理PipeLines的性能。

序列和流

序列和流都使用的是惰性求值。

惰性求值被称为传需求调用,是计算机编程的一个概念,目的是最小化计算机要做的工作。可以表示为“延迟求值”和“最小化求值”。除了可以提升性能,还可以构造一个无限的数据类型。

内联函数与扩展函数

Kotlin扩展函数

Kotlin内联函数

委托

委托介绍

**委托模式(delegation pattern)**是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项基本技巧,许多其他的模式,如状态模式策略模式访问者模式本质上是在更特殊的场合采用了委托模式。委托模式使得我们可以用聚合来替代继承,它还使我们可以模拟mixin

在Kotlin中支持委托模式,使用by关键字来委托

kotlin
//接口
interface Base {
    fun print()
}

//实现此接口被委托的类
class BaseImpl(val x: Int) : Base {
    override fun print() {
        print(x)
    }
}

//看成代理类,使用by关键字进行委托
class Derived(b: Base) : Base by b

fun main() {
    //委托
    val base = BaseImpl(10)
    Derived(base).print() //输出10
}

委托属性

顾名思义,就是将自身属性的值的管理委托一个代理类进行统一管理,不再依赖于自己的getter/setter方法。委托属性的语法:

kotlin
val/var <property name>: <Type> by <expression>

如:

kotlin
//委托属性
class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "${property.name}: $thisRef"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("value=$value")
    }
}

class User {
    var name: String by Delegate()
    var password: String by Delegate()
}

fun main() {
    //委托属性
    val u = User()
    println(u.name)
    u.name = "Tony"
    println(u.password)
    u.password = "123456"
}

泛型

类型擦除

Kotlin的泛型拥有自己的特点。比如对扩展函数涉及泛型的类,需要指定泛型参数,必须是具体类型或子类型。

kotlin
fun <T: View> T.longClick(block: (T) -> Boolean) = setOnLongClickListener{block(it as T)}

Java通过类型擦除支持泛型

类型擦除是在使用泛型时,编译时会将类型擦除掉,比如List<String>等均会被擦除成List<Object>

在Kotlin中,我们可以通过声明匿名内部类、反射和内联函数来获得泛型信息,以匿名内部类为例:

kotlin
object Generic1 {
    open class InnerClass<T>
    fun main() {
        val innerClass = object : InnerClass<Int>() {
            //匿名内部类的声明在编译时进行,实例化在运行时进行
        }
    }
}

型变

类和类型

Kotlin中,类和类型是不一样的概念。类型总结了具有相同特征的一组对象的共同特征。类型可以说是一个抽象接口,它指定了如何使用对象。类表示该类型的实现,它是具体的数据结构和方法集合。比如List是类,而List<String>是类型。

ClassType
StringYesYes
String?NoYes
ListYesYes
List<String>NoYes

同理,子类与子类型也有很大区别。

任何时候,如果需要的是A类型值的任何地方,都可以使用B类型值来替换,就可以说B类型是A类型的子类型或者称A类型是B类型的超类型。

型变

型变是指类型转换后的继承关系。分为逆变,协变和不变。

协变

如果A是B的子类型,并且Generic<A>Generic<B>的子类型,那么Generic<T>可以称为一个协变类。

Java上界通配符<? extends T>

Java协变是通过上界通配符实现。

如果DogAnimal的子类,但List<Dog>并不是List<Animal>的子类。例如,下面的代码会在编译时报错:

java
List<Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
animals = dogs;

而如果我们使用上界通配符后,List<Dog>变成了List<? extends Animal>的子类型,即animals变成了可以放入任何Animal及其子类的List

java
List<? extends Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
animals = dogs;
Kotlin关键词out
kotlin
var animals: List<Animal> = ArrayList()
val dogs = ArrayList<Dog>
animals = dogs

上述代码在Kotlin中居然没有编译报错,因为Kotlin的List源码中使用了out,相当于Java的上界通配符。

当类的参数类型使用了out后,该参数只能出现在方法的返回类型中。

@UnsafeVariance

List的contains(),containsAll(),indexOf(),lastIndexOf()方法中,入参均出现了泛型E,并使用@UnsafeVariance修饰,因为这里的修饰,才打破了out的使用限制,否则编译会报错。

逆变

如果A是B的子类型,并且Generic<B>Generic<A>的子类型,那么Generic<T>可以称为一个逆变类。

Java的下界通配符<? super T>

Java的逆变通过下界通配符实现。

java
List<? extends Animal> animals = new ArrayList<>();
animals.add(new Dog());

上面代码会报错,因为他是协变的,无法添加新的对象。(原因:这个数组表示为Animal的任意一种子类的数组,但不确定具体是哪一种,故编译器无法确定具体类型,所以为了类型安全,禁止添加任何具体类型的对象。)

使用下界通配符后,代码就能编译通过:

java
List<? super Animal> animals = new ArrayList<>();
animals.add(new Dog());

? super Animal表示Animal及其父类。所有animals可以接受所有Animal的子类到列表中。

Java的上界通配符和下界通配符符合PESC(Profucer Extends, Consumer Super)原则。如果参数化类型是一个生产者,则使用<? extends T>;如果是一个消费者,则使用<? super T>

生产者表示频繁往外读取数据T,而不从中添加数据。消费者表示只往里插入数据而不读取数据。

Kotlin的关键词in

相当于Java的下界通配符。同样也是在List的源码中使用的。

当类的参数类型使用in后,该参数只能出现在方法的入参之中。

不变

默认情况下,Kotlin中的泛型类是不变的。这意味着它们既不是协变的,也不是逆变的。

泛型约束,类型投影与星号投影

泛型约束

Java中用extends关键字指明上界,Kotlin中用:代替extends对泛型的类型上界进行约束。

上界
kotlin
fun <T: Number> sum(vararg param: T) = param.sumByDouble { it.toDouble() }
fun main() {
    val result1 = sum(1,10,0.6)
    val result2 = sum(1,10,0.6,"kotlin") //compile error
}

上述代码传参只能是Number及其子类,传其他类型时就会报错。

Kotlin默认的上界是Any?

where关键字

一个类型参数需要指定多个约束时,Java中使用&连接多个类和接口。

而在Kotlin中,使用where关键字来实现多个约束

kotlin
class MyClass<T>(var variable: Class<T>) where T: Class A, T: ClassB

类型投影

MutableList是不可变的,可读可写。那假如我们对其使用inout修饰呢?

kotlin
fun main() {
    val list1: MutableList<String> = mutableListOf()
    list1.add("hello")
    list1.add("world")
    val list2:MutableList<out String> = mutableListOf()
    list2.add("hello")
    list2.add("world") // compile error
    val list3: MutableList<in String> = mutableListOf()
    list1.add("hello")
    list1.add("world")
    lateinit var list4: MutableList<String>
    list4 = list3 // compile error
}

使用out时会报错,因为该参数只能出现在方法的返回类型中。而使用in进行赋值时报错是因为只能出现在入参中。

星号投影

星号投影表示不知道关于泛型实参的任何信息。

类似于Java中的无界类型通配符"?",Kotlin使用星号投影""。""代指了所有类型,相当于Any?。例如, MutableList<*>表示MutableList<out Any?>

而由于使用out修饰以及星号投影类型的不确定性,导致写入的任何值都有可能跟原有类型冲突。因此星号类型不能写入,只能读取。