Kotlin 基础速成
Kotlin 基础速成:变量 / 函数 / 类 / 可见性
目标:用最短路径让读者能读懂并写出 Kotlin 日常代码。
假设读者有 C++ / Go / Java / Python 任一语言基础,本文件只讲 Kotlin 与此不同的地方,相同的地方一句带过。
代码风格约定:所有结论基于 Kotlin 1.9/2.x 正式语法,无推测。
0. 心智模型:先记住这 6 条,再谈语法
Kotlin 的设计目标只有一个:在保留静态类型和性能的前提下,把样板代码砍到最少。
由此派生出 6 条贯穿全语言的规则,后面所有语法都是它们的结果。
| # | 规则 | 含义 | 与 C++/Java 的差异 |
|---|---|---|---|
| 1 | 默认非空 | String 不能装 null,要装必须写成 String? |
Java 默认可空,C++ 引用不可空但裸/智能指针可空 |
| 2 | 默认不可变 | val 优先,var 次之 |
C++ const 是可选的,Kotlin 反过来 |
| 3 | 默认 final | 类和方法默认不能被继承/重写,要开放必须写 open |
Java 默认开放,Kotlin 反过来 |
| 4 | 默认 public | 不写修饰符就是 public |
与 Java 一致;比 C++ class 默认 private 友好 |
| 5 | 类型后置 | name: Type,而非 Type name |
与 Go 一致,Pascal 风格也同样如此 |
| 6 | 无 new、语句即表达式 |
if/when/try 都能返回值 |
C++/Java 需要三元或语句体,代码更啰嗦 |
推论:Kotlin 里你看到的每个 : Type、?、open,都不是冗余语法,而是"打破某个默认值"的显式开关。
读懂 Kotlin 代码的诀窍:看到显式的东西,问自己"它打破了哪个默认值"。
1. 变量:声明与定义
1.1 两类变量:val 和 var
val answer: Int = 42 // val = 只读引用(read-only reference),初始化后不能再赋值
var score: Int = 0 // var = 可变引用(mutable reference),可反复赋值
score = 10 // OK
// answer = 43 // 编译错误:Val cannot be reassigned
关键更正(高频误解):val 不等于"常量值",它等于"只读引用"。
引用本身不可改,但引用指向的对象内容可以改:
val list = mutableListOf(1, 2)
list.add(3) // OK:对象内容可变
// list = otherList // 错误:引用不可变
类比 C++:val 更像 T* const(指针常量),不定相同,指向可变对象。类比 Go:val ≈ slice 变量本身不可换,但底层数组可写。
1.2 类型推断:多数情况下不用写类型
val name = "Kotlin" // 推断为 String
val pi = 3.14 // 推断为 Double
val count = 10 // 推断为 Int
val longVal = 10L // 推断为 Long(加 L 后缀)
val hex = 0xFF // Int(十六进制字面量)
val mask = 0b1010_1100 // Int(二进制 + 下划线分隔符)
val big = 1_000_000 // Int(下划线只是可读分隔符,无语义)
val f = 1.0f // Float(加 f 后缀,否则推断为 Double)
原则(团队共识):
- 局部变量:依赖推断,不写类型(减少噪音)。
- 公开 API(函数返回、属性):显式写类型(推断结果变化不应意外改变对外契约)。
1.3 可空性:Kotlin 最重要的语言特性
类型系统中区分"可空"和"非空",编译器强制你处理 null。
var a: String = "hello"
// a = null // 编译错误:Null can not be a value of a non-null type String
var b: String? = "hello"
b = null // OK
// 四种处理可空值的方式:
val len1 = b?.length // 安全调用:b 为 null 时整条表达式返回 null,类型 Int?
val len2 = b?.length ?: 0 // Elvis 运算符:null 时给默认值 0,类型 Int
val len3 = b!!.length // 非空断言:b 为 null 时抛 NPE(不推荐,等于放弃保护)
val len4 = if (b != null) b.length else 0 // 显式判空:编译器在此做了智能转换(smart cast)
智能转换(smart cast):一旦你在分支里检查了类型或非空,编译器自动把变量当作该类型用,无需手动强转:
if (b != null) {
println(b.length) // 此处 b 被智能转换为 String(非空),不需要 !!
}
// 注意:var 若在检查后被其它代码可能修改(如闭包捕获),智能转换会失效,编译器会报错
何时用 !!:几乎不用。出现 !! 通常意味着设计问题(可以把不可空性前移到函数签名上)。合法场景极少,例如已由外部逻辑保证非空但编译器无法证明(此时加注释说明)。
1.4 常量:const val / lateinit / lazy
| 写法 | 语义 | 约束 | 典型场景 |
|---|---|---|---|
const val X = 1 |
编译期常量,直接内联进字节码 | 只能是顶层 / object / companion object 内;类型限 String 和基本类型 |
协议号、魔法数、版本号 |
val x = compute() |
运行时初始化一次,之后只读 | 任意位置;延迟到首次到达该语句才计算 | 构造期计算一次的派生值 |
lateinit var x: T |
稍后手动初始化 | 只能 var;不可用于可空类型和基本类型(Int/Long/Boolean…);使用前必须已赋值,否则抛 UninitializedPropertyAccessException |
DI 注入、Android onCreate 中赋值、@Autowired |
val x by lazy { ... } |
首次访问时才计算,之后缓存 | 委托给 Lazy<T>,默认线程安全(SYNCHRONIZED) |
开销大的懒加载对象 |
const val MAX_SIZE = 100 // OK:顶层编译期常量
// const val NOW = System.currentTimeMillis() // 错误:值不是常量表达式
class Repo {
lateinit var conn: Connection // 稍后注入
val expensive by lazy { buildIndex() }
}
1.5 基本类型:没有隐式拓宽转换(与 C++/Java 的重要差异)
Kotlin 不做隐式数值转换。这是为了避免 long -> int 这类静默精度丢失。
val i = 10 // Int
val l = 10L // Long
// val wrong: Long = i // 错误:Type mismatch,Int 不会自动变 Long
val right: Long = i.toLong() // OK:显式转换
// 转换函数族(通吃):toByte() toShort() toInt() toLong() toFloat() toDouble() toChar()
// 字符串转换:安全的写法用 xxxOrNull(),失败返回 null 而不是抛异常
val n1 = "123".toInt() // 123
val n2 = "abc".toIntOrNull() // null(推荐,配合 ?: 使用)
1.6 字符串模板
val name = "World"
println("Hello, $name") // 简单变量:$name
println("Length = ${name.length}") // 表达式:${...}
println("${'$'}99") // 输出字面 $:需要转义
1.7 一段对照(可直接记住)
| 需求 | Kotlin | Java 等价 |
|---|---|---|
| 不可变局部变量 | val x = 1 |
final int x = 1 |
| 可变局部变量 | var x = 1 |
int x = 1 |
| 类常量 | const val X = 1(伴生对象内) |
static final int X = 1 |
| 可能为 null | var x: String? = null |
String x = null |
无 new |
val d = Date() |
Date d = new Date() |
2. 函数:声明、参数、返回值
2.1 基本形式
可见性? 修饰符? fun 函数名(参数名: 参数类型, ...): 返回类型 { 函数体 }
^^^^^^^^^^^^ 不能省略时必须写,见下
fun add(a: Int, b: Int): Int {
return a + b
}
返回 Unit(等价于 Java void)时可省略返回类型,此时 return 也可省略:
fun printSum(a: Int, b: Int): Unit { println(a + b) } // 完整写法
fun printSum2(a: Int, b: Int) { println(a + b) } // 惯用写法:省略 Unit
2.2 单表达式函数体(expression body)
函数体只有一个表达式时,用 = 替代花括号,返回类型可推断(公开 API 仍建议显式写):
fun area(r: Double): Double = Math.PI * r * r
fun isEven(n: Int) = n % 2 == 0 // 推断 Boolean
判别建议:单行逻辑用 =,多行逻辑用 {}。这是 Kotlin 社区的统一写法,不是个人偏好。
2.3 参数:默认参数、命名参数、vararg
这三项是 Kotlin 消灭 Java "重载爆炸" 的手段。
fun connect(
host: String,
port: Int = 8080, // 默认参数
timeoutMs: Long = 3_000,
useTls: Boolean = false
) { /* ... */ }
connect("db.local") // 用默认 port/timeout/useTls
connect("db.local", 5432) // 按位置传(跳过后面的)
connect("db.local", useTls = true) // 命名参数:跳过中间的
connect(host = "db.local", port = 5432, timeoutMs = 1000) // 全命名
命名参数规则(重要):一旦某个实参使用了命名形式,它之后的所有实参都必须是命名形式(避免歧义)。
fun f(a: Int = 1, b: Int = 2, c: Int = 3) { println("$a $b $c") }
f(b = 20, a = 10) // OK:命名后可乱序
// f(a = 10, 20) // 错误:混用后位置不明确
可变参数 vararg:
fun sumOf(vararg nums: Int): Int = nums.sum()
sumOf(1, 2, 3) // nums 在函数体内是 IntArray
val arr = intArrayOf(1, 2, 3)
sumOf(*arr) // 展开操作符 *:把数组打散传入
// 注意:vararg 之后仍可定义别的位置参数,但调用时必须用命名参数
2.4 函数可以放在哪儿
| 位置 | 写法 | 说明 |
|---|---|---|
| 顶层(文件直接下辖) | fun top() {} |
不需要类包裹。编译为 文件名Kt.class,JVM 上静态方法。这也是 Kotlin 没有 "工具类" 概念的原因 |
| 类成员(方法) | class A { fun m() {} } |
通过实例调用 |
| 伴生对象 | companion object { fun create() = A() } |
类似静态工厂,见 §3.4 |
| 局部函数 | 写在另一个函数内部 | 只能被外层函数访问,适合提取私有逻辑而不污染类作用域 |
| 扩展函数 | fun String.last(): Char = this[length-1] |
不动源码给已有类型加方法,见下 |
| lambda / 匿名函数 | val f: (Int) -> Int = { it * 2 } |
一等公民,见 §2.5 |
扩展函数(extension function) —— Kotlin 招牌特性之一,Java 不存在:
fun String.isEmail(): Boolean = Regex("^\\S+@\\S+\\.\\S+$").matches(this)
"a@b.com".isEmail() // true —— 看起来像成员方法,实际编译成静态 Util
- 它不会真的修改类,只是语法糖(静态解析),不存在动态分派。
- 若与真实成员方法同名同签名,成员方法优先(不产生冲突告警)。
2.5 传参进阶:把函数作为参数(高阶函数)
函数类型写作 (参数类型列表) -> 返回类型。
// 接收一个函数类型的参数
fun measure(block: () -> Unit): Long {
val t0 = System.nanoTime()
block() // 调用传入的函数
return System.nanoTime() - t0
}
measure { println("work") } // 尾随 lambda:最后一个参数是函数时可移到括号外
// 带参数、带返回值的函数类型
fun <T> Collection<T>.filterBy(predicate: (T) -> Boolean): List<T> {
val out = ArrayList<T>()
for (item in this) if (predicate(item)) out.add(item)
return out
}
listOf(1, 2, 3).filterBy { it > 1 } // it = 单参数隐式名,等价于 { n -> n > 1 }
lambda 规则速记:
- 单参数时可用默认名
it,多参数必须显式命名{ a, b -> ... }。 - 最后一个参数是函数类型 → 可写成
f(a) { ... }或f { ... }(尾随 lambda 语法)。 - lambda 里
return语义与 Java 不同:从外层函数返回。只想从 lambda 返回需用标签return@filterBy。
2.6 其它常见修饰符
| 修饰符 | 作用 | 例子 |
|---|---|---|
inline |
减少 lambda 带来的对象分配开销,常用于集合操作 | inline fun measure(block: () -> Unit) |
tailrec |
尾递归优化为循环,防止栈溢出 | tailrec fun gcd(a: Int, b: Int): Int |
infix |
中缀调用 a to b |
infix fun Int.pow(n: Int),调用 2 pow 3 |
operator |
运算符重载 | operator fun plus(other: Vec): Vec |
suspend |
协程挂起函数 | suspend fun load(): Data |
@JvmOverloads |
给默认参数生成 Java 可见的重载 | 供 Java 调用方使用 |
2.7 返回值细节
Unit:无意义返回值,单元素半类型,可被当作普通值使用(不像 Javavoid那样特殊)。Nothing:永远不返回(抛异常或无限循环)。返回类型标成Nothing有助于编译器做流程分析(如在 Elvis 后的 smart cast)。fun fail(msg: String): Nothing = throw IllegalStateException(msg) val x = value ?: fail("missing") // 之后编译器知道 x 非空- 具名返回值在 tailrec / 多出口函数里建议显式写返回类型,避免推断意外变化。
3. 类:声明与实现
3.1 主构造函数:一行搞定"字段 + 赋值"
class Person(
val name: String, // 主构造函数参数加 val/var → 自动成为属性(字段+getter/setter)
var age: Int, // var → 生成 getter + setter
email: String // 不加 val/var → 仅构造参数,不作为属性
) {
val display = "$name <$email>" // 属性初始化
init { // 初始化块,主构造函数体
require(age >= 0) { "age must be >= 0" }
}
fun greet() = "Hi, I'm $name"
}
val p = Person("Tom", 18, "t@x.com")
p.name // 读(val 只读)
p.age = 19 // 写(var 可写)
// p.email // 错误:未标记为 val/var,不是属性
对照 Java:上面 3 行 ≈ Java 中 25 行(字段 + 构造器 + getter + setter + toString 的一半)。这是 Kotlin 最大的生产力来源。
3.2 次构造函数(secondary constructor)
class User(val id: Long, val name: String) {
constructor(name: String) : this(generateId(), name) // 必须委派主构造函数
}
经验:99% 的场景不需要次构造函数 —— 用默认参数替代(§2.3),这也是官方推荐做法。
3.3 属性:getter / setter / 幕后字段 field
class Temperature(var celsius: Double) {
val fahrenheit: Double
get() = celsius * 9 / 5 + 32 // 自定义 getter,每次访问实时计算,无存储
var label: String = ""
set(value) { // 自定义 setter
field = value.trim() // field = 幕后字段(backing field),不能在 setter 里写 this.label,否则无限递归
}
}
规则:
field只在 getter/setter 内可见,用于访问属性真实存储。- 若 getter/setter 都不使用
field,则该属性无幕后字段,纯的派生值。 - 自定义 getter/setter 时可单独降低可见性:最常见是
private set(见 §4.3)。
3.4 特殊类形态
| 形态 | 声明 | 自动生成 | 用途 |
|---|---|---|---|
data class |
data class Point(val x: Int, val y: Int) |
equals/hashCode/toString/copy()/componentN()(解构) |
DTO、值对象 |
object(单例) |
object Config { val timeout = 30 } |
线程安全的懒加载单例,直接 Config.timeout 访问 |
全局配置、工具类替代 |
companion object |
class A { companion object { fun create() = A() } } |
类级别的"静态"成员,A.create() |
工厂方法、类常量(const val) |
enum class |
enum class Level { DEBUG, INFO } |
name/ordinal/values() |
有限取值集合 |
sealed class |
sealed class Res { ... } |
受限继承:子类必须在同包同模块(Kotlin 1.5+ 同编译单元、同包) | 状态建模,when 可穷尽性检查(不需要 else) |
value class (1.5+) |
@JvmInline value class Id(val raw: Long) |
编译期内联,零包装开销 | 强类型 ID,避免 Long 混淆 |
interface |
interface Repo { fun find(id: Long): String } |
方法可有默认实现 | 抽象契约 |
data class Point(val x: Int, val y: Int)
val p1 = Point(1, 2)
val p2 = p1.copy(y = 3) // 复制并改部分属性,保持不可变习惯
val (px, py) = p1 // 解构(依赖 componentN)
sealed class Result {
data class Ok(val data: String) : Result()
data class Err(val msg: String, val code: Int = 500) : Result()
}
fun handle(r: Result) = when (r) { // 不需要 else,编译器知道已穷尽
is Result.Ok -> r.data
is Result.Err -> "Error ${r.code}: ${r.msg}"
}
3.5 继承:open / override / interface / abstract
open class Shape(open val area: Double) { // 默认 final,必须写 open 才能被继承
open fun draw() = println("shape") // 方法同理,默认 final
}
class Circle(val r: Double) : Shape(Math.PI * r * r) { // : 后面既是继承也是调用父类构造函数
override fun draw() = println("circle r=$r") // override 关键字强制写,不允许遗漏
}
abstract class Animal(val name: String) { // abstract 类默认自带 open,方法同理
abstract fun speak(): String // 无实现,子类必须实现
}
interface Flyable {
val maxAltitude: Double // 接口里可声明抽象属性
fun fly() = println("flying") // 接口方法可有默认实现
}
class Bird : Animal("bird"), Flyable { // 单继承 + 多实现,逗号分隔
override val maxAltitude = 100.0
override fun speak() = "tweet"
}
踩点:
- 子类构造 : 父类必须立刻初始化 → 写法
class Sub : Base(args),即"继承列表里的位置就得调用父构造",没有 Java 的super(...)行。 override修饰的成员默认仍然是 open(保留可覆盖性)。要彻底终止继承,显式写override fun f() = ...后加final override(很少用)。val属性可以被override成var(放宽),反之不行。- Kotlin 单继承 + 多实现,接口冲突时必须显式实现:
class C : A, B { override fun f() { super<A>.f(); super<B>.f() } // 指定调用哪个父的实现 }
3.6 对象表达式 vs object 声明
val listener = object : OnClick { // 匿名内部类(C++ 无对应概念,Java new Interface(){} 的等价物)
override fun onClick() = println("clicked")
}
object Registry { val map = mutableMapOf<String, Int>() } // 具名单例
4. 可见性 / 调用权限
4.1 四个修饰符
| 修饰符 | 顶层声明(文件直属) | 类成员 | 与 Java 差异 |
|---|---|---|---|
public(默认) |
到处可见 | 任何能看到该类的地方可见 | 一致 |
private |
仅当前文件内可见 | 仅当前类内可见 | Java 顶层类 private 不允许内部类那样,Kotlin 允许 file-private |
protected |
不允许(顶层无 protected) | 当前类 + 子类(不含同包其他类) | Java 的 protected 含同包访问权,Kotlin 收窄,更严格更直觉 |
internal |
同一 模块内可见 | 同一模块内可见 | Java 无对应(近似"包私有 + 更大范围"),这是 Kotlin 独有 |
模块(module)的准确定义:一起编译的一套 Kotlin 文件 —— 一个 Gradle Module/Subproject、一个 Maven 项目、或一个 IntelliJ Module、或单次 kotlinc 调用的编译单元。
internal 是模块化封装的主要手段:库作者把实现细节标 internal,对外只暴露 public API。它会被编译进字节码但名字带 mangled 后缀(如 foo$module_name),Java 调用虽然理论可行但会被工具警告。
4.2 完整可见性矩阵
顶层声明:
| 声明 | 同文件 | 同模块 | 其它模块 |
|---|---|---|---|
fun f()(public) |
✅ | ✅ | ✅ |
private fun f() |
✅ | ❌ | ❌ |
internal fun f() |
✅ | ✅ | ❌ |
protected fun f() |
❌ 编译错误 | — | — |
类成员:
| 声明 | 同一类 | 子类(同模块) | 子类(跨模块) | 同模块其它代码 | 跨模块其它代码 |
|---|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ | ✅ |
private |
✅ | ❌ | ❌ | ❌ | ❌ |
protected |
✅ | ✅ | ✅ | ❌ | ❌ |
internal |
✅ | ✅ | ✅(子类也是 internal/public 时可见) | ✅ | ❌ |
4.3 属性的读写权限分离:最常见用法
class Counter {
var count = 0
private set // 外部只读内部可写:这是 Kotlin 中最常见的封装手法
fun inc() { count++ }
}
val c = Counter()
println(c.count) // OK:getter 仍是 public
// c.count = 5 // 错误:Cannot assign to 'count': the setter is private
也可写 UE:
internal val cache: Map<String, String>
get() = internalCache // getter 为 internal,因此属性整体 external 不可见(不写也可以,默认与属性同级)
规则:getter 的可见性必须与属性相同;只有 setter 允许比属性更严格(private set / internal set / protected set)。
4.4 构造函数的可见性
class Singleton private constructor(val data: String) { // 禁止外部 new
companion object {
fun create(data: String) = Singleton(data) // 只能通过工厂方法创建
}
}
4.5 覆盖(override)时的可见性
open class Base {
protected open fun hook() {}
}
class Derived : Base() {
override fun hook() {} // 默认保留 protected,无需重复写
// public override fun hook() {} // 可以放宽为 public,但**不能收窄为 private**(编译错误)
}
规则:子类覆盖时可见性只能不变或放宽,不能收窄。这与 Java 一致(里氏替换原则)。
4.6 internal 在公开 API 中的坑(真实项目高频编译错误)
internal class Impl
// class Api { fun get(): Impl = Impl() } // 编译错误:public function exposes its internal return type
规则:public API 的签名(参数、返回值、泛型实参、父类)不能包含 internal 或 private 类型。函数体内部使用则没有限制。
4.7 对照速记
| 场景 | Kotlin | Java | C++ |
|---|---|---|---|
| 文件内私有的辅助函数 | private fun helper()(文件顶层) |
只能塞进某个类并标 private static |
static 于 .cpp 的匿名 namespace |
| 只给库内部用 | internal class Impl |
无直接对应(只能放同一个 package 并依赖封包约定的包私有) | 不 export 的符号 / internal namespace |
| 子类可见但外部不 | protected open fun hook() |
protected void hook()(同包也可访问) |
protected: |
| 只读暴露给外部 | var x: T; private set |
private T x; public T getX() |
private: T x; public: T getX() |
5. 常见编译错误 → 原因对照
| 报错信息(关键片段) | 真实原因 | 修复 |
|---|---|---|
Val cannot be reassigned |
给 val 重新赋值 |
改用 var,或本就该是不可变 |
Null can not be a value of a non-null type |
把 null / 可空值赋给非空类型 | 类型加 ?,或用 ?. ?: 处理 |
Only safe (?.) or non-null asserted (!!.) calls are allowed |
直接访问可空对象的成员 | 用 ?.;确认非空用 checkNotNull(x) 替代 !! |
Smart cast is impossible |
编译器无法证明 var 非空(如被闭包捕获 / 是 open 属性) | 复制到局部 val a = x ?: return 后再用 |
Type mismatch(Int/Long) |
无隐式数值转换 | 显式 .toLong() 等 |
'a' is final, so it cannot be overridden |
父类成员未加 open |
给父类成员/类加 open(或改成 abstract/interface) |
'a' hides member of supertype and needs 'override' |
子类忘了写 override |
加 override |
public function exposes its internal parameter/return type |
public 签名引用了 internal 类型 |
把 API 改 internal,或把返回类型抽成 public 接口 |
Cannot assign to 'x': the setter is private |
外部写 private set 的属性 |
通过类方法修改 |
Modifier 'protected' is not applicable to 'top-level' |
顶层写了 protected | 顶层只允许 public/private/internal |
lateinit property has not been initialized |
用了未初始化的 lateinit var |
检查初始化时机;或用 ::prop.isInitialized |
6. 自测题(共 12 题,覆盖本文全部要点)
答案见 Answers.md。先做题再看答案,能立刻定位薄弱点。
val list = mutableListOf(1)之后list.add(2)合法吗?list = mutableListOf(3)合法吗?为什么?- 把
Int变量赋给Long类型,为什么不能直接赋值?正确写法? "12a".toIntOrNull()返回什么?相比toInt()的好处?- 写出
fun connect(host: String, port: Int = 80, useTls: Boolean = false)三种调用方式:只传 host;传 host+useTls=true;全命名传参。 - 命名参数的"混用规则"是什么?
vararg传数组给函数时要用到哪个符号?Unit和Nothing分别代表什么?各自典型场景?class User(name: String, val id: Long)中name能被u.name访问吗?- 主构造函数里能做参数校验吗?写在哪?
- 自定义 setter 里访问真实存储用什么关键字?写
this.x = v会怎样? - 为什么 Kotlin 类默认是 final?想开放继承怎么做?
- 顶层
protected允许吗?internal是什么范围?private set解决了什么问题?
7. 30 秒速查卡
变量: val 只读(var? 可空) var 可变 const val 编译期常量 lateinit var 延迟 var
类型: x: T (后置) T? 可空 T!!不太推荐 as? 安全强转
函数: fun f(a: Int, b: Int = 1): R { return ... } Unit 可省略返回类型
fun f(a: Int): R = expr 单表达式体
f(b = 2) / vararg nums: Int / *arr 命名 / 可变参 / 展开
类: class A(val x: Int, y: String) { init {} 主构造函数 + init
var v: Int = 0; private set 读写权限分离
val computed: Int get() = x * 2 自定义 getter
} data/object/companion/enum/sealed/value
继承: open class / override fun / abstract class / interface / : 父构造直接调用
可见性:public(默认) private(文件或类内) protected(类+子类) internal(同模块)
附录:怎么跑起来
# 方式 A:安装 kotlinc(Chocolatey / brew install kotlin / sdk install kotlin)
kotlinc KotlinBasics.kt -include-runtime -d app.jar
java -jar app.jar
# 方式 B:Kotlin 脚本(Kotlin 2.x 支持直接 .kts / 部分版本支持 .kt 直接运行)
kotlin KotlinBasics.kt
# 方式 C(零安装):https://play.kotlinlang.org —— 粘贴代码即可在线运行
# 方式 D(推荐长期):IntelliJ IDEA 新建 Kotlin/JVM 项目,直接右键 Run

浙公网安备 33010602011771号