18 KiB
引入 Kotlin
预计时间 1d
个人认为,Kotlin 是最好的 JVM 语言。
如果你会写 Java 或 Cpp/Python
推荐 IDE
这边推荐 Jetbrains 的 IntelliJ IDEA
当然,各种类型的 VS Code,Neo Vim 也是很不错的,这边不提供具体教学。
Hello World!
先创建一个完全空的 Kotlin 项目/文件,在里面写:
fun main() {
println("Hello World!")
}
编译运行。要么点击上方的绿色开始键,要么是 main 函数左侧的绿色开始键。
结果:
Hello World
进程已结束,退出代码为 0
这是一切的开始。
变量
变量很简单,对于一个数字变量,可以直接这么写:
val a = 1
var b = 2
这里,val 的意思是,a 是一个不可变的变量,var 的意思是,b 是一个可变的变量。
更准确地说,val 是只读变量:你不能让 a 再指向另一个对象,但如果它指向的对象本身可变,对象内部仍然可能改变。初学阶段可以先把它理解成“不能重新赋值的变量”。
类型推断与显式类型
Kotlin 会根据右边的值自动推断类型,这叫类型推断:
val intValue = 1 // Int
val longValue = 1L // Long
val doubleValue = 3.14 // Double
val floatValue = 3.14f // Float
val text = "Kotlin" // String
val flag = true // Boolean
val letter = 'K' // Char
如果需要,也可以显式写出类型:
val a: Int = 1
val b: String = "abc"
val c: Double = 3.0
Kotlin 的类型都使用大写开头,例如 Int、Long、Double、Boolean、String、Char。
数字还支持常见的非十进制写法:
val hex = 0xFF
val binary = 0b1010
val underscore = 1_000_000
字符串模板
Kotlin 的字符串可以直接嵌入变量或表达式:
val name = "Kotlin"
val version = 2
println("Hello, $name!")
println("Version: ${version + 1}")
$name 用于简单变量,${...} 用于表达式。
常量、延迟初始化与懒加载
如果值在编译期就能确定,并且是不可变的,可以使用 const val。它通常写在顶层或 object / companion object 中:
const val MAX_COUNT = 100
const val DEFAULT_NAME = "guest"
普通类或函数中不能定义 const 变量。类中的只读属性通常用 val:
class Demo {
val normal: Int = 1
lateinit var text: String
val lazyValue: Int by lazy {
println("第一次访问时才会计算")
42
}
}
lateinit var:允许稍后初始化,适合不能在构造函数中确定的值。直接使用未初始化的lateinit变量会抛异常。by lazy:第一次访问时才执行 lambda 并得到值,之后缓存结果。适合初始化成本较高的只读属性。
可空类型
Kotlin 把“可能为空”写进类型里:
var name: String = "Kotlin"
// name = null // 编译错误
var nullableName: String? = null
nullableName = "Kotlin"
String? 表示这个变量可能是 String,也可能是 null。要访问可空对象,需要安全调用:
val length = nullableName?.length ?: 0
?.:如果对象为null,整个表达式返回null。?::Elvis 运算符,如果左边为null,使用右边的值。!!:强制认为对象非空,如果真的是null会抛异常。除非你很确定,否则少用。
函数
函数的作用,是把一段逻辑取个名字,之后可以重复调用。Kotlin 的函数比 Java 更自由:函数可以直接写在文件顶层,不必先放进某个类里。
基本写法
Kotlin 函数使用 fun 关键字:
fun add(a: Int, b: Int): Int {
return a + b
}
fun:声明这是一个函数。add:函数名。a: Int, b: Int:参数列表。每个参数都必须写成参数名: 类型。: Int:返回值类型。表示这个函数会返回一个Int。{ ... }:函数体。return:返回最终结果,可以是表达式,也可以是一个变量。
调用函数时直接写函数名和参数:
val result = add(1, 2)
println(result) // 3
main 函数
Kotlin 程序从 main 函数开始执行:
fun main() {
println("Hello World!")
}
main 是顶层函数,不需要写在类里面。它没有参数,也没有返回值时,返回类型可以省略。
没有返回值
如果函数没有返回值,可以省略返回类型,也可以显式写 Unit:
fun printMessage(message: String) {
println(message)
}
fun printMessage2(message: String): Unit {
println(message)
}
Unit 相当于 Java / C++ 里的 void。实际写代码时,大多数情况直接省略即可。
Unit 函数里也可以写 return,但后面不跟值:
fun printPositive(number: Int) {
if (number <= 0) {
return
}
println(number)
}
这里 return 的含义是终止函数。
单表达式函数
如果函数体只有一个表达式,可以省掉大括号,直接写等号:
fun add(a: Int, b: Int): Int = a + b
fun square(x: Int) = x * x
square 的返回类型可以由编译器推断出来。不过如果是给别人用的公开函数,建议还是把返回类型写清楚,代码会更易读。
默认参数
Kotlin 支持给参数设置默认值:
fun greet(name: String = "Kotlin") {
println("Hello, $name!")
}
greet() // Hello, Kotlin!
greet("World") // Hello, World!
默认参数的好处是,不需要为了几种常见情况写很多重载函数。调用时如果不传这个参数,就使用默认值。
写出参数名
调用函数时,可以写出参数名:
fun createUser(name: String, age: Int, city: String = "Unknown") {
println("$name, $age, $city")
}
createUser(age = 18, name = "Alice")
命名参数的意义是,调用处更清楚每个值代表什么。参数很多时尤其有用。命名参数还可以和默认参数配合,只传需要修改的部分。
变长参数
如果参数个数不确定,可以使用 vararg:
fun sum(vararg numbers: Int): Int {
var result = 0
for (number in numbers) {
result += number
}
return result
}
println(sum(1, 2, 3))
println(sum(10, 20, 30, 40))
vararg 参数在函数内部可以当作数组一样遍历。一个函数通常只有一个 vararg,而且一般放在参数列表最后。
如果已经有一个数组,可以用 * 展开后传进去:
val numbers = intArrayOf(1, 2, 3)
println(sum(*numbers))
局部函数
函数内部也可以定义函数,这叫局部函数:
fun outer() {
fun inner(x: Int): Int {
return x * 2
}
println(inner(3)) // 6
}
局部函数只在外部函数内部可见,适合把一段只在这里使用的逻辑拆出来,避免重复。
函数也可以作为值
Kotlin 里函数不只能调用,还可以像变量一样传递。函数类型写成:
(参数类型, 参数类型) -> 返回类型
例如:
val square: (Int) -> Int = { x -> x * x }
这里的 square 是一个变量,它的类型是 (Int) -> Int,表示接收一个 Int,返回一个 Int。具体 lambda 写法后面会详细讲。
控制流
控制流决定代码按什么顺序执行。Kotlin 的 if、when 都是表达式,也就是它们可以直接产生一个值。
if 表达式
普通写法:
val a = 10
val b = 20
if (a > b) {
println("a 更大")
} else {
println("b 更大")
}
因为 if 可以返回值,所以可以这样写:
val max = if (a > b) a else b
println(max) // 20
也可以写代码块,最后一行作为结果:
val result = if (a > b) {
println("a 更大")
a
} else {
println("b 更大")
b
}
Kotlin 没有三目运算符 ? :,因为 if ... else ... 已经能当表达式使用。
when 表达式
when 类似其他语言的 switch,但更灵活:
fun describe(obj: Any): String {
return when (obj) {
1 -> "One"
"Hello" -> "Greeting"
is Long -> "Long"
!is String -> "Not a string"
else -> "Unknown"
}
}
特点:
- 不需要
break。 - 每个分支用
->。 - 可以匹配具体值、区间、类型、条件。
when也可以作为表达式,直接把结果赋给变量。
val score = 85
val level = when (score) {
in 90..100 -> "优秀"
in 80 until 90 -> "良好"
in 60 until 80 -> "及格"
else -> "不及格"
}
如果 when 作为表达式使用,并且没有覆盖所有情况,就必须写 else。
for 循环
遍历区间:
for (i in 1..5) {
println(i) // 1 2 3 4 5
}
.. 表示闭区间,包含左右两端。
左闭右开区间用 until:
for (i in 1 until 5) {
println(i) // 1 2 3 4
}
降序和步长:
for (i in 5 downTo 1) {
println(i)
}
for (i in 1..10 step 2) {
println(i) // 1 3 5 7 9
}
遍历集合:
val items = listOf("apple", "banana", "orange")
for (item in items) {
println(item)
}
同时拿到下标和值:
for ((index, value) in items.withIndex()) {
println("$index: $value")
}
while 与 do-while
while 先判断条件,再执行:
var i = 0
while (i < 3) {
println(i)
i++
}
do-while 先执行一次,再判断条件:
var j = 0
do {
println(j)
j++
} while (j < 3)
break 与 continue
break 结束整个循环,continue 跳过本次循环,进入下一次:
for (i in 1..10) {
if (i == 3) {
continue
}
if (i == 8) {
break
}
println(i)
}
嵌套循环中,可以用标签指定跳出哪一层:
outer@ for (i in 1..3) {
for (j in 1..3) {
if (i == 2 && j == 2) {
break@outer
}
println("$i, $j")
}
}
初学阶段知道有标签即可,平时不一定会经常用到。
集合
集合是日常写程序最常用的工具之一。Kotlin 把集合分成“只读”和“可变”两类。
创建集合
val list = listOf(1, 2, 3, 4, 5, 6) // 只读 List
val mutableList = mutableListOf(1, 2, 3) // 可变 List
val set = setOf(1, 2, 2, 3) // 只读 Set,自动去重
val mutableSet = mutableSetOf(1, 2, 3)
val map = mapOf("a" to 1, "b" to 2) // 只读 Map
val mutableMap = mutableMapOf<String, Int>()
val array = arrayOf("1", "2", "3") // 数组
val ints = intArrayOf(1, 2, 3) // 基本类型数组
listOf、setOf、mapOf 创建的是只读集合,不能添加、删除元素。需要修改时用 mutableListOf、mutableSetOf、mutableMapOf。
访问与修改
val list = listOf(10, 20, 30)
println(list[0]) // 10
println(list.first()) // 10
println(list.last()) // 30
println(list.size) // 3
可变列表可以修改:
val list = mutableListOf(10, 20, 30)
list.add(40)
list.remove(20)
list[0] = 100
println(list) // [100, 30, 40]
Map 的访问:
val map = mutableMapOf("a" to 1, "b" to 2)
println(map["a"]) // 1
map["c"] = 3
println(map) // {a=1, b=2, c=3}
遍历集合
val list = listOf("a", "b", "c")
for (item in list) {
println(item)
}
list.forEach { item ->
println(item)
}
遍历 Map:
val map = mapOf("a" to 1, "b" to 2)
for ((key, value) in map) {
println("$key = $value")
}
常用集合操作
Kotlin 的集合操作很简洁,很多操作会返回一个新集合,而不是修改原集合:
val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }
// [2, 4, 6, 8, 10]
val evens = numbers.filter { it % 2 == 0 }
// [2, 4]
val total = numbers.sum()
// 15
val hasBig = numbers.any { it > 4 }
// true
val allPositive = numbers.all { it > 0 }
// true
val firstBig = numbers.find { it > 3 }
// 4
val sorted = numbers.sortedDescending()
// [5, 4, 3, 2, 1]
val text = numbers.joinToString()
// "1, 2, 3, 4, 5"
链式调用:
val result = numbers
.filter { it % 2 == 0 }
.map { it * 10 }
println(result) // [20, 40]
这种写法在 Kotlin 中非常常见。filter 负责筛选,map 负责转换,最后得到新的结果。
Lambda 与高阶函数
Lambda 是一段可以当作值传递的代码。高阶函数则是“接收函数作为参数”或者“返回函数”的函数。
Lambda 基本写法
val add = { a: Int, b: Int ->
a + b
}
println(add(1, 2)) // 3
Lambda 用大括号包起来,箭头左边是参数,箭头右边是函数体。函数体最后一行会作为返回值,不需要写 return。
没有参数:
val hello = {
println("Hello")
}
hello()
只有一个参数时,可以省略参数名,用 it 代替:
val square: (Int) -> Int = { it * it }
println(square(5)) // 25
也可以自己命名参数:
val square2: (Int) -> Int = { x -> x * x }
Lambda 的类型写法:
val func: (Int, String) -> String = { number, text ->
text + number
}
(Int, String) -> String 表示:接收一个 Int 和一个 String,返回一个 String。
高阶函数
如果函数的参数或返回值是函数,它就是高阶函数:
fun applyTwice(x: Int, operation: (Int) -> Int): Int {
return operation(operation(x))
}
val result = applyTwice(3) { it * 2 }
println(result) // 12
这里 operation 的类型是 (Int) -> Int,表示它接收一个 Int,返回一个 Int。
尾随 lambda
如果函数的最后一个参数是 lambda,可以把 lambda 写在圆括号外面:
fun repeatTwice(action: () -> Unit) {
action()
action()
}
repeatTwice {
println("Hello")
}
如果 lambda 是唯一参数,圆括号也可以省略:
repeatTwice {
println("Kotlin")
}
如果函数还有其它参数,最后一个 lambda 仍然可以写在外面:
val sum = listOf(1, 2, 3).fold(0) { acc, item ->
acc + item
}
fold 的第一个参数是初始值 0,后面的 lambda 负责累加。
返回函数
函数也可以返回另一个函数:
fun makeAdder(x: Int): (Int) -> Int {
return { y -> x + y }
}
val add5 = makeAdder(5)
println(add5(3)) // 8
返回的 lambda 可以捕获外部变量 x,这种能力让 Kotlin 在写回调和 DSL 时非常方便。
可空函数类型
函数类型的变量也可以为空:
var callback: (() -> Unit)? = null
callback?.invoke()
callback = { println("Done") }
callback?.invoke()
?.invoke() 表示如果 callback 不为空,就调用它。
空安全
Kotlin 把“这个值可能为 null”写进类型系统里。这样可以减少很多运行时的空指针异常。
可空类型
普通类型不能赋值为 null:
var name: String = "Kotlin"
// name = null // 编译错误
如果确实可能为空,要在类型后面加 ?:
var nullableName: String? = null
nullableName = "Kotlin"
String 和 String? 是两种不同的类型。String? 表示它可能是 String,也可能是 null。
安全调用
访问可空对象时,使用 ?.:
val name: String? = null
val length = name?.length
如果 name 是 null,整个表达式返回 null,不会抛异常。所以 length 的类型是 Int?。
Elvis 运算符
?: 表示如果左边为 null,就使用右边的值:
val name: String? = null
val length = name?.length ?: 0
println(length) // 0
Elvis 运算符经常和安全调用一起使用:
val displayName = user?.name ?: "匿名用户"
非空断言
!! 表示“我确定它不是 null”:
val name: String? = "Kotlin"
val length = name!!.length
如果 name 真的是 null,这里会抛出异常。除非你非常确定,否则不要随便用 !!。
let 处理可空值
let 可以让一段代码只在对象非空时执行:
val name: String? = "Kotlin"
name?.let {
println("长度是 ${it.length}")
}
如果 name 是 null,let 里面的代码不会执行。
安全转换
类型转换时,可以用 as?,转换失败会返回 null,而不是抛异常:
val obj: Any = "hello"
val text: String? = obj as? String
对应的 as 如果转换失败,会抛异常。初学阶段知道 as? 更安全即可。
类与接口
这部分只讲最基础的概念。类里面的成员函数、继承细节、接口默认实现等内容,本引入不展开。
类
最简单的类:
class MyClass
带主构造函数的类:
class Person(val name: String, var age: Int)
val name:只读属性,创建后不能重新赋值。var age:可变属性,可以修改。
创建对象:
val person = Person("Alice", 20)
println(person.name)
person.age = 21
println(person.age)
如果需要在创建对象时执行一些初始化逻辑,可以用 init 块:
class Person(val name: String) {
init {
println("创建了 $name")
}
}
类里面也可以定义函数,但本引入先不展开成员函数。
继承
Kotlin 的类默认不能被继承。要允许继承,需要加 open:
open class Animal(val name: String)
class Dog(name: String) : Animal(name)
子类继承父类时,需要在类名后面写出父类构造调用。
接口
接口用 interface 定义:
interface Greeter {
fun greet(): String
}
接口可以只声明函数,也可以提供默认实现。这里先知道接口长什么样即可。
数据类
如果类的主要作用是保存数据,可以用 data class:
data class User(val id: Int, val name: String)
val user = User(1, "Alice")
println(user) // User(id=1, name=Alice)
val copied = user.copy(name = "Bob")
println(copied) // User(id=1, name=Bob)
data class 会自动生成 toString、equals、hashCode、copy 等方法,很适合表示简单数据。是一个替代 Lombook 的好东西。