Kotlin 面试题库
收录真实面试常见问题,答案长度适合口头表达;拿不准时可返回相关主题复习。
语言核心
6个问题 · 0 已看01 Kotlin 中的 val、var 与不可变对象有什么区别? 查看答案 ▾ 收起 ▴
val 让引用在初始化后不能重新赋值,var 则允许名称改指向另一个值。两者都不决定引用对象本身能否变化;val 完全可以指向仍可增删元素的 MutableList,多个别名也会观察到同一批修改。不可变性来自对象模型和公开 API。我会先用 val 缩小赋值范围,再单独判断调用方需要可变对象、只读视图,还是与原对象隔离的副本。
09 Kotlin 会为数据类生成哪些成员,哪些属性会参与? 查看答案 ▾ 收起 ▴
Kotlin 会根据主构造函数中声明的属性生成 equals、hashCode、toString、按顺序编号的组件函数和 copy。类体属性仍是普通实例状态,但不会进入这些成员,因此副本会重新初始化它们。我把构造函数视为值语义边界;属性移入或移出它,会同时改变比较、哈希、日志、解构和复制。数据类可以显式提供 equals、hashCode 或 toString,但自定义相等性必须保持哈希契约;copy 和生成的组件函数不能手动提供。
13 Kotlin 属性委托必须提供什么契约? 查看答案 ▾ 收起 ▴
只读委托属性需要 operator getValue:接收者是委托,参数描述属性所有者与 KProperty,返回值必须适合属性类型。可变属性还需要 setValue,其 value 参数必须能够接收属性值。实现 ReadOnlyProperty 或 ReadWriteProperty 并非强制,但能明确表达契约。委托依赖特定所有者时,我会写出具体接收者类型;只有真正通用的委托才使用 Any?。调用点的 by 会在编译期解析这些操作符,不会在运行时按名称猜测方法。
14 lazy、observable 与 vetoable 有什么区别? 查看答案 ▾ 收起 ▴
lazy 是只读委托,在第一次访问时计算并缓存首次成功的结果;线程模式控制初始化与发布,初始化失败后会在后续读取时重试。observable 可读写,会在保存新值后调用回调,所以能通知变化,却不能自动回滚。vetoable 在保存前调用谓词,谓词返回 false 时保留旧值。我只在这些时机正好符合 API 意图时使用它们。需要说明原因或原子更新多个字段的验证,应放进显式方法,而不是静默否决。
16 provideDelegate 在 getValue 和 setValue 之外增加了什么? 查看答案 ▾ 收起 ▴
provideDelegate 会拦截所属对象的属性与 by 右侧对象之间的绑定。它在所属对象初始化时运行,返回之后真正接收 getValue 和 setValue 调用的委托。提供者因此可以尽早检查属性名、所有者类型、注解或注册表项,也能为每个属性创建独立状态。我会把检查限制在绑定元数据上,因为此时所属对象可能只完成了部分初始化。运行时的值验证仍应放在 setValue 中;调用方需要结果时,则应使用显式方法。
33 Kotlin 中 T 与 T? 的实际区别是什么? 查看答案 ▾ 收起 ▴
T 排除 null,T? 则在该类型可表示的值中加入 null。T 值可以传给需要 T? 的位置,但 T? 必须经过检查、安全调用、默认处理或拒绝,才能作为 T 使用。这种区别让缺失出现在 API 签名中,也让编译器拒绝未经检查的成员访问。它不会验证领域内容:非空 String 仍可能是空白或格式错误。我会把不确定性留在输入边界,在那里校验,再在缺失已解决后向内部提供非空领域值。
类型系统
2个问题 · 0 已看02 Kotlin 的可空类型与智能转换如何协同工作? 查看答案 ▾ 收起 ▴
String? 这类可空类型允许 null,因此代码处理这种可能之前,Kotlin 会阻止普通成员访问。空值检查、类型检查或提前返回可以在剩余控制流中收窄类型,这种由编译器证明的收窄就是智能转换。检查与使用之间的值必须稳定;可变属性或开放属性可能变化,所以智能转换可能被拒绝。把属性先读取到局部 val,既能固定本次观察到的值,也常能让证明成立。
04 Kotlin 如何处理数字类型推断、转换与除法? 查看答案 ▾ 收起 ▴
无后缀整数文字在范围允许时通常推断为 Int,小数文字默认推断为 Double。Kotlin 不会把 Int 实参隐式扩宽为 Long 或 Double;调用方要使用 toLong()、toDouble() 等转换,使精度与范围选择保持可见。运算符可以处理混合数字操作数,但赋值仍受预期类型约束。两个整数相除会截去小数部分。我会在运算前选择结果类型,测试溢出边界,并明确金额单位和舍入策略。
控制流
4个问题 · 0 已看03 为什么穷尽 when 对枚举和密封层级有用? 查看答案 ▾ 收起 ▴
穷尽 when 会把封闭状态集合中的每个值写明。when 作为表达式时,Kotlin 要求所有可能状态都产生结果。对枚举成员或密封子类型逐项列举并省略宽泛 else,能让编译器发现后续新增状态;尚未定义新行为的决策点都会停止编译。任意文本这类开放输入适合使用 else,但在封闭领域中,else 往往会掩盖遗漏的业务逻辑,把有用的编译错误变成意外的默认行为。
30 Kotlin lambda 中的 return 何时能退出外层函数? 查看答案 ▾ 收起 ▴
只有 lambda 经过可内联路径传递时,裸 return 才能退出包围它的具名函数,例如直接传给内联的 forEach。内联会把 lambda 体放到调用点,因此这叫非局部返回。普通非内联回调不能这样做,crossinline 也会明确禁止。只返回本次 lambda 调用时,我使用 return@label;匿名函数中的无限定 return 则只返回匿名函数自身。提前退出逻辑复杂时,普通循环通常更清楚,也不必依赖内联控制流规则。
44 内联与契约怎样影响 Kotlin 作用域函数块? 查看答案 ▾ 收起 ▴
五个标准作用域函数都是内联函数,并且每次作用域函数调用都会在当前位置执行块一次。它们的契约把这种调用形状提供给编译器分析,但不是业务副作用的运行时防护,也不说明外层函数会被调用几次。内联还允许块中的裸 return 退出包围它的具名函数,这就是非局部返回。只想结束块时,我使用 return@let 等标签;把代码移到其他高阶函数后重新编译,并且没有基准时不作性能结论。
47 为什么针对密封类型的 when 中,else 分支可能有害? 查看答案 ▾ 收起 ▴
宽泛的 else 无需命名所处理的变体就能满足穷尽要求。层级以后增加状态时,旧代码仍会编译,并悄悄把新状态送入回退,即使业务规则本应不同。逐项列出密封变体并省略 else,能把新增子类型变成由编译错误驱动的迁移清单。只有未知实现或取值确实属于边界,例如开放协议时,我才保留 else。编译器证明的是类型覆盖,不是分支正确性,因此我仍会测试每个叶节点的代表性载荷和转换规则。
集合
3个问题 · 0 已看05 为什么 Kotlin 只读集合不一定不可变? 查看答案 ▾ 收起 ▴
List、Set 和 Map 只公开读取操作,并不保证底层对象不会变化。MutableList 可以赋给 List 引用,另一个别名仍能增删元素,List 视图随后也会观察到变化;val 只禁止引用重新赋值。在 API 边界,我会区分实时只读视图与稳定快照。调用方需要快照时,我会复制集合结构,再单独判断可变元素对象是否也需要复制。
08 为什么哈希集合中的键应保持稳定? 查看答案 ▾ 收起 ▴
HashSet 和 HashMap 先根据 hashCode 选择存储桶,再用 equals 确认匹配。对象插入后,如果参与这两个方法的属性发生变化,按新状态查找时就可能进入另一个桶。条目仍占据旧桶,却难以找到或删除。Kotlin 数据类的主构造函数属性默认参与生成的相等性和哈希计算,因此这个问题很容易被忽略。我会使用不可变身份字段或专用键值,并在模型允许修改时测试修改后的成员查找。
11 为什么可变数据类用作 HashMap 键很危险? 查看答案 ▾ 收起 ▴
HashMap 先根据 hashCode 选择存储桶,再使用 equals 确认匹配。数据类生成的相等性和哈希包含主构造函数属性。如果插入后修改了这些属性,或修改了参与哈希的嵌套值,查找可能进入另一个桶,而条目仍留在旧桶中。这个键随后可能难以查找或删除。我会使用只含稳定 val 属性和不可变可达状态的专用键类型。如果标识独立于可变字段,我会显式建模该标识,而不是对整个实体快照做哈希。
集合处理
1个问题 · 0 已看06 什么情况下应使用 Sequence,而不是急切集合操作? 查看答案 ▾ 收起 ▴
当求值语义很重要时,我会选择 Sequence,例如管道能够短路、需要避免物化中间结果,或数据源本来就按需产生值。map、filter 等中间操作是惰性的,first、toList 等终结操作才会启动消费。我不会直接假设它更快,因为惰性调用也有成本,状态型步骤还可能需要完整上游。我还会确认输入在需要时有限、消费期间资源保持打开,并验证数据源是否支持调用方期望的重复遍历。
映射与分组
1个问题 · 0 已看07 associateBy 产生重复键时会怎样,应如何处理? 查看答案 ▾ 收起 ▴
associateBy 每个键只返回一个值,因此后出现的同键元素会替换较早的值。只有在业务明确采用“最后值胜出”策略时,这才安全。若键必须唯一,我会在插入时拒绝重复项,或比较输入与结果大小并报告冲突键。若一个键确实对应多个值,我会使用 groupBy 并返回 Map<K, List
对象建模
5个问题 · 0 已看10 Kotlin 数据类的 copy 是深拷贝吗,如何隔离嵌套状态? 查看答案 ▾ 收起 ▴
copy 是浅拷贝。它会构造新的外层实例,把每个主构造函数参数默认设为当前属性;除非调用方替换参数,否则嵌套引用会原样传递。因此,两个副本可能共享同一个 MutableList 或可变子对象。对于嵌套不可变数据,我会在每个受影响层级显式调用 copy。对于可变对象图,我会先定义所有权,再复制需要隔离的集合结构和可变元素。只读 List 类型只能缩小可用操作,不能证明其他别名无法修改其底层对象。
15 自定义属性委托会怎样意外共享状态? 查看答案 ▾ 收起 ▴
委托若把一个值保存在自身字段中,绑定到同一委托实例的所有属性就会共享该值。因此,把它声明为 object 或放进顶层变量,可能让无关字段和对象互相影响。我通常在每个属性声明中创建有状态委托,让每个“所有者-属性”组合拥有独立实例。若共享是有意设计,委托必须用 thisRef 与 KProperty 等显式键索引状态,并定义生命周期和并发策略。我会交错写入两个对象上的两个属性;只测试一个对象发现不了这种泄漏。
37 封装如何帮助 Kotlin 类维护不变量? 查看答案 ▾ 收起 ▴
封装让类成为合法状态转换的所有者。我会把可变表示保持私有,校验构造输入,并公开 reserve 或 withdraw 这类操作,而不是不受限制的 setter。每个操作检查前置条件,要么留下合法状态,要么在不产生部分修改的情况下失败。公开 val 若引用可变集合仍然不够,因此还要检查别名并按需防御性复制。测试应主动攻击边界:使用非法构造、被拒绝的操作和可变输入,再确认每个可观察状态仍满足不变量。
38 在 Kotlin 中,何时应选择接口而不是抽象类? 查看答案 ▾ 收起 ▴
需要为互不相关的类命名一个共同角色时,我优先使用接口,尤其是一个类可能同时承担多个角色的情况。接口可以声明抽象成员与默认行为,但不持有构造状态或后备字段。多个实现需要共享每个实例的状态、构造规则、受保护辅助函数,或步骤顺序固定的模板时,我会选择抽象类。由于一个类只能有一个类父类型,这项选择会占用更多类型层级。无论选择哪种形式,我都会说明替换契约,而不是只为测试随意开放成员。
40 Kotlin 设计审查中的「组合优于继承」是什么意思? 查看答案 ▾ 收起 ▴
这表示除非领域中确实存在稳定的「是一个」关系,否则我会把所需能力建模为对象「拥有」的协作者,通常通过构造函数注入接口。组合能缩小替换边界,允许各自拥有独立生命周期,也不会继承无关状态或开放成员。继承并非禁止;稳定且可替换的层级,或共享模板协议,仍适合使用它。我会检查每个派生实例在所有基类型调用点是否满足基类契约。若目标只是复用日志、重试或格式化代码,组合辅助对象通常更清楚。
相等性与哈希
1个问题 · 0 已看12 数组属性会怎样影响 Kotlin 数据类的相等性? 查看答案 ▾ 收起 ▴
数据类生成的相等性会委托给每个属性类型,而 Kotlin 数组的 equals 使用引用语义,不会逐元素比较。因此,两个 Packet 分别持有独立分配的 ByteArray 时,即使每个字节都相同,也可能比较为不相等。当属性表示值集合时,我优先使用 List。必须使用数组时,我会同时重写 equals 和 hashCode,配对使用 contentEquals 与 contentHashCode;嵌套数组则使用 contentDeepEquals 与 contentDeepHashCode。我还会用两个独立分配、内容相同的数组测试,避免复用同一引用掩盖错误。
函数与接收者
7个问题 · 0 已看17 带接收者的 lambda 如何支持 Kotlin 内部 DSL? 查看答案 ▾ 收起 ▴
ServerBuilder.() -> Unit 这样的接收者函数类型会把 ServerBuilder 作为 lambda 中的 this,因此块内可以省略显式参数直接调用其成员;再配合尾随 lambda,入口就能写成 server { … }。这里没有新的解析器:入口函数创建构建器,在它上面调用函数值,校验状态,再返回结果。我会缩小接收者的公开表面,因为每个公开成员都会变成块内词汇。最终结果通常应使用独立类型,避免暂时可变状态逸出。
18 @DslMarker 解决什么问题,又不解决什么问题? 查看答案 ▾ 收起 ▴
@DslMarker 控制嵌套 DSL 作用域中的隐式接收者解析。多个接收者类型带有同一标记时,只有最近的接收者能被隐式使用;调用外层接收者需要限定的 this。这样可防止嵌套块悄悄调用结构上不合适的外层操作。该注解不会移除外层对象,也不会执行运行期值校验、清理字符串或让结果不可变。我会让同一逻辑 DSL 的所有接收者类型共享一个标记,并保留负向编译测试,证明非法嵌套确实被拒绝。
21 为什么 Kotlin 扩展函数不具备多态性? 查看答案 ▾ 收起 ▴
扩展调用根据编译期已知的接收者表达式类型、可见声明和实参解析,运行时子类型不会选择更具体的扩展。即使存在 Alert 扩展,只要 Alert 值进入声明为 Message 的参数,message.kind() 就会解析为 Message 扩展。行为必须随运行时子类型变化时,我会使用开放成员或接口操作。我还会通过具体类型、基类类型和接口类型变量分别测试扩展适配器,因为只测具体类型会掩盖这个区别。
22 扩展与真实成员具有相同签名时会发生什么? 查看答案 ▾ 收起 ▴
适用的真实成员优先,扩展不能覆盖或替换它;不同签名仍可作为重载共存。这条规则也影响 API 演进:依赖可能新增一个与调用方现有扩展同签名的成员,源码重新编译后就会选择新成员。我会避免在常用接收者类型上使用宽泛名称,留意遮蔽警告,并在升级依赖时运行调用点行为测试。两个操作都合理时,我会给扩展使用领域明确的名称,而不是依赖微妙的重载解析。
29 Kotlin 的函数类型与预期类型怎样决定 lambda 的形状? 查看答案 ▾ 收起 ▴
函数类型会规定参数类型、结果、可选接收者以及调用能否挂起。赋值目标或接收形参提供预期类型,Kotlin 因而可以推断 lambda 参数类型,并在单参数时使用 it;没有显式带标签返回时,最后一个表达式提供结果。lambda 缺少有效上下文或重载产生多个候选形状时,我会补充显式类型。我还会区分 ((String) -> Int)? 与 (String) -> Int?:前者是可空函数值,后者是返回可空结果的函数。
41 怎样在 let、run、with、apply 与 also 之间选择? 查看答案 ▾ 收起 ▴
我按两个维度选择。第一,let、run 和 with 返回 lambda 结果,apply 与 also 返回上下文对象。第二,run、with 和 apply 把对象作为 this 接收者,let 与 also 则把它作为 lambda 参数传入,默认名是 it。with 是把对象放在括号中的普通函数,其余四个是扩展函数。我会先确定所需输出类型,再考虑写法;集中操作成员时用接收者,涉及多个对象时用具名参数。两种形式都不清楚时,就改用局部变量。
43 嵌套 Kotlin 作用域函数有什么风险?应怎样审查? 查看答案 ▾ 收起 ▴
接收者式块会增加隐式 this,参数式块又常增加隐式 it。出现嵌套后,最近的适用接收者或 lambda 参数可能遮蔽外层对象;若属性同名,赋值甚至会通过编译却落到错误对象。我会先给每个 it 命名并标出静态类型,再给接收者 lambda 加标签,在所有权关键处写 this@label,然后跟踪每次读取、写入和返回值。如果解释代码时需要频繁跨越多层接收者,我会改用局部变量拉平结构,或提取具名辅助函数。
API 设计
5个问题 · 0 已看19 Kotlin 构建器 DSL 应在哪里完成校验和所有权转换? 查看答案 ▾ 收起 ▴
我把 build() 当作未完成的暂时可变装配状态与合法结果之间的边界。它会检查涉及多个字段的约束,把原始值解析成领域类型,并在返回前复制构建器持有的集合。如果只读 List 仍与构建器的 MutableList 互为别名,仅改变公开类型并不够。单字段检查可以提前执行以改善错误位置,但 build 仍须保证结果的全部不变量。我会测试缺失值和边界值,再在构建后修改构建器与原始输入,以证明返回对象已经隔离。
20 分阶段构建器何时优于带可空字段的单个可变构建器? 查看答案 ▾ 收起 ▴
分阶段构建器适合少量、稳定且必须按顺序完成的步骤。每个操作返回只公开下一批合法操作的类型,只有最后阶段能产生结果;遗漏步骤或顺序错误会在编译期失败,不会拖到最后的 !! 或 requireNotNull。我不会为每个可选开关或具体值范围创建阶段,因为类型数量会迅速增长,运行期检查仍不可少。没有重复结构时,构造函数参数更简单;只有当顺序本身属于领域契约时,阶段类型的成本才值得。
24 如何让 Kotlin 扩展 API 保持可预测? 查看答案 ▾ 收起 ▴
我先判断所有权:扩展应靠近证明其合理性的领域规则,而不是放进通用工具包。我使用满足需求的最窄可见性,对容易冲突的名称采用显式导入,并避免在 Any、String 或 List
32 API 何时应使用函数类型、类型别名或 fun interface? 查看答案 ▾ 收起 ▴
API 只需要一种调用形状时,我使用函数类型,例如 (Order) -> Boolean。类型别名可以给这个形状增加领域名称,但不会创建独立类型,仍与底层函数类型赋值兼容。fun interface 会创建名义 SAM 类型,可以包含非抽象成员和专用扩展,也能改善领域或 Java 互操作边界;它还可能需要普通函数类型不需要的转换。我只在确实存在契约或角色时选择接口,不会仅因为 SAM 构造语法能接收 lambda 就使用它。
36 API 何时应返回 T?,而不是结果类型? 查看答案 ▾ 收起 ▴
当缺失是预期结果、只有一种明确含义,而且调用方只需要区分有值与无值时,我返回 T?,例如简单的缓存未命中。调用方必须区分输入无效、未找到、冲突或依赖故障时,我使用密封结果;把这些情况压成 null 会丢失下一步行动。只有缺省与显式提供默认值完全等价时,才适合用 Elvis 回退。调用方错误使用 requireNotNull,对象状态错误使用 checkNotNull;两者都不应代替正常的可恢复分支。
属性与空安全
1个问题 · 0 已看23 扩展属性与可空接收者分别受哪些约束? 查看答案 ▾ 收起 ▴
扩展属性只增加访问器语法,不增加存储,因此不能拥有自己的后备字段。getter 通常应从接收者状态计算便宜且稳定的值;隐藏的 I/O 或全局缓存应改用显式函数或状态所有者。Customer? 这样的可空接收者可以在值为 null 时直接调用,函数体中的 this 仍然可空。在调用点加 ?. 会跳过函数体,可能绕过回退逻辑。我会分别测试直接调用与安全调用,并把 null 行为写进公开契约。
类型建模
3个问题 · 0 已看25 何时应选择内联值类,而不是类型别名或数据类? 查看答案 ▾ 收起 ▴
当一个底层值足以完整描述领域概念,而且我需要独立静态类型时,我会使用内联值类,例如区分 UserId 与 OrderId。类型别名只为现有类型改名;底层类型相同的别名仍然赋值兼容,无法阻止参数对调。值由多个组成部分构成、需要 copy 或解构,或者适合常规对象表示时,数据类更合适。可能采用未装箱表示只是次要收益:我先为类型边界选择值类,再在性能确实重要的位置验证装箱。
27 内联值类会自动强制不变量与不可变性吗? 查看答案 ▾ 收起 ▴
不会。init 块可以拒绝非法底层值,但只要主构造函数仍然公开,伴生对象工厂就不是必经入口。规范化或校验必须统一时,我会限制构造函数可见性,并让所有公开工厂经过同一检查。数据属性上的 val 只阻止重新赋值引用,不会冻结引用背后的 MutableList 或其他可变对象;这类修改可能在键插入映射后改变相等性与哈希。我倾向于使用稳定的不可变底层表示,在边界复制可变输入,并单独测试框架反序列化路径。
45 在 Kotlin 中,怎样选择密封类、密封接口与枚举? 查看答案 ▾ 收起 ▴
领域只是固定的单例常量集合,并且成员共享同一组属性时,我使用枚举。各变体需要不同载荷,同时还要共享构造状态或实现时,我使用密封类。变体载荷不同却无需共享构造,尤其是同一个类要同时属于多个受控分类时,我使用密封接口。三者都支持穷尽 when;决定因素是载荷形状、状态所有权与组合方式,而不是语法偏好。若外部模块必须增加实现,我会改用普通接口。
JVM 表示
1个问题 · 0 已看26 Kotlin 内联值类何时使用装箱表示? 查看答案 ▾ 收起 ▴
在 JVM 上,值以其值类类型直接使用时,编译器倾向于采用底层表示。值作为泛型类型实参、实现的接口、Any,或底层值无法表示的可空值类类型使用时,通常需要生成的包装类。因此,同一个源码值可以在一次调用中未装箱,在下一次调用中装箱。这仍不是堆分配保证,因为后续编译器与 JIT 优化可能消除对象。我会先检查目标产物并分析真实调用形状,再为性能修改 API。
JVM 互操作
2个问题 · 0 已看28 JVM 名称修饰与装箱暴露如何影响 Java 调用 Kotlin 值类? 查看答案 ▾ 收起 ▴
值类参数可能编译成其底层 JVM 类型,因而与直接使用该类型的重载发生冲突。Kotlin 会在方法名后添加稳定哈希来避免冲突,但得到的名称难以或无法从 Java 源码调用。Java 可以传递底层值时,我会发布明确的 @JvmName 桥接,并立即恢复领域类型。Java 必须持有包装对象时,Kotlin 2.4.10 提供实验性的 @JvmExposeBoxed 或 -Xjvm-expose-boxed。我会把生成的构造函数与重载当作 ABI,用 javap 检查,并编译一个 Java 消费方测试。
35 什么是 Kotlin 平台类型,怎样控制它的空值风险? 查看答案 ▾ 收起 ▴
平台类型来自 Java,表示 Kotlin 没有足够信息判断声明的可空性。IDE 可能显示 String!,但这个感叹号不是 Kotlin 源码语法。调用方可以把该值当作可空或非空类型,也能直接调用成员,但 Java 实现仍可能返回 null。我会检查受支持的可空性注解,并在适配器中立即把未注解或不可信的结果赋给显式 T?。适配器随后完成默认、拒绝或领域结果映射,再把非空数据交给核心代码。
状态与并发
1个问题 · 0 已看31 Kotlin lambda 捕获可变状态会带来哪些风险? 查看答案 ▾ 收起 ▴
捕获的 var 属于外层绑定,因此多个回调可能观察并修改同一份状态;复制函数值引用不会克隆闭包。这会造成使用方之间的意外状态串扰,而多线程调用还会产生竞态,因为捕获不提供同步或原子性。长期保存的回调也会让捕获对象保持可达,可能延长请求或服务的生命周期。我会为每个独立所有者创建一次工厂结果,需要时捕获小型不可变快照,为有意共享定义同步策略,并交错调用回调进行测试。
类型分析
1个问题 · 0 已看34 为什么 Kotlin 能在空检查后智能转换局部 val,却可能拒绝转换可变属性? 查看答案 ▾ 收起 ▴
智能转换是一项控制流证明,因此编译器必须知道被检查的值在使用前不会改变。局部 val 通常只有一个稳定值。可变属性可能被其他代码修改,自定义 getter 也可能每次返回不同结果,所以 property != null 不能证明之后再次读取仍非空。我会先把属性读取到局部 val,再检查这个快照。如果操作必须相对于并发变化保持原子性,快照仍然不够;同步应放在状态所有者内部。
继承与分派
1个问题 · 0 已看39 open、override 与 final override 如何控制 Kotlin 的运行时分派? 查看答案 ▾ 收起 ▴
普通类及其成员默认都是 final。基类必须把类和可替换成员标记为 open,派生声明则必须写 override。之后,即使通过基类或接口引用调用,实际对象的运行时类型也会选择对应覆盖。覆盖成员默认仍向下一层子类开放,除非写成 final override;某一层已经完成协议时,我会采用这种形式。我还会避免在构造函数中调用开放成员,因为分派可能在派生属性初始化前进入覆盖实现,读取默认值或意外抛出异常。
可空性
1个问题 · 0 已看42 为什么 let 本身不具备空安全语义?value?.let 保证了什么? 查看答案 ▾ 收起 ▴
let 是可在可空接收者上调用的泛型扩展,因此 value.let 一定执行块,参数类型可能是 T?。value?.let 的条件行为来自安全调用符:值为空时跳过调用,非空时以 T 进入块。整个表达式仍可能为空,原因既可能是接收者为空,也可能是块返回空。若两种原因在业务上含义不同,我会使用显式分支或结果类型,而不是接上一个 Elvis 回退并抹掉差别。
继承
1个问题 · 0 已看46 Kotlin 在密封层级中究竟限制了什么? 查看答案 ▾ 收起 ▴
直接子类型必须是与密封父类型同包同模块的具名声明,不能是局部类或匿名对象。这项限制不会自动关闭后续每一代。保持 final 的直接子类型不能扩展;sealed 子类型会继续受控边界;显式 open 的子类型则可在普通可见性允许的位置拥有间接子类。多平台代码还会增加源集边界。我会检查每个直接子类型的修饰符,因为只说父类型是密封的,会夸大编译器对每个具体后代的了解程度。
泛型
1个问题 · 0 已看48 为什么泛型密封结果类型常使用 out T 与 Nothing? 查看答案 ▾ 收起 ▴
在结果层级中,成功分支产生 T,加载或失败分支通常不产生成功值。把 Outcome 声明为 Outcome
没有符合筛选条件的问题。