快速上手Kotlin
1.Kotlin 语言简介
Kotlin 是 JetBrains 开发的基于 JVM 的静态编程语言,2017 年被 Google 定为 Android 官方一级语言。它完全兼容 Java,语法更简洁,自带空安全,支持函数式编程与协程,可用于 Android、后端、跨平台等场景,是比 Java 更现代、安全、高效的开发语言。
2.变量声明
Kotlin 作为静态强类型语言,变量声明兼具类型安全与语法简洁性,支持类型自动推导,同时设计了严格的空安全机制,其核心声明方式可从基础关键字声明、空安全声明、动态 / 任意类型声明、只读变量声明四个核心维度展开。
2.1 val/ var 基础声明
Kotlin 中var(可变变量)、val(只读变量)是最基础的声明关键字,支持类型自动推导,且推导后类型不可变更,
var:声明可变变量,赋值后可修改值,但类型仍不可变val:声明只读变量,赋值后不可修改
// 自动推到为 String 类型
var s1 = "hi world"
// 报错:类型已确定为 String,无法赋值 int
s1 = 1000
// val 只读,赋值后不可修改,类型同样自动推动
val s2 = 123
// 报错:val不可重新赋值
s2 = 456
2.2 空安全声明
在 Kotlin 中,空安全(Null Safety)不仅是一个特性,而是整个语言架构的核心。其核心目标是将潜在的 NullPointerException (NPE) 从运行时提前到编译期解决。
2.2.1 空安全核心语法
(1) 可空类型声明:Type vs Type?
Kotlin 中变量默认不可空,如果你尝试给一个普通类型的变量赋值为 null,编译器会直接报错。
- 不可空变量:必须在定义时或构造函数中初始化。
- 可空变量:在类型后添加
?,允许赋值为null。
var a: String = "Kotlin" // 不可空
// a = null // 编译错误
var b: String? = "Safe" // 可空
b = null // 正常执行
(2) 延迟初始化:处理“暂时无法赋值”的非空变量
有时变量必须是非空的,但无法在声明时立即初始化(例如在 onCreate 或依赖注入中赋值)。Kotlin 提供了两种方案
| 特性 | lateinit | by lazy |
|---|---|---|
| 关键字 | lateinit var | val ... by lazy { } |
| 可变性 | 必须是 可变变量 (var) | 必须是 只读变量 (val) |
| 类型限制 | 不能用于基本类型(Int, Float 等) | 可用于任何类型 |
| 初始化时机 | 手动后续赋值 | 第一次使用该变量时自动触发 Lambda 逻辑 |
| 安全性 | 若未初始化即访问,抛出异常 | 线程安全(默认),确保只初始化一次 |
// 1. lateinit:手动延迟赋值
lateinit var userName: String
fun initUser() { userName = "Alice" }
// 2. by lazy:懒加载赋值
val config: String by lazy {
println("正在读取复杂配置...")
"Optimized_Config"
}
2.2.2 安全操作符:优雅地处理 null
为了避免传统的 if (obj != null) 嵌套,Kotlin 提供了简洁的操作符:
(1) 安全调用符 ?.
如果变量不为空,则调用方法/属性;如果为空,则直接返回 null,不触发异常。
val len: Int? = name?.length
(2) Elvis 操作符(空合并符) ?:
类似于兜底策略:如果左侧表达式结果为 null,则返回右侧的默认值。
val len = name?.length ?: 0 // 如果 name 为空,len 结果为 0
(3) 非空断言 !!
强制告诉编译器:“我确定这里一定不为空,请把它当做非空类型处理”。
警告: 如果变量实际为 null,程序会立即崩溃并抛出 NPE。应尽量减少使用。
2.3 Any/Nothing?/dynamic: 任意类型声明
Kotlin 中提供了Any/Nothing?/dynamic声明可接收任意类型、且后续可变更类型的变量
2.3.1 Any 与 Any?:所有类型的根基类,
Any:所有非空类型的根类(Root)。- 特性:任何类型都可赋值给
Any声明的变量,但仅能调用Any自身的方法 / 属性(如toString()、hashCode()),调用子类特有方法会编译报错, - 限制:由于它不包含
null,所以不能将null赋值给Any类型变量。
- 特性:任何类型都可赋值给
-
Any?:这是真正意义上的“任意类型”。- 特性:它是
Any的可空版本,可以接收包括null在内的任何数据。
- 特性:它是
val x: Any = "Hello"
// x = null // 编译报错:Any 类型不可为空
val y: Any? = null // 正常执行
println(y.toString())
// 访问子类属性需要类型检查或转换
if (x is String) {
println(x.length) // 智能转换(Smart Cast),此时可以直接访问 length
}
2.3.2 dynamic: 特定平台的动态类型
重要提示:dynamic 关键字仅在 Kotlin/JS 目标平台中可用。
- 现状:在标准的 Kotlin/JVM(即 Android 开发环境)中,并不存在
dynamic类型。 - 特性(仅限 JS) :它会关闭编译器的类型检查。你可以对
dynamic变量调用任何方法或属性,编译器不会报错,但如果运行时该方法不存在,程序会崩溃。 - JVM 替代方案:在 Android 开发中,如果你需要类似的灵活性,通常使用泛型、
Any?配合类型判断,或者使用反射。
2.3.3 Nothing 与 Nothing?:底层的虚无
-
Nothing:它是所有类型的子类(Bottom Type)。- 核心用途:表示“永远不会返回的结果”。例如:一个始终抛出异常的函数、一个无限循环的函数。
- 逻辑意义:如果一个表达式的类型是
Nothing,说明代码执行到这里就会终止。
-
Nothing?:一个特殊的类型,它唯一可能的值就是null。- 特性:当你写
val a = null且没有声明类型时,编译器推断出的类型就是Nothing?。
- 特性:当你写
// 用途 1:表示函数不返回
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
// 用途 2:Nothing 是所有类型的子类,可以用于辅助推断
val data: String = fail("出错") // 编译通过,因为 Nothing 是 String 的子类
// Nothing? 示例
val n: Nothing? = null
// val m: Nothing? = 1 // 编译报错,只能是 null
2.4 Kotlin 变量声明与空安全总结
| 特性维度 | 关键字 / 语法 | 技术要点 |
|---|---|---|
| 基础推导 | var / val | var 可变,val 只读(类似常量)。 |
| 根类/动态 | Any / dynamic | Any 是所有类的根类;dynamic 仅用于 JS 互操作。 |
| 空安全声明 | Type? | 显式区分可空与不可空,编译期拦截风险。 |
| 延迟初始化 | lateinit / by lazy | lateinit 用于手动赋值,by lazy 用于自动懒加载。 |
| 安全调用 | ?. | 链式调用时保护程序不崩溃。 |
| 空合并 | ?: | 提供默认值,消除 if-else 判空逻辑。 |
| 非空断言 | !! | 开发者风险自担的强制转换。 |
3. 逻辑控制流
3.1 条件语句
在 Kotlin 中,if 和 when 是表达式,即它们可以返回一个值。
if / else :kotlin中没有三元运算符 (a > b ? a : b),因为 if 表达式可以直接完成这个工作。
// 每个分支的最后一行表达式,就是该分支的返回值。
val max = if (a > b) a else b // 直接作为结果返回
when(增强版 switch):when 是 Kotlin 中最常用的语法之一。它可以匹配常量、范围、类型甚至是表达式。
val result = when (x) {
1 -> "数值是 1"
2, 3 -> "数值是 2 或 3"
in 4..10 -> "数值在 4 到 10 之间"
is String -> "这是一个字符串,长度为 ${x.length}"
else -> "其他情况" // 必须覆盖所有可能性,或者提供 else
}
3.2 循环语句
for 循环与区间 (Ranges):Kotlin 的 for 循环通常配合区间使用
// 闭区间:[1, 5],包含 5
for (i in 1..5) print(i)
// 半开区间:[1, 5),不包含 5
for (i in 1 until 5) print(i)
// 步长与倒序
for (i in 10 downTo 0 step 2) print(i) // 10, 8, 6...
// 遍历集合
val list = listOf("Apple", "Banana")
for (item in list) println(item)
while 与 do-while: 与Java一致
4. 函数
4.1 基础声明与单表达式
如果函数体只有一行,可以省略大括号和 return。
// 标准写法: fun 方法名(参数列表)返回值 { 函数体 }
fun sum(a: Int, b: Int): Int {
return a + b
}
// 简写:单表达式函数
fun sum(a: Int, b: Int) = a + b
4.2 默认参数与命名参数
这是 Android 开发中减少“模板代码”的神器。
fun request(url: String, method: String = "GET", timeout: Int = 5000) {
print("请求 $url, 方式 $method")
}
fun main() {
// 调用时非常灵活
request("https://google.com") // 使用默认值
request("https://api.com", timeout = 1000) // 命名参数:只修改超时时间
request("https://www.baidu.com", "POST")
}
注意:默认参数不是只能放在最后,但强烈建议把带默认值的参数统一放后面,否则很容易踩坑。
5.类与对象
5.1 基础语法
Kotlin 的类声明极其精简。最常用的属性声明直接写在类名后面的 主构造函数 里。
class User(val name: String, var age: Int)
-
默认 Final:在 Kotlin 中,所有的类默认都是
final的(不可被继承)。如果你希望某个类能被继承,必须手动加上open关键字。这体现了 Kotlin 的“确定性”哲学。 -
init代码块:主构造函数不能包含代码。如果你需要在对象创建时执行逻辑,使用init块。 -
在 Kotlin 中,构造函数分为两种:主构造函数 (Primary Constructor) 和 次构造函数 (Secondary Constructor)
主构造函数 (Primary Constructor)
Kotlin 最常用的方式。直接跟在类名后面,既定义了参数,也定义了属性。
声明即定义
如果在参数前加上 val 或 var,Kotlin 会自动帮你做三件事:
- 声明一个同名的成员变量。
- 将参数值赋值给这个变量。
- 生成对应的 Getter/Setter。
// 极简写法:这一行代码就完成了属性声明和赋值
class User(val name: String, var age: Int)
// 如果不加 val/var,它仅仅是一个“临时参数”,只能在 init 块或属性初始化时使用
class User(name: String) {
val upperName = name.uppercase()
}
init 代码块
主构造函数不能包含任何逻辑代码。如果需要在对象创建时执行复杂的逻辑,请使用 init 关键字。
class Person(val name: String) {
init {
println("第一步:正在初始化 $name")
}
init {
println("第二步:可以在类中写多个 init 块,按顺序执行")
}
}
次构造函数 (Secondary Constructor)
虽然主构造函数能覆盖 90% 的场景,但有时你需要多个构造逻辑(例如为了兼容 Java 的多个构造函数,或者需要不同的逻辑入口)。
- 关键字:
constructor - 铁律:每一个次构造函数都必须直接或间接委托给主构造函数(使用
this关键字)。
class View {
var context: String
var attrs: String? = null
// 主构造函数(隐式的,无参)
constructor(context: String) {
this.context = context
}
// 次构造函数,委托给上面的构造函数
constructor(context: String, attrs: String) : this(context) {
this.attrs = attrs
}
构造参数的“高级玩法”
私有化构造函数 如果你想实现某种特殊模式(如单例或工厂),可以隐藏构造函数:
class Secret private constructor(val key: String) {
companion object {
fun create() = Secret("123456")
}
}
默认参数代替次构造函数
在 Kotlin 中,很少需要写多个次构造函数,因为你可以通过默认参数搞定一切。
// 这一个函数就涵盖了:无参构造、单参数构造、双参数构造
class Button(
val text: String = "Default",
val color: String = "Blue"
)
// 使用:
val b1 = Button()
val b2 = Button("Submit")
val b3 = Button(color = "Red") // 具名参数调用
5.2 数据类
kotlin中使用 data关键字声明数据类,数据类会自动生成
equals()/hashCode()(用于比较两个对象内容是否一致)。toString()(输出属性内容而非内存地址)。copy()(非常重要:用于创建一个属性微调后的新对象)。
data class Project(val id: Int, val title: String)
5.3 单例类
在 Android 开发中,我们经常需要单例(如 NetworkManager 或 ThemeConfig)。Kotlin 把单例模式直接做成了语法。
** object 声明(全局单例)**
不用写复杂的“双重检查锁定”,直接用 object。
object DatabaseConfig {
val dbName = "app_cache"
fun clear() { /* 逻辑 */ }
}
// 使用时直接调用,不需要实例化
DatabaseConfig.clear()
companion object (伴生对象)
Kotlin 中没有 static 关键字。如果你想让某个方法或变量可以通过类名直接访问,请使用伴生对象。
class Logger {
companion object {
const val TAG = "APP_LOG"
fun d(msg: String) = println("[$TAG] $msg")
}
}
// 调用
Logger.d("Something happened")
5.4 抽象类
抽象类:用 abstract class 定义,不能直接实例化,只能被继承。
特点:不能直接实例化,只能被继承,专门用来做父类模板。
abstract class Animal
// val animal = Animal() // 错误:抽象类不能创建对象
- 可包含:抽象方法 / 属性、普通方法、普通属性、构造方法。
- 抽象成员(
abstract fun/abstract val)无实现,子类必须重写。 - 子类继承后要实现所有抽象成员,用
override标注。 - 是单继承,可用来定义带有共性状态和行为的父类模板。
5.5 继承与接口
Kotlin 使用冒号 : 代替了 implements 和 extends。
继承
要求父类必须是 open 的,只能继承一个父类,可以重新父类方法
接口
Kotlin 使用 interface 关键字声明接口。通过冒号 : 来实现接口,并且必须使用 override 关键字重写方法。可以同时实现多个接口
- 接口可以有 抽象方法 (只声明不实现)和 默认方法
- 接口中的属性不能存储状态,必须是抽象的或提供 Getter
interface Device {
val brand: String // 抽象属性,子类必须提供值
val name: String get() = "Commoner" // 提供默认 Getter,但没有实际变量存储它
fun powerOn() // 抽象方法
fun selfCheck() { // 默认实现
println("$brand device is checking hardware...")
}
}
处理冲突:当两个接口撞名时
如果实现的两个接口中有同名的默认方法,子类必须显式重写该方法,并使用 super<InterfaceName> 指定调用哪一个。
interface A { fun foo() { println("A") } }
interface B { fun foo() { println("B") } }
class C : A, B {
override fun foo() {
super<A>.foo() // 调用 A 的实现
super<B>.foo() // 调用 B 的实现
}
}
函数时接口(SAM 转换)
如果一个接口只有一个抽象方法,可以用 fun interface 声明。这允许你像使用 Lambda 表达式一样简写它,在 Android 事件监听中极其常用。
fun interface OnClickListener {
fun onClick(id: Int)
}
// 简写调用:
val listener = OnClickListener { id -> println("Clicked $id") }
6. 数组
在 Kotlin 中,集合(List/Set/Map)是日常写业务逻辑的主力,而数组(Array) 则更多出现在底层交互、性能优化以及与 Java 代码的互操作中。Kotlin 的数组是定长的(Fixed-size) ,一旦分配了内存,大小就无法改变。
6.1 简介
对象数组 Array<T>
它是泛型数组。无论你传入什么类型,它在 JVM 层面都会被编译为对象数组。
Array<String>对应 JVM 的String[]。- 性能陷阱:如果你写
Array<Int>,它在 JVM 底层会被编译为Integer[]。这意味着每一个数字都会被**装箱(Boxing)**成对象,产生额外的内存开销和寻址损耗。
原生类型数组 (Primitive Arrays)
为了解决装箱带来的性能损耗,Kotlin 针对基本数据类型提供了专门的数组类:IntArray、ByteArray、FloatArray 等。
IntArray在 JVM 底层会直接编译为基本类型的int[]。ByteArray会编译为byte[](这在处理底层 IO 流、Socket 通信或图像二进制数据时是唯一选择)。- 优势:内存连续,无装箱开销,缓存命中率高,性能极致。
6.2 数组的创建与初始化
Kotlin 提供了极其简便的顶层函数来创建数组,同时支持通过 Lambda 表达式进行动态初始化。
// 1. 使用 arrayOf 创建对象数组
val strings: Array<String> = arrayOf("A", "B", "C")
val mixed = arrayOf(1, "Two", 3.0) // 推断为 Array<Any>
// 2. 使用原生类型专属函数创建(推荐用于高性能场景)
val numbers: IntArray = intArrayOf(1, 2, 3, 4, 5)
val bytes: ByteArray = byteArrayOf(0x01, 0x02, 0x03)
// 3. 动态初始化:指定大小,并用 Lambda 计算每个元素的值
// 创建一个大小为 5 的数组,元素为其索引的平方:[0, 1, 4, 9, 16]
val squares = IntArray(5) { index -> index * index }
// 创建一个全为 0 的数组,大小为 10
val zeros = IntArray(10)
6.3 数组的常用操作
虽然数组在底层和集合完全不同,但 Kotlin 为数组提供了与集合完全一致的扩展函数。你可以像操作 List 一样操作 Array:
Kotlin
val arr = intArrayOf(1, 2, 3, 4, 5)
// 遍历
for (i in arr) { println(i) }
// 使用函数式操作符(注意:大部分操作符执行后会返回一个新的 List,而不是 Array)
val doubledList = arr.map { it * 2 }
val filteredList = arr.filter { it > 3 }
// 数组的修改与访问(支持重载的 [] 操作符)
arr[0] = 10
val first = arr[0]
7. 集合
7.1 基础使用
在 Kotlin 中,集合被严格划分为只读(Read-only) 和 可变(Mutable) 两种。
| 集合类型 | 只读接口 (不可修改内容) | 可变接口 (可增删改) | 常用初始化函数 |
|---|---|---|---|
| List (有序列表) | List<T> | MutableList<T> | listOf(), mutableListOf() |
| Set (无重复集合) | Set<T> | MutableSet<T> | setOf(), mutableSetOf() |
| Map (键值对) | Map<K, V> | MutableMap<K, V> | mapOf(k to v), mutableMapOf() |
// 只读 List,连 add 和 remove 方法都没有
val readOnlyList = listOf("A", "B", "C")
// 可变 List
val mutableList = mutableListOf(1, 2, 3)
mutableList.add(4)
7.2 常用拓展函数
日常开发中每天都会用到的高频操作符。它们通常配合 Lambda 表达式 {} 一起使用,默认参数名为 it。
filter (过滤)
挑出符合条件的元素,丢弃不符合的。
val numbers = listOf(1, 2, 3, 4, 5, 6)
// 只保留偶数
val evens = numbers.filter { it % 2 == 0 }
// 结果: [2, 4, 6]
map (映射/转换)
将集合中的每一个元素变成另一种形态(非常适合做 DTO 到 UI Model 的转换)。
val users = listOf("Alice", "Bob")
// 把名字字符串,映射成 User 对象
val userObjects = users.map { name -> User(name) }
flatMap (平铺映射)
专门用来处理“集合中的集合”。把嵌套的结构“拍平”。
val nestedList = listOf(
listOf(1, 2),
listOf(3, 4)
)
val flat = nestedList.flatMap { it }
// 结果: [1, 2, 3, 4]
聚合与查找
any { ... }:只要有一个满足条件,就返回 true。all { ... }:必须全部满足条件,才返回 true。firstOrNull { ... }:找到第一个满足条件的元素,找不到返回 null(极其安全的查找方式,避免越界崩溃)。
性能优化 .asSequence()
对于追求极致性能和底层确定性的开发者来说,普通的链式调用有一个隐患:每一次操作(如 filter 或 map)都会在内存中创建一个新的临时集合。
如果你的数据量非常大,在操作链前加上 .asSequence() 就可以开启“懒加载”模式。它会把所有的操作符融合成一次迭代,杜绝中间集合的内存分配开销:
val optimizedResult = largeList.asSequence()
.filter { /* 复杂判断 */ }
.map { /* 复杂转换 */ }
.toList() // 只有在最后调用 toList() 时,前面的操作才会真正触发执行
小结:什么时候用 Array?什么时候用 List?
- 绝大多数业务场景:无脑使用
List或MutableList。API 丰富,支持扩容,使用安全。 - 极致性能场景:当需要在内存中进行大量密集的数值计算时,使用
IntArray/FloatArray等原生数组,避免 JVM 装箱开销。 - 底层数据交互:处理 IO 流、Binder 传递大数据、JNI 调用 C/C++ 代码时,必须使用
ByteArray等原生数组。 - 变长参数(vararg) :当函数接收变长参数时,内部实际接收到的是一个数组。可以使用展开操作符
*将数组传给变长参数函数。