一、语法修饰符修饰的特殊函数(语言特性,Java不存在)
1. 扩展函数 / 扩展属性 fun T.xxx()
定义
不修改原有类源码,为类新增函数;本质是静态工具方法,第一个参数为目标对象。
fun String.isValidPhone(): Boolean {
// this = 调用者对象
return this.matches(Regex("^1[3-9]\\d{9}$"))
}
// 调用
"13800000000".isValidPhone()
底层字节码等效Java
public static boolean isValidPhone(String $this) {}
⚠️核心陷阱(面试高频)
- 静态分发:扩展函数无法重写。变量声明类型决定调用哪个扩展函数,不是实际对象类型
open class Animal
class Cat : Animal()
fun Animal.say() = "animal"
fun Cat.say() = "cat"
val a: Animal = Cat()
println(a.say()) // 输出 animal!不会动态分派
- 若成员函数与扩展函数同名、同参数,成员函数优先级永远更高,扩展函数被隐藏。
- 扩展函数不能访问类
private/protected成员。 - 可定义可空类型扩展函数
fun String?.test(){},安全调用空对象。
2. infix 中缀函数
使用条件(全部满足)
- 成员函数 / 扩展函数
- 仅有一个参数
- 函数标记
infix
语法:省略 . 和 ()
infix fun Int.pow(n: Int): Int {
var res = 1
repeat(n) { res *= this }
return res
}
fun main(){
val num = 2 pow 3 // 等价 2.pow(3)
}
标准库经典:to 创建 Pair
val map = mapOf(1 to "A") // infix fun <A,B> A.to(that:B):Pair<A,B>
限制
不能支持具名参数、可变参数;参数不能带默认值。
3. tailrec 尾递归函数
普通递归问题
每一次递归调用压入栈帧,深度过大 → StackOverflowError
尾递归要求:
递归调用必须是函数最后一条有效执行语句(尾部调用),不能参与运算
✅正确:return fact(n-1, n*acc)
❌错误:return n * fact(n-1)(递归结果还要参与乘法,不是尾调用)
tailrec fun factorial(n: Long, acc: Long = 1): Long {
if(n <= 1) return acc
return factorial(n - 1, n * acc) // 尾部调用
}
底层
编译器直接将递归优化为while循环,不创建栈帧。
⚠️ 如果不满足尾递归条件,编译器不会报错,仅仅不会优化。
4. inline / noinline / crossinline 内联函数
4.1 inline
普通高阶函数:传递Lambda会生成匿名内部类,频繁调用产生对象开销。
inline:编译期把函数体 + Lambda代码直接拷贝到调用处,消除函数对象创建。
inline fun measureTime(block: ()->Unit): Long{
val start = System.currentTimeMillis()
block()
return System.currentTimeMillis() - start
}
关键特性:非局部返回
内联Lambda里可以直接 return,跳出外层函数(普通Lambda只能return@标签)
inline fun test(block: ()->Unit){ block() }
fun demo(){
test {
return // 直接退出demo(),非局部返回
}
}
4.2 noinline
一个inline函数接收多个lambda,只想部分Lambda不内联,标记 noinline。
适用场景:需要把lambda作为参数继续传递、存入变量(内联代码没有对象,无法传递)
inline fun run(a:()->Unit, noinline b:()->Unit){
a()
val func: ()->Unit = b // noinline lambda可以赋值给变量
}
4.3 crossinline
禁止Lambda中非局部return,强制只能带标签返回。
场景:Lambda不在当前函数直接执行(交给另一个线程、异步执行),非局部return会造成逻辑异常。
inline fun asyncRun(crossinline block: ()->Unit) {
Thread {
block()
}.start()
}
// 调用处
asyncRun {
// return // 编译报错!禁止非局部返回
return@asyncRun // 只能标签返回
}
5. suspend 挂起函数(协程核心特殊函数)
suspend fun requestData(): String {
delay(1000) // 挂起函数,不阻塞线程
return "result"
}
硬性规则
- 只能在 suspend函数 / 协程作用域内调用,普通函数无法直接调用;
- 本质:编译器自动增加隐藏参数
Continuation<T>,没有任何线程魔法; - 挂起 ≠ 阻塞:线程释放,可以去执行其他任务;恢复时不一定在原线程。
陷阱
suspend 仅仅是调用限制标记,不会自动切换线程;切换线程依靠 withContext。
二、运算符重载函数(operator 特殊函数)
必须加 operator 关键字,固定函数名,允许使用运算符语法调用。
| 函数签名 | 运算符 | 说明 |
|---|---|---|
operator fun plus(other:T) |
+ |
加 |
operator fun minus(other:T) |
- |
减 |
operator fun times(other:T) |
* |
乘 |
operator fun div(other:T) |
/ |
除 |
operator fun rem(other:T) |
% |
取余 |
operator fun unaryMinus() |
-obj |
负号一元运算 |
operator fun unaryPlus() |
+obj |
正号一元运算 |
operator fun not() |
!obj |
取反 |
operator fun get(i:Int) |
obj[i] |
下标读取 |
operator fun set(i:Int,v:T) |
obj[i]=v |
下标赋值 |
operator fun invoke() |
obj() |
对象当作函数调用 |
operator fun contains(other:T) |
x in obj |
in 判断 |
operator fun compareTo(other:T) |
> < >= <= |
大小比较 |
invoke 示例(超级常用)
class Logger {
operator fun invoke(msg: String) {
println("log:$msg")
}
}
val log = Logger()
log("测试消息") //等价 log.invoke("测试消息")
面试经常问:
invoke使用场景:函数式对象、封装请求、点击代理。
三、标准库五大作用域函数(let/run/apply/also/with)【重中之重】
底层全部基于带接收者的函数字面量 T.() -> R
| 函数 | 接收者引用 | 返回值 | 函数签名 | 核心场景 |
|---|---|---|---|---|
T.let(block: (T)->R) |
it(参数) |
lambda返回结果 | 扩展函数 | 空安全 ?.let{},对象转换 |
T.run(block:T.()->R) |
this |
lambda返回结果 | 扩展函数 | 计算逻辑、临时作用域 |
T.apply(block:T.()->Unit) |
this |
自身this | 扩展函数 | 对象初始化、配置属性 |
T.also(block:(T)->Unit) |
it |
自身this | 扩展函数 | 日志打印、附加操作、不破坏链式调用 |
with(obj:T, block:T.()->R) |
this |
lambda返回结果 | 普通函数(非扩展) | 大量连续调用同一对象方法 |
完整示例对比
data class User(var name: String, var age: Int)
fun main(){
val origin = User("张三",18)
// apply:配置对象,返回自身
val user1 = origin.apply { age = 20 }
// also:附加操作,返回自身
val user2 = origin.also { println(it.name) }
// let:转换对象,返回lambda结果
val userName:String = origin.let { it.name }
// run:作用域计算
val info = origin.run { "$name,$age" }
// with
val desc = with(origin){ "$name | $age" }
}
选型口诀
- 初始化构建对象 →
apply - 额外打印/埋点,不想打断链式 →
also - 空安全 + 对象转换 →
let - 无需对象、仅需要局部作用域 → 独立
run{} - 大量调用同一个对象,不想重复写变量名 →
with
配套衍生函数
takeIf / takeUnless
// takeIf:条件true返回自身,false返回null
val str:String? = "hello".takeIf { it.isNotEmpty() }
// takeUnless:条件false返回自身,true返回null
val str2:String? = "".takeUnless { it.isEmpty() }
典型搭配:obj.takeIf{}.let{}
四、Data Class 编译器自动生成特殊函数
定义 data class,编译器自动生成5套函数
data class Book(val id:Int, val name:String)
-
component1()、component2()...解构声明使用
val (id,name) = Book(1,"Kotlin")
// 等价
val id = book.component1()
val name = book.component2()
-
equals():按所有属性值比较 -
hashCode():基于所有属性生成哈希 -
toString():格式化输出所有属性 -
copy():复制对象,选择性修改属性
val b1 = Book(1,"A")
val b2 = b1.copy(name = "B")
⚠️限制
- 主构造函数至少要有一个参数;
- 自动生成函数仅使用主构造参数,类内部额外定义属性不参与 equals/hashCode;
五、其他编译器生成特殊函数
1. 枚举类自动函数
enum class Color{ RED,BLUE }
自动生成:
-
values():返回所有枚举数组 -
valueOf(String):根据名字获取枚举,找不到抛异常
2. 匿名函数 & 带接收者函数字面量
普通Lambda:(T)->R
带接收者Lambda:T.()->R(作用域函数底层基础)
// 带接收者lambda
val block: StringBuilder.() -> Unit = {
append("test")
}
StringBuilder().block()
3. 工厂函数(约定俗称特殊函数)
companion object 中定义静态创建方法,命名一般 create / newInstance,替代构造函数
class User private constructor(val name: String){
companion object{
fun create(name:String):User = User(name)
}
}
优势:私有构造、方法名语义清晰、支持重载多个创建规则。
六、易混淆特殊函数面试深度问答(背诵版)
Q1: apply 和 also 区别?
- apply:引用
this,适合操作对象内部属性; - also:引用
it,适合读取对象、外部操作(打印日志)。
Q2: let 和 run 区别?
let 使用 it,适合可空对象 ?.let;run 使用 this,适合直接访问对象成员。
Q3: inline 非局部返回原理?
代码直接展开到调用处,return作用域是外层函数;普通lambda是对象,return只能返回lambda。
Q4: 扩展函数为什么不支持多态?
静态解析,编译期根据变量声明类型绑定方法,不依赖运行时实际类型。
Q5: suspend函数底层原理?
每个挂起函数隐式传入 Continuation,挂起点被编译器切分为多个状态机,不需要额外线程。
七、补充边界场景
- 顶层函数:直接写在文件内,不属于任何类,编译为静态方法;
-
main()特殊入口函数,Kotlin允许无参数顶层main; -
operator fun equals():只重载==;===引用比较不能重载。