开始
基础语法
- val和var
- 自动类型推断
- 嵌入变量和嵌入表达式(语法糖)
- 从根源避免空指针崩溃,通过:
(1) 用Int?明确标记 “可能为 null” 的变量;
(2)强制要求使用前必须检查 null(否则编译报错);
(3)检查后自动转换为非空类型,安全使用。
基础
基本类型
- Kotlin 的Char(字符,比如'A')不是数字,不能像 Java 那样直接当数字用(比如'A' + 1在 Kotlin 里不允许,Java 里会当成 65+1=66)。需要显示转换。toInt()
- kotlin不支持八进制(比如 Java 的012在 Kotlin 里无效)
- 可以用_把数字拆开,更易读
- 数字的 “表示方式”:原生类型 vs 装箱
Kotlin 的数字在 JVM 上的存储分两种情况(和 Java 类似,但更智能):
原生类型:普通变量(如Int、Long)会存在 JVM 的 “原生类型”(类似 Java 的int、long),效率高。
装箱类型:如果是 “可空数字”(如Int?、Long?)或用在泛型里(如List<Int>),会被 “装箱” 成对象(类似 Java 的Integer、Long)。
关键区别:装箱后 “同一性” 和 “相等性” 的不同
===:检查 “是否是同一个对象”(地址相同)
==:检查 “值是否相等”
例子:
val a: Int = 10000 // 原生类型(非对象)
println(a === a) // true(自己和自己当然是同一个)
val boxedA: Int? = a // 装箱成对象
val anotherBoxedA: Int? = a // 又一个装箱对象
println(boxedA === anotherBoxedA) // false(两个不同的对象,地址不同)
println(boxedA == anotherBoxedA) // true(值都是10000,相等)
- 较小的类型不能隐式转换为较大的类型
- 子类型数组不能赋值给父类型数组,这是为了避免运行时错误。数组是“不型变“的
val strArray: Array<String> = arrayOf("a", "b")
val anyArray: Array<Any> = strArray // 错误!Kotlin不允许
// 为什么?假设允许,会出问题:
anyArray[0] = 123 // 想给Any数组放个Int,看似合法
// 但strArray和anyArray指向同一个数组,此时strArray[0]变成了Int,
// 而strArray声明的是String类型,运行时会崩溃!
- Array类存储的是 “对象”(比如Array<Int>存储的是Int?装箱对象),会有性能损耗。如果需要存储原生类型(如int、byte),Kotlin 提供了专门的数组类,比如IntArray(),byteArrayOf()等
- 原生数组直接存储底层数据(如int),不需要装箱成Integer对象,运算和访问速度更快,适合大量数值处理(如游戏、数据分析)。
- 两种字符串字面值:转义字符串 vs 原生字符串
- Kotlin 中的if不仅仅是 “条件判断语句”,更是 “表达式”—— 它能返回一个值,这是和 Java 中if最大的区别,也让代码更简洁。
- 在 Kotlin 中任何表达式都可以用标签( label )来标记。 标签的格式为标识符后跟 @ 符号,例如: abc@ 、 fooBar@ 都是有效的标签。要为一个表达式加标签,我们只要在其前加标签即可。
// 用loop@标记外层循环
loop@ for (i in 1..3) {
for (j in 1..3) {
if (i == 2) {
// 当i=2时,跳过loop@标记的外层循环的当前迭代(即i=2的整个循环)
continue@loop
}
println("i=$i, j=$j")
}
}
- 在多层循环中,普通的break和continue只能作用于 “最近的一层循环”,如果需要操作外层循环,就必须用标签。标签让循环控制更精确,避免了用 “标志位变量”(如isFound = true)来间接控制外层循环的繁琐写法。
- 标签处返回 核心作用 & 为什么不能用break和continue
https://odocs.myoas.com/docs/47kgJ0DYOnSbKeqV
类和对象
类和继承
- 所有类都隐式继承自Any(即使不写extends Any),它是最顶层的超类。Any和 Java 的Object不同:Any只有 3 个成员函数:equals()、hashCode()、toString(),没有其他方法(比如getClass()、wait()等,这些需要通过 Java 互操作获取)
- “避免无意识的继承”“显式优于隐式”
- 只要一个类包含抽象属性或抽象方法,那么这个类就必须被声明为抽象类(用 abstract 修饰),否则会导致编译错误。
抽象属性 / 方法的核心特点是 “只声明、不实现”,需要子类去实现具体逻辑。如果一个类包含这样的未实现成员,说明这个类本身是 “不完整的”,无法直接实例化(因为实例化后调用抽象成员会无具体逻辑可执行)。
- 多态:实现条件:
基于继承(或接口实现);
子类重写父类的方法;
父类引用指向子类对象(父类类型 变量名 = 子类对象)
- animal1 和 animal2 都是 Animal 类型(父类引用),但实际指向的是 Cat 和 Dog 对象。调用 eat() 时,程序会自动根据对象的实际类型执行对应的重写方法(这称为 “动态绑定”),表现出不同行为 —— 这就是多态。
延迟初始化
类中存在很多全局变量实例,为了保证它们能够满足Kotlin的空指针检查语法标准,你不得不做许多的非空判断保护才行,即使你非常确定它们不会为空。解决办法就是对全局变量进行延迟初始化。
- lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null了。
- 一个全局变量使用了lateinit关键字时,请一定要确保它在被任何地方调用之前已经完成了初始化工作,否则Kotlin将无法保证程序的安全性。
- ::adapter.isInitialized可用于判断adapter变量是否已经初始化。
密封类
密封类(Sealed Class) 是一种特殊的类,它的核心作用是限制类的继承结构,确保所有子类的类型在编译期就是已知的、有限的。
这种特性让密封类非常适合表示 “有限的状态集合” 或 “固定的类型分支”,比如网络请求的状态(成功 / 失败 / 加载中)、UI 的不同状态(正常 / 空数据 / 错误)等。(意思是告诉程序所有的情况我都能考虑到,不用再"else"了)
- 密封类本身是抽象的
- 密封类的子类必须在同一个文件中定义(或同一包内,Kotlin 1.5+),外部无法新增子类。这保证了密封类的 “封闭性”—— 所有可能的子类型在编译期都是已知的。
- 增强 when 表达式的安全性
当用 when 表达式处理密封类时,由于所有子类都是已知的,不需要写 else 分支,编译器会自动检查是否覆盖了所有可能的子类型。如果遗漏了某个子类,编译器会报错,避免逻辑漏洞。
扩展函数
- 扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。
fun ClassName.methodName(param1: Int, param2: Int): Int {
return 0
}
- 最好将扩展函数定义成顶层方法,这样可以让扩展函数拥有全局的访问域。
fun String.lettersCount(): Int {
var count = 0
for (char in this) {
if (char.isLetter()) {
count++
}
}
return count
}
- 看上去就好像是String类中自带了lettersCount()方法一样。
- 扩展函数在很多情况下可以让API变得更加简洁、丰富,更加面向对象。我们再次以String类为例,这是一个final类,任何一个类都不可以继承它,也就是说它的API只有固定的那些而已,至少在Java中就是如此。然而到Kotlin中就不一样了,我们可以向String类中扩展任何函数,使它的API变得更加丰富。
运算符重载
- operator关键字
class Obj {
operator fun plus(obj: Obj): Obj {
// 处理相加的逻辑
}
}
- 上述代码就表示一个Obj对象可以与另一个Obj对象相加,最终返回一个新的Obj对象。
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)
}//多重重载
}