Kotlin 的基础语法看起来简洁,但真正上手时,很多人会卡在几个细节上:val 和 var 到底怎么选,为什么类型不能隐式转换,for 区间写法该怎么理解,以及顶层函数、扩展函数在 Java 层究竟会变成什么。本文按这些常见问题重新梳理一遍核心语法,并保留关键代码示例,方便你直接对照理解 Kotlin 的实际行为。
读完后,你可以更清楚地判断哪些语法是 Kotlin 的“语法糖”,哪些属于编译期规则,哪些又会影响到与 Java 的互操作方式。这些判断,比单纯记住写法更重要。
变量声明:先理解 val、var 和初始化规则
Kotlin 声明变量主要有两种方式:val 和 var。其中,val 用于声明不可变变量,var 用于声明可变变量。实际开发中,Kotlin 更鼓励优先使用 val,只有在确实需要重新赋值时才改用 var。

这里需要注意一个容易被误解的点:val 限制的是“变量引用不能再次指向别处”,并不代表它指向的对象本身一定不可变,这一点与大多数高级编程语言的行为类似。
Kotlin 还提供了较强的类型推断能力,因此声明变量时,通常要么直接给初始值,要么显式写出类型。
val a = 1
1. val b: Int
b = 2
2. var c = 3
c = 4
3.
class cla {
val d: Int
init {
d = 5
}
}
编译期初始化检查与 lateinit
如果你声明了类型但没有立刻赋值,Kotlin 会要求该变量在使用前完成初始化,这个检查发生在编译期。
但如果使用 lateinit 关键字声明变量,情况就不同了。它只能用于可变变量,也就是只能修饰 var。此时是否已初始化不再由编译器静态保证,而是推迟到运行时检查。这类写法常见于依赖注入等场景。
另外,成员变量不能像局部变量那样随意先声明再拖到对象创建后赋值。成员属性必须在对象构造完成前初始化,因此常见做法是先声明类型,再在 init 代码块里完成赋值,上面的 d 就属于这种情况。
类型转换:Kotlin 不做隐式转换
Kotlin 的基本数据类型都有类包装能力,因此字面量可以直接调用函数。但它和 Java 一个很大的区别在于:Kotlin 不允许隐式类型转换。
也就是说,Int 不会自动变成 Long。如果要转换,就必须显式调用对应的 toXxx() 方法。不过,字面量本身可以直接调用这些转换函数。
var a: Int = 1
1. var b: Long = a /* x */
2. var b: Long = a.toLong() /* √ */
3. var b: Long = 1.toLong() /* √ */
这条规则的好处是,数值类型变化更明确,阅读和维护时不容易漏掉潜在的精度或范围问题。
控制流程怎么写:循环、区间、if 与 when
Kotlin 的 while 与 Java 基本一致,但 for 更强调“遍历”语义,采用的是 for-in 形式。也就是说,in 前面的变量会依次接收集合或区间中的元素。

在区间表达上,..、until 和 downTo 是最常见的三种写法:
..:生成全闭区间until:生成左闭右开区间downTo:生成倒序的闭区间
步长默认为 1,也可以在表达式末尾用 step 指定。
while (expression) { ... }
val arr = IntArray(5) /* 创建长度为5的Int数组 */
1. for (num in arr) { ... }
2. for (i in 0..arr.size - 1) { ... }
3. for (i in 0..arr.size - 1 step 2) { ... }
4. for (i in 0 until arr.size) { ... }
5. for (i in arr.size - 1 downTo 0) { ... }
if 和 when 不只是语句,也是表达式
Kotlin 里,if 和对标 Java switch 的 when 都不只是流程控制语句,它们本身还能返回值。返回的是命中分支中最后一行表达式的结果。
这让很多简单判断可以直接写成函数表达式体,代码会更紧凑。
fun max(a: Int, b: Int): Int = if (a > b) a else b
fun is10(c: Int): Boolean = when (c) {
10 -> true
else -> fal
}
从写法上看,这种设计减少了中间变量,也让“条件判断的结果是什么”表达得更直接。
数组怎么区分:Array 和 IntArray 有什么不同
Kotlin 中常见的数组有两类:Array 和 IntArray。它们分别对应 Java 层的 Integer[] 和 int[]。
这不是单纯命名不同,而是底层表示也不同。因此两者互转时,底层逻辑是“拷贝”,并不是零成本视图转换。转换时分别调用 toTypedArray() 和 toIntArray()。
val a = arrayOf(1, 2, 3)
val b = IntArray(5)
val c: Array<Int> = c.toTypedArray()
val d: IntArray = a.toIntArray()
如果你的代码需要频繁在 Kotlin 和 Java 之间传递数组,理解这层差异就很重要,因为它会直接影响性能和 API 选择。
函数不一定属于类:顶层函数与顶层属性
Kotlin 一个很有代表性的设计,就是允许函数直接定义在文件顶层,而不必强行放进某个类里。典型例子就是 main 函数。

这种设计很适合工具方法、程序入口以及不依赖对象状态的逻辑。虽然 Kotlin 源码里看不到“工具类”,但在 Java 层,这些顶层函数仍会被包装进自动生成的类中,并以静态方法的形式出现。
fun main() {
val name = "Kotlin"
println("Hello, " + name + "!")
for (i in 1..5) {
println("i = $i")
}
}
顶层函数是以 kt 文件为单位独立存在的。通常情况下,Java 层生成的类名会使用文件名;如果有需要,也可以通过注解自定义这个名字。
@file:JvmName("MathUtils")
fun add(a: Int, b: Int) = a + b
/* -> */
public final class MathUtils {
/* 没有构造函数 */
public static int add(int a, int b) {
return a + b;
}
}
不过,这里有一个实际开发中的风险点:如果不同文件生成了相同的 Java 类名,就可能出现命名冲突。
顶层属性在 Java 层如何表现
顶层属性的概念与顶层函数类似。它们同样会落到自动生成类中,通常会生成 private static 字段,以及对应的 get/set 方法;如果属性是 val,则没有 set。
这些属性会在对应文件生成类第一次被加载时初始化。
如果你希望某个属性在 Java 中直接表现为 public static final,可以使用 const。但它只能修饰基本数据类型和 String,并且属性本身必须是 val,同时要在声明时完成初始化。
/* God.kt */
val name = "洪秀全"
const val rank = 2
/* -> */
public final class GodKt {
private static final String name = "洪秀全";
public static getName() {
return name;
}
public static final int rank = 2;
}
扩展函数与扩展属性:语法像成员,本质不是改类
很多人会觉得 Kotlin 的标准库 API 比 Java 更“顺手”,其中一个核心原因就是扩展函数。它看起来像给某个类新增了成员方法,但实际上并没有修改原类定义。
从 Kotlin 语法上看,扩展函数写在类外部;从 Java 层看,它本质上仍是静态方法,只不过接收者对象会作为参数传入。
/* StringUtils.kt */
fun String.lastChar(): Char = this.get(this.length - 1)
/* -> */
public final class StringUtilsKt {
public static char lastChar(String s) {
return s.charAt(s.length() - 1);
}
}
/* --- */
class SomeClass {
fun String.lastChar(): Char = this.get(this.length - 1)
}
/* -> */
public final class SomeClass {
public static char lastChar(SomeClass sc, String s) {
return s.charAt(s.length() - 1);
}
}
扩展函数又可以分为顶层扩展函数和成员扩展函数。两者在 Java 层都还是静态方法,但后者只能在所属类实例的上下文里使用,作用范围更受限制。
这一节也能顺带看出 Kotlin 文件的工程意义:与 Java 必须让文件名对应 public 顶级类不同,Kotlin 文件可以同时包含多个类、多个顶层函数和顶层属性。
扩展属性为什么不能直接存值
扩展属性可以理解为“以属性形式包装的扩展函数返回值”。由于它并没有真的往原类里新增字段,所以扩展属性本身不存在可存储的 backing field。
这意味着它不能直接初始化,也不会自动拥有默认的 get 实现,通常需要自己定义访问器。
val String.lastChar: Char
get() = get(length - 1)
/* -> */
public final class SomeClass {
public static char getLastChar(String s) {
return s.charAt(s.length() - 1);
}
}
可变参数与中缀调用:mapOf 背后做了什么
Kotlin 支持用 vararg 声明可变参数,这一点和 Java 类似,但使用上有一个关键差别:Java 的可变参数函数通常可以直接传数组,而 Kotlin 需要显式展开数组,写法是使用展开运算符 *。
这个特性在 mapOf、listOf 等标准库函数里非常常见。
/* mapOf的函数声明 */
1. public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>
2. val map = mapOf(1 to "one", 2 to "two", 3 to "three")
3. 1.to("one")
4. infix fun Any.to(other: Any) = Pair(this, other)
5.
fun main(args: Array<String>) {
val list = listOf("args: ", *args)
/* 相当于用"args: "和args的各参数建立集合 */
}
上面这段代码还涉及另一个 Kotlin 很有辨识度的语法:中缀调用。像 1 to "one" 这种写法,本质上是函数调用的简化形式,对应普通写法就是 1.to("one")。
要声明一个中缀函数,需要满足“只有一个参数”,并使用 infix 修饰。标准库里的 to 正是一个扩展函数,因此它可以方便地创建任意类型的 Pair。













