快速上手Kotlin - Kotlin基础

21 阅读13分钟

快速上手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 提供了两种方案

特性lateinitby lazy
关键字lateinit varval ... 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 AnyAny?:所有类型的根基类,

  • 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 NothingNothing?:底层的虚无

  • 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 / valvar 可变,val 只读(类似常量)。
根类/动态Any / dynamicAny 是所有类的根类;dynamic 仅用于 JS 互操作。
空安全声明Type?显式区分可空与不可空,编译期拦截风险。
延迟初始化lateinit / by lazylateinit 用于手动赋值,by lazy 用于自动懒加载。
安全调用?.链式调用时保护程序不崩溃。
空合并?:提供默认值,消除 if-else 判空逻辑。
非空断言!!开发者风险自担的强制转换。

3. 逻辑控制流

3.1 条件语句

在 Kotlin 中,ifwhen表达式,即它们可以返回一个值

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)

whiledo-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 最常用的方式。直接跟在类名后面,既定义了参数,也定义了属性

声明即定义

如果在参数前加上 valvar,Kotlin 会自动帮你做三件事:

  1. 声明一个同名的成员变量。
  2. 将参数值赋值给这个变量。
  3. 生成对应的 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关键字声明数据类,数据类会自动生成

  1. equals() / hashCode()(用于比较两个对象内容是否一致)。
  2. toString()(输出属性内容而非内存地址)。
  3. copy()非常重要:用于创建一个属性微调后的新对象)。
data class Project(val id: Int, val title: String)

5.3 单例类

在 Android 开发中,我们经常需要单例(如 NetworkManagerThemeConfig)。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 使用冒号 : 代替了 implementsextends

继承 要求父类必须是 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 针对基本数据类型提供了专门的数组类:IntArrayByteArrayFloatArray 等。

  • 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()

对于追求极致性能和底层确定性的开发者来说,普通的链式调用有一个隐患:每一次操作(如 filtermap)都会在内存中创建一个新的临时集合

如果你的数据量非常大,在操作链前加上 .asSequence() 就可以开启“懒加载”模式。它会把所有的操作符融合成一次迭代,杜绝中间集合的内存分配开销:

val optimizedResult = largeList.asSequence()
    .filter { /* 复杂判断 */ }
    .map { /* 复杂转换 */ }
    .toList() // 只有在最后调用 toList() 时,前面的操作才会真正触发执行

小结:什么时候用 Array?什么时候用 List?

  1. 绝大多数业务场景:无脑使用 ListMutableList。API 丰富,支持扩容,使用安全。
  2. 极致性能场景:当需要在内存中进行大量密集的数值计算时,使用 IntArray / FloatArray 等原生数组,避免 JVM 装箱开销。
  3. 底层数据交互:处理 IO 流、Binder 传递大数据、JNI 调用 C/C++ 代码时,必须使用 ByteArray 等原生数组。
  4. 变长参数(vararg) :当函数接收变长参数时,内部实际接收到的是一个数组。可以使用展开操作符 * 将数组传给变长参数函数。