Kotlin

简介

Kotlin是一种现代但已经成熟的编程语言,它简洁、安全、可与Java和其他语言互操作,并提供了许多在多个平台之间重用代码的方法。它由JetBrains公司于2011年设计和开发,并在2016年正式发布。Kotlin旨在解决Java语言在编码效率和代码质量方面存在的问题,并且与Java语言完全兼容。Kotlin通过简化语法、提供更强大的功能以及减少样板代码的编写,使得开发者能够更高效地编写清晰、简洁而又安全的代码。


Android Studio中创建 Kotlin File文件

  • Kotlin Class 文件:用于定义一个类,适合面向对象编程的场景,如定义 ActivityFragment 等。
  • Kotlin File 文件:用于定义顶级函数、变量等,适合函数式编程的场景,如 Jetpack Compose 的 UI 组件定义。

这两者的选择主要取决于你要定义的内容和你所使用的编程范式。在 Jetpack Compose 中,由于其函数式编程的特点,使用 Kotlin File 是更自然的选择。而在传统的 Android 开发中,定义 Activity 类时,Kotlin Class 是更合适的。

在目标目录下,右键New - Kotlin Class/File选择Class或者File都可以,Android Studio会根据文件内容自动变更

image-20240822150142616 image-20240822150300407

Kotlin文件的运行入口是main()方法:

1
2
3
fun main() {
println("Hello Kotlin!")
}

标准函数和静态方法

标准函数with、run和apply

Kotlin的标准函数指的是Standard.kt文件中定义的函数,任何Kotlin代码都可以自由地调用。

with函数接收两个参数:任意类型的对象,lambda表达式。在lambda表达式中提供第一个参数对象的上下文,并使用lambda表达式中的最后一行代码作为返回值返回

1
2
3
4
val result = with(obj) {
// obj的上下文
"value" // 返回值
}

with函数可以在连续调用同一个对象的多个方法时,让代码更精简。

1
2
3
4
5
6
7
8
9
10
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = with(StringBuilder()) {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
toString()
}
println(result)

run函数的用法和使用场景其实和with函数是非常类似的,只是稍微做了一些语法改动而已。首先run函数通常不会直接调用,而是要在某个对象的基础上调用;其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。

1
2
3
4
val result = obj.run {
// obj的上下文
"value" // 返回值
}

apply函数和run函数也是极其类似的,都要在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。

1
2
3
4
// result == obj
val result = obj.apply {
// 这里是obj的上下文
}

定义静态方法

和绝大多数主流编程语言不同的是,Kotlin却极度弱化了静态方法这个概念,想要在Kotlin中定义一个静态方法反倒不是一件容易的事。
那么Kotlin为什么要这样设计呢?因为Kotlin提供了比静态方法更好用的语法特性,单例类

1
2
3
4
5
object Util {
fun doAction() {
println("do action")
}
}

虽然这里的doAction()方法并不是静态方法,但是我们仍然可以使用Util.doAction()的方式来调用,这就是单例类所带来的便利性。

不过,使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,而如果只是希望让类中的某一个方法变成静态方法的调用方式,可以使用companion object

1
2
3
4
5
6
7
8
9
10
class Util {
fun doAction1() {
println("do action1")
}
companion object {
fun doAction2() {
println("do action2")
}
}
}

companion object这个关键字实际上会在Util类的内部创建一个伴生类,而doAction2()方法就是定义在这个伴生类里面的实例方法。只是Kotlin会保证Util类始终只会存在一个伴生类对象,因此调用Util.doAction2()方法实际上就是调用了Util类中伴生对象的doAction2()方法。


Kotlin确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法,这些语法特性基本可以满足我们平时的开发需求了。然而如果需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层方法

如果我们给单例类或companion object中的方法加上@JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
class Util {
fun doAction1() {
println("do action1")
}

companion object {
@JvmStatic
fun doAction2() {
println("do action2")
}
}
}

@JvmStatic注解只能加在单例类或companion object中的方法上。由于doAction2()方法已经成为了真正的静态方法,那么现在不管是在Kotlin中还是在Java中,都可以使用Util.doAction2()的写法来调用了。


顶层方法指的是没有定义在任何类中的方法,Kotlin编译器会将所有的顶层方法全部编译成静态方法。想要定义一个顶层方法,首先需要创建一个Kotlin文件。对着任意包名右击 → New → Kotlin File/Class,在弹出的对话框中输入文件名即可,创建类型要选择File

例如创建一个Helper.kt文件:

1
2
3
fun doSomething() {
println("do something")
}

在Kotlin代码中,所有的顶层方法都可以在任何位置被直接调用,不用管包名路径,也不用创建实例,直接输入方法名即可

但如果是在Java代码中调用,因为Java中没有顶层方法这个概念,所有的方法必须定义在类中。Kotlin编译器会自动创建对应的的Java类,doSomething()方法以静态方法的形式定义在HelperKt类里。


扩展函数

扩展函数表示,即使不修改某个类的源码,仍然可以打开这个类,向该类添加新的函数。

1
2
3
fun ClassName.methodName(param1: Int, param2: Int): Int {
return 0
}

定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中。

文件名虽然并没有固定的要求,但是建议向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,这样便于以后查找。当然,扩展函数也是可以定义在任何一个现有类当中的,并不一定非要创建新文件。不过通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域

例如定义一个函数,用于统计字符串中字母的数量。可以创建一个String.kt文件:

1
2
3
4
5
6
7
8
9
10
fun String.lettersCnt(): Int {
var cnt = 0
// 自动拥有String实例的上下文,可以直接遍历this,this代表字符串本身
for (char in this) {
if (char.isLetter()) {
cnt++
}
}
return cnt
}

运算符重载

Kotlin允许我们将所有的运算符甚至其他的关键字进行重载,从而拓展这些运算符和关键字的用法。运算符重载使用的是operator关键字

例如让两个Money对象相加,Money.kt文件:

1
2
3
4
5
6
7
8
9
10
11
12
class Money(val value: Int) {

operator fun plus(money: Money): Money {
val sum = value + money.value
return Money(sum)
}

operator fun plus(newValue: Int): Money {
val sum = value + newValue
return Money(sum)
}
}

这样,就可以将两个Money对象相加,也能直接和数字相加。

1
2
3
4
5
6
val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2
val money4 = money3 + 20
println(money3.value)
println(money4.value)

高阶函数

如果一个函数接收另一个函数作为参数,或者返回值类型是另一个函数,该函数就称为高阶函数。

函数类型的基本规则:

1
(String, Int) -> Unit
  • ->的左边用来声明函数接收的参数,多个参数之间逗号分隔,如果不接收任何参数,写一对空括号
  • ->的右边用于声明函数的返回值类型,如果没有返回值就使用Unit

将上述函数类型添加到某个函数的参数声明或者返回值声明上,这个函数就是一个高阶函数:

1
2
3
fun example(func: (String, Int) -> Unit) {
func("hello", 123)
}

高阶函数允许让函数类型的参数来决定函数的执行逻辑。Lambda表达式是最常见也是最普遍的高阶函数调用方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}

fun main() {
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2) {
n1, n2 -> n1 + n2
}
val result2 = num1AndNum2(num1, num2) {
n1, n2 -> n1 - n2
}
println("result1 is $result1")
println("result2 is $result2")
}

apply函数可以用于给Lambda表达式提供一个指定的上下文,当需要连续调用一个对象的多个方法时,apply函数可以让代码更精简。

1
2
3
4
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}

这里给StringBuilder类定义了一个build扩展函数,这个扩展函数接收一个函数类型参数,并且返回值类型也是StringBuilder。

StringBuilder.:在函数类型的前面加上ClassName.,表示这个函数类型是定义在哪个类当中。

现在我们就可以使用自己创建的build函数来简化StringBuilder构建字符串的方式了。这里仍然用吃水果这个功能来举例:

1
2
3
4
5
6
7
8
9
10
11
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().build {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())
}

可以看到,build函数的用法和apply函数基本上是一模一样的,只不过我们编写的build函数目前只能作用在StringBuilder类上面,而apply函数是可以作用在所有类上面的。如果想实现apply函数的这个功能,需要借助于Kotlin的泛型才行

Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行开销完全消除。内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字声明即可

为什么Kotlin要提供一个noinline关键字来排除内联功能呢?内联的函数类型参数只允许传递给另一个内联函数,这是它最大的局限性。而非内联的函数类型参数可以自由地传递给其他任何函数。另外,内联函数和非内联函数有一个重要的区别,内联函数引用的lambda表达式可以使用return关键字进行函数返回非内联函数只能进行局部返回