希望帮你在Kotlin进阶路上少走弯路,在技术上稳步提升。当然,由于个人知识储备有限,笔记中难免存在疏漏或表述不当的地方,也非常欢迎大家提出宝贵意见,一起交流进步。 ------ Android_小雨
整体目录:Kotlin 进阶不迷路:41 个核心知识点,构建完整知识体系
一、前言
1.1 类与构造函数的核心作用
在面向对象编程体系中,类 是对一类事物共同属性和行为的抽象描述,相当于创建对象的"模板"。例如"Person"类可抽象人类的"姓名、年龄"等属性和"说话、行走"等行为。而对象则是类的具体实例,是模板的"实体化产物",比如通过"Person"类创建的"张三""李四"。
构造函数作为类的特殊成员,核心职责是初始化对象------在创建对象时为属性赋值、执行必要的初始化逻辑(如参数校验、资源加载),确保对象创建后处于可用状态。没有构造函数,对象的属性可能处于未定义的无效状态,无法正常使用。
1.2 Kotlin 构造函数的特点
相较于 Java 等传统面向对象语言,Kotlin 构造函数设计更简洁、灵活,核心特点体现在两方面:
- 语法极致简洁:支持在类名后直接定义主构造函数,无需单独编写函数体;可通过"val/var"关键字将构造参数直接声明为类属性,省去手动定义属性和赋值的冗余代码。
- 主/次构造分离:将构造函数分为"主构造函数"和"次构造函数",主构造承担核心初始化逻辑,次构造作为辅助补充,适配多样化实例化场景,同时通过委托机制保证初始化逻辑统一。
1.3 本文核心内容预告
本文将遵循"基础认知→核心详解→关联分析→实践应用→总结建议"的逻辑展开:首先解析主构造函数的语法、初始化方式及实例;接着讲解次构造函数的定义规则、适用场景及示例;然后深入分析主/次构造函数的委托关系、初始化顺序和参数传递逻辑;再结合实际开发场景说明两者的应用场景;最后总结核心知识点、避坑要点及选择技巧,帮助读者高效掌握 Kotlin 构造函数的使用。
二、主构造函数:类的核心初始化入口
主构造函数是 Kotlin 类的"默认初始化入口",承担类的核心初始化逻辑,是最常用的构造函数类型,其设计初衷是简化常规场景下的对象创建流程。
2.1 基本语法
Kotlin 主构造函数的语法极为简洁,直接在类名后通过"()"定义参数列表,无需像 Java 那样单独编写构造方法体。基本语法格式如下:
kotlin
// 基础语法:类名后紧跟主构造参数列表
class 类名(参数1: 类型1, 参数2: 类型2, ...) {
// 类体:包含属性、方法、初始化块等
}
// 含访问修饰符的语法(如私有主构造)
class 类名 private constructor(参数1: 类型1, 参数2: 类型2) {
// 类体
}
语法说明:
- 参数列表格式为"参数名: 类型",多个参数用逗号分隔,用于接收对象创建时传入的初始化数据;
- 若类体无逻辑,花括号可省略,如"class Person(name: String, age: Int)";
- 当主构造需要添加访问修饰符(如 private)时,必须显式添加"constructor"关键字,否则默认公开。
示例:定义"Person"类,主构造接收"姓名"和"年龄"参数:
kotlin
// 简单主构造函数示例
class Person(name: String, age: Int) {
// 类体可后续添加属性和方法
}
2.2 简洁初始化
主构造函数的核心优势是"简洁初始化",通过"val/var"修饰参数可直接将其声明为类成员属性,同时支持为参数设置默认值实现"可选参数",大幅简化代码。
2.2.1 直接声明属性
若构造参数需要作为类的属性被外部访问或内部使用,只需在参数前添加"val"(只读属性)或"var"(可读写属性),Kotlin 会自动完成属性定义和赋值。示例:
kotlin
// 主构造参数直接声明为类属性
class Person(val name: String, var age: Int) {
// 类方法使用属性
fun introduce() {
println("我叫$name,今年$age 岁")
}
}
// 测试代码
fun main() {
// 创建对象时直接传入参数初始化属性
val zhangsan = Person("张三", 25)
println(zhangsan.name) // 输出:张三(val修饰,只读)
zhangsan.age = 26 // var修饰,可修改
zhangsan.introduce() // 输出:我叫张三,今年26 岁
}
解析:通过"val name"和"var age"将构造参数直接声明为属性,创建对象时传入的参数会自动赋值给属性,无需像 Java 那样单独定义"private String name"再通过构造函数赋值,代码精简效果显著。
2.2.2 设置默认值
为构造参数设置默认值后,创建对象时可选择性传入该参数(传入则覆盖默认值,不传入则使用默认值),实现"按需初始化"。语法格式为"参数名: 类型 = 默认值",示例:
kotlin
// 主构造参数设置默认值
class Person(val name: String, var age: Int = 18) {
fun introduce() {
println("我叫$name,今年$age 岁")
}
}
// 测试代码
fun main() {
// 1. 传入所有参数(覆盖默认值)
val zhangsan = Person("张三", 25)
zhangsan.introduce() // 输出:我叫张三,今年25 岁
// 2. 只传入无默认值的参数(使用age默认值18)
val lisi = Person("李四")
lisi.introduce() // 输出:我叫李四,今年18 岁
// 3. 按参数名传入,灵活调整顺序
val wangwu = Person(age = 30, name = "王五")
wangwu.introduce() // 输出:我叫王五,今年30 岁
}
解析:"age"参数默认值为 18,创建对象时可传可不传;按参数名传入时可打破参数顺序限制,尤其适合参数较多的场景,提升代码可读性。
2.3 初始化块
主构造函数本身没有函数体,无法编写复杂初始化逻辑(如参数校验、数据转换、资源加载)。Kotlin 提供**初始化块(Initializer Block)**解决这一问题,通过"init"关键字定义,用于补充主构造函数的初始化逻辑。
2.3.1 基本语法与执行时机
kotlin
class 类名(参数列表) {
// 初始化块:补充初始化逻辑
init {
// 初始化代码(参数校验、数据转换等)
}
// 多个初始化块按顺序执行
init {
// 第二个初始化块逻辑
}
}
执行时机:创建对象时,初始化块会在主构造参数初始化后立即执行;若有多个初始化块,按代码定义顺序依次执行。
2.3.2 实用示例
示例 1:参数校验(确保对象创建的合法性)
kotlin
class Person(val name: String, var age: Int) {
// 初始化块:校验参数合法性
init {
require(name.isNotBlank()) { "姓名不能为空!" } // 非空校验
require(age in 0..150) { "年龄必须在0-150之间!" } // 范围校验
println("Person对象初始化完成:name=$name, age=$age")
}
}
// 测试代码
fun main() {
// 合法参数:正常初始化
val zhangsan = Person("张三", 25) // 输出:Person对象初始化完成:name=张三, age=25
// 非法参数:抛出异常
// val lisi = Person("", 20) // 抛出IllegalArgumentException: 姓名不能为空!
// val wangwu = Person("王五", 200) // 抛出IllegalArgumentException: 年龄必须在0-150之间!
}
示例 2:数据转换(处理参数格式适配)
kotlin
class Person(val name: String, ageStr: String) {
// 类属性:存储转换后的整数年龄
val age: Int
// 初始化块:将字符串年龄转换为整数
init {
require(name.isNotBlank()) { "姓名不能为空!" }
// 转换失败则抛出异常
age = ageStr.toIntOrNull() ?: throw IllegalArgumentException("年龄必须是整数!")
require(age in 0..150) { "年龄必须在0-150之间!" }
}
}
// 测试代码
fun main() {
val zhangsan = Person("张三", "25")
println("${zhangsan.name} 的年龄是 ${zhangsan.age}") // 输出:张三 的年龄是 25
// 非法参数:抛出异常
// val lisi = Person("李四", "abc") // 抛出IllegalArgumentException: 年龄必须是整数!
}
解析:主构造参数"ageStr"未用"val/var"修饰,仅作为临时参数;在初始化块中将其转换为整数后赋值给类属性"age",实现参数格式适配。
2.4 简单示例
综合以上知识点,通过"Book"类展示主构造函数的完整使用流程:
kotlin
/**
* 书籍类:主构造包含核心属性
* title:书名(只读,必填)
* author:作者(只读,必填)
* price:价格(可读写,默认39.9)
* category:分类(只读,默认"未知")
*/
class Book(
val title: String,
val author: String,
var price: Double = 39.9,
val category: String = "未知"
) {
// 初始化块1:参数校验
init {
require(title.isNotBlank()) { "书名不能为空!" }
require(author.isNotBlank()) { "作者不能为空!" }
require(price > 0) { "价格必须大于0!" }
}
// 初始化块2:打印初始化信息
init {
println("书籍初始化完成:《$title》($author),分类:$category,价格:$price 元")
}
// 类方法:修改价格
fun updatePrice(newPrice: Double) {
require(newPrice > 0) { "新价格必须大于0!" }
price = newPrice
println("价格更新为:$newPrice 元")
}
}
// 测试代码
fun main() {
// 1. 传入所有参数
val book1 = Book("Kotlin从入门到精通", "张三", 59.9, "编程")
book1.updatePrice(49.9) // 输出:价格更新为:49.9 元
// 2. 传入必填参数(使用默认值)
val book2 = Book("Java核心技术", "李四")
// 输出:书籍初始化完成:《Java核心技术》(李四),分类:未知,价格:39.9 元
// 3. 按参数名传入(灵活调整顺序)
val book3 = Book(
title = "Python编程实践",
author = "王五",
category = "编程",
price = 69.9
)
}
三、次构造函数:灵活的辅助初始化方式
主构造函数适用于"参数固定、场景单一"的初始化场景,而实际开发中常需要"多样化参数组合""兼容旧逻辑"等灵活需求。Kotlin 的次构造函数作为辅助初始化方式,可适配这些复杂场景,同时通过委托机制保证核心逻辑统一。
3.1 基本语法
次构造函数通过"constructor"关键字定义,位于类体内部,必须通过": this(...)"委托给主构造函数或其他次构造函数。基本语法格式如下:
kotlin
class 类名(主构造参数列表) {
// 次构造函数1:委托给主构造函数
constructor(参数列表1) : this(主构造参数映射逻辑) {
// 次构造专属初始化逻辑(可选)
}
// 次构造函数2:委托给其他次构造函数
constructor(参数列表2) : this(参数列表1映射逻辑) {
// 次构造专属初始化逻辑(可选)
}
}
语法说明:
- "constructor"是次构造函数的标识,不可省略;
- ": this(...)"是委托语句,必须存在------次构造函数不能独立初始化,最终需委托到主构造函数;
- 花括号内是次构造专属逻辑,仅在通过该次构造创建对象时执行。
3.2 核心规则
次构造函数的核心规则是必须委托,即所有次构造函数最终都要委托到主构造函数,形成"次→主"或"次→次→主"的委托链。这一规则的本质是"保证主构造函数的核心初始化逻辑(参数校验、属性赋值)在所有场景下都被执行",避免不同构造函数导致的对象状态不一致。
3.2.1 委托给主构造函数(最常用)
次构造函数直接通过"this(主构造参数)"委托给主构造函数,示例:
kotlin
class Person(val name: String, val age: Int, val gender: String) {
// 初始化块:主构造核心逻辑
init {
require(name.isNotBlank()) { "姓名不能为空!" }
require(age in 0..150) { "年龄必须在0-150之间!" }
println("主构造初始化完成:$name($age 岁,$gender)")
}
// 次构造1:无参数,委托主构造(传默认值)
constructor() : this("匿名", 0, "未知") {
println("无参次构造执行:创建默认对象")
}
// 次构造2:双参数,委托主构造(性别默认)
constructor(name: String, age: Int) : this(name, age, "未知") {
println("双参次构造执行:性别默认未知")
}
}
// 测试代码
fun main() {
println("=== 无参次构造创建 ===")
val person1 = Person()
// 输出:主构造初始化完成:匿名(0 岁,未知)→ 无参次构造执行:创建默认对象
println("=== 双参次构造创建 ===")
val person2 = Person("张三", 25)
// 输出:主构造初始化完成:张三(25 岁,未知)→ 双参次构造执行:性别默认未知
}
3.2.2 委托给其他次构造函数(链式委托)
次构造函数委托给另一个次构造函数,再由该次构造委托给主构造函数,形成链式委托,示例:
kotlin
class Person(val name: String, val age: Int, val gender: String) {
init {
println("主构造初始化:$name($age,$gender)")
}
// 次构造3:双参数→委托主构造
constructor(name: String, age: Int) : this(name, age, "未知") {
println("双参次构造执行")
}
// 次构造2:单参数→委托次构造3
constructor(name: String) : this(name, 18) {
println("单参次构造执行")
}
// 次构造1:无参数→委托次构造2
constructor() : this("匿名") {
println("无参次构造执行")
}
}
// 测试代码
fun main() {
val person = Person()
// 输出顺序:主构造初始化→双参次构造执行→单参次构造执行→无参次构造执行
}
3.3 适用场景
次构造函数的核心价值是"适配多样化初始化场景",常见适用场景包括:
3.3.1 多参数组合需求
当创建对象需要支持"不同数量、不同类型参数"时,通过次构造函数提供多个入口。例如"User"类需要支持:1. 用户名+密码登录;2. 用户名+密码+手机号注册;3. 第三方登录(如微信openId)。
kotlin
class User(val username: String, val password: String, val phone: String?) {
init {
require(username.isNotBlank()) { "用户名不能为空!" }
require(password.length >= 6) { "密码长度不能小于6位!" }
}
// 场景1:用户名+密码(手机号为空)
constructor(username: String, password: String) : this(username, password, null)
// 场景2:微信登录(自动生成用户名和密码)
constructor(wechatOpenId: String) : this(
username = "wechat_$wechatOpenId",
password = generateRandomPwd(),
phone = null
)
// 辅助方法:生成随机密码
private fun generateRandomPwd(): String {
val chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
return (1..8).map { chars.random() }.joinToString("")
}
}
3.3.2 兼容旧逻辑或第三方接口
项目迭代或对接第三方时,需兼容旧参数格式。例如旧系统"Order"类用"整数状态码"(0=待支付,1=已支付),新系统用"字符串状态",通过次构造函数适配。
kotlin
// 新系统主构造:状态为字符串
class Order(val orderNo: String, val amount: Double, val status: String) {
init {
require(orderNo.isNotBlank()) { "订单号不能为空!" }
require(amount > 0) { "金额必须大于0!" }
}
// 次构造:兼容旧系统整数状态码
constructor(orderNo: String, amount: Double, statusCode: Int) : this(
orderNo = orderNo,
amount = amount,
status = when (statusCode) {
0 -> "待支付"
1 -> "已支付"
else -> throw IllegalArgumentException("状态码不合法!")
}
)
}
3.4 简单示例
通过"Student"类展示多个次构造函数的使用,适配不同创建场景:
kotlin
/**
* 学生类:主构造包含核心属性
* id:学号(必填)
* name:姓名(必填)
* grade:年级(必填)
* score:平均分(默认60.0)
*/
class Student(
val id: String,
val name: String,
val grade: String,
var score: Double = 60.0
) {
init {
require(id.isNotBlank()) { "学号不能为空!" }
require(name.isNotBlank()) { "姓名不能为空!" }
}
// 次构造1:无参(测试用,生成默认学生)
constructor() : this(
id = "TEST_${System.currentTimeMillis()}",
name = "测试学生",
grade = "高一"
)
// 次构造2:三参数(使用默认平均分)
constructor(id: String, name: String, grade: String) : this(id, name, grade, 60.0)
// 次构造3:四参数(分数字符串转Double)
constructor(id: String, name: String, grade: String, scoreStr: String) : this(
id = id,
name = name,
grade = grade,
score = scoreStr.toDoubleOrNull() ?: throw IllegalArgumentException("分数必须是数字!")
)
// 展示学生信息
fun showInfo() {
println("学号:$id,姓名:$name,年级:$grade,平均分:$score")
}
}
// 测试代码
fun main() {
// 1. 无参次构造
Student().showInfo()
// 2. 三参数次构造
Student("2024001", "张三", "高一").showInfo()
// 3. 四参数次构造(字符串分数)
Student("2024002", "李四", "高二", "85.5").showInfo()
}
四、主构造函数与次构造函数的关联
主构造函数和次构造函数并非独立存在,而是通过"委托关系""初始化顺序""参数传递"形成紧密关联,理解这些关联是正确使用构造函数的关键。
4.1 委托关系
Kotlin 规定:所有次构造函数最终必须委托到主构造函数,形成"委托链"。这一规则的核心目的是"保证主构造函数的核心逻辑(参数校验、属性赋值)在任何初始化场景下都被执行",避免对象状态不一致。
委托关系的两种形式:
- 直接委托:次构造→主构造,如"constructor(name: String) : this(name, 18)";
- 间接委托:次构造→次构造→...→主构造,如"constructor() : this("匿名") → constructor(name: String) : this(name, 18)"。
强制要求:若类显式定义主构造函数,所有次构造函数必须显式委托,否则编译报错。例如以下代码编译失败:
kotlin
// 编译失败:次构造未委托主构造
class Person(val name: String) {
// 错误:缺少委托语句
constructor(age: Int) { }
}
4.2 初始化顺序
通过次构造函数创建对象时,初始化逻辑的执行顺序是固定的,遵循"主构造参数初始化 → 初始化块(按顺序) → 次构造函数体"的规则。这一顺序决定了"哪些逻辑先执行,哪些属性可使用"。
4.2.1 执行顺序示例验证
kotlin
class Person(val name: String, val age: Int) {
// 初始化块1
init {
println("init块1:name=$name,age=$age")
}
// 类属性
val gender: String = "未知"
// 初始化块2
init {
println("init块2:gender=$gender")
}
// 次构造函数
constructor(name: String) : this(name, 18) {
println("次构造体执行:默认年龄18")
}
}
// 测试代码
fun main() {
val person = Person("张三")
}
输出结果:
ini
init块1:name=张三,age=18
init块2:gender=未知
次构造体执行:默认年龄18
解析:执行顺序完全遵循规则,先完成主构造参数初始化和初始化块逻辑,再执行次构造体,确保次构造体可使用所有已初始化的属性。
4.3 参数传递
次构造函数向主构造函数传递参数是委托关系的核心,本质是"将次构造的输入参数转换为主构造所需参数",常见传递方式有三种:
4.3.1 直接传递
次构造参数与主构造参数一一对应,直接传入。示例:
kotlin
class Person(val name: String, val age: Int, val gender: String) {
// 次构造参数直接传递给主构造
constructor(name: String, age: Int) : this(name, age, "未知")
}
4.3.2 默认值填充
次构造参数不足时,为缺失参数填充默认值后传递。示例:
kotlin
class Book(val title: String, val author: String, val price: Double = 39.9) {
// 无参次构造:填充所有默认值
constructor() : this("未知书名", "未知作者")
}
4.3.3 数据转换
次构造参数类型与主构造不一致时,先转换再传递。示例:
kotlin
class Product(val id: Long, val price: Double) {
// 次构造:字符串id转Long,整数价格转Double
constructor(idStr: String, priceInt: Int) : this(
id = idStr.toLong(),
price = priceInt.toDouble()
)
}
五、实用场景举例
结合实际开发场景,说明主、次构造函数的合理搭配使用。
5.1 主构造函数场景:常规实体类初始化
数据库实体类、接口返回数据模型等场景,通常需要将外部数据直接映射为类属性,初始化逻辑简单,适合用主构造函数。
kotlin
import java.util.Date
/**
* 订单实体类:映射数据库order表
* 字段与主构造参数一一对应,通过初始化块校验参数
*/
data class OrderEntity(
val id: Long,
val userId: Long,
val amount: Double,
val createTime: Date,
val status: String
) {
init {
require(id > 0) { "订单ID必须大于0!" }
require(userId > 0) { "用户ID必须大于0!" }
require(amount > 0) { "金额必须大于0!" }
require(status in listOf("待支付", "已支付", "已取消")) { "状态不合法!" }
}
}
// 使用:接口返回数据直接构造对象
fun main() {
val order = OrderEntity(
id = 10001,
userId = 101,
amount = 199.9,
createTime = Date(),
status = "待支付"
)
}
解析:数据模型类的核心需求是"属性映射"和"参数校验",主构造函数的简洁语法和初始化块的校验能力完美适配,代码清晰高效。
5.2 次构造函数场景:多场景初始化适配
工具类、第三方集成类等需要适配多种创建方式的场景,适合用"主构造+次构造"组合。例如"Excel导出工具类"需要支持:1. 指定文件路径;2. 指定输出流;3. 默认路径。
kotlin
import java.io.OutputStream
/**
* Excel导出工具类
* 主构造:接收输出流(核心初始化方式)
*/
class ExcelExporter(private val outputStream: OutputStream) {
// 初始化块:校验输出流
init {
require(outputStream != null) { "输出流不能为null!" }
}
// 次构造1:指定文件路径(转换为输出流)
constructor(filePath: String) : this(java.io.FileOutputStream(filePath))
// 次构造2:默认路径(项目根目录excel导出.xlsx)
constructor() : this("${System.getProperty("user.dir")}/excel导出.xlsx")
// 导出方法
fun export(data: List<Map<String, Any>>) {
// 导出逻辑:使用outputStream写入数据
println("使用${outputStream}导出数据,共${data.size}条")
}
}
// 测试代码
fun main() {
val data = listOf(mapOf("name" to "张三", "age" to 25))
// 1. 默认路径导出
ExcelExporter().export(data)
// 2. 指定路径导出
ExcelExporter("D:/data.xlsx").export(data)
// 3. 指定输出流导出
ExcelExporter(System.out).export(data)
}
解析:主构造函数接收核心依赖"输出流",保证核心逻辑统一;次构造函数适配"文件路径""默认路径"等场景,通过参数转换委托给主构造,既灵活又保证一致性。
六、总结与使用建议
Kotlin 主/次构造函数的设计核心是"简洁高效+灵活适配",掌握两者的使用技巧可大幅提升对象初始化的代码质量。
6.1 核心知识点回顾
| 类型 | 核心语法 | 核心规则 |
|---|---|---|
| 主构造函数 | 类名后括号定义参数,val/var声明属性 | 核心初始化入口,可通过init块补充逻辑 |
| 次构造函数 | constructor关键字定义,: this(...)委托 | 必须委托到主构造,适配多样化场景 |
| 关联关系 | 委托链、固定初始化顺序、参数映射传递 | 次构造最终委托主构造,保证逻辑统一 |
6.2 避坑点
- 委托遗漏:显式定义主构造后,次构造必须委托,否则编译报错;
- 初始化顺序错误:init块执行时次构造体未执行,不可在init块中依赖次构造体的逻辑;
- 参数未声明为属性:主构造参数未加val/var时,仅为临时参数,类内部不可长期使用;
- 默认值位置错误:有默认值的参数需放在无默认值参数之后,否则编译报错(如"class Person(age: Int = 18, name: String)"错误)。
6.3 选择技巧
- 优先使用主构造函数:常规场景(如实体类、数据模型)优先用主构造,通过val/var声明属性,init块补充校验,简洁高效;
- 复杂场景加次构造:需要适配多参数组合、兼容旧逻辑、第三方集成等场景时,添加次构造函数,通过委托复用主构造逻辑;
- 避免过度使用次构造:若次构造过多(超过3个),可考虑用"建造者模式"替代,提升代码可读性;
- 私有主构造控制实例化:需要限制对象创建方式(如单例模式)时,将主构造设为private,通过次构造或静态方法控制实例化。
总之,Kotlin 构造函数的使用核心是"主构造定核心,次构造补灵活",结合实际场景合理搭配,可写出简洁、高效、易维护的代码。