面向对象基础:类与对象、属性与字段、构造函数、初始化块、成员函数
好,咱们今天聊聊面向对象。说实话,很多初学者觉得这玩意儿玄乎,其实说白了,它就是一套组织代码的「套路」。你想想看,现实世界里到处都是对象——你手里的手机、坐的椅子、敲代码的电脑。面向对象编程,就是把这种「万物皆对象」的思维搬到代码里。
我个人习惯把类理解成「图纸」,对象就是按图纸造出来的「实物」。一张图纸可以造无数个实物,一个类也可以实例化无数个对象。嗯,这个比喻虽然老套,但确实管用。
1. 类与对象:图纸与实物的关系
在 Kotlin 里定义一个类,用 class 关键字就行。简单到令人发指:
class Person {
var name: String = ""
var age: Int = 0
}
然后创建对象:
val person = Person()
person.name = "张三"
person.age = 25
你看,就这么简单。不过我在项目中遇到过一个问题——很多人把类写得跟数据容器似的,所有属性都公开,到处改来改去。后来维护起来那叫一个酸爽。所以我建议,一开始就想清楚哪些属性该暴露,哪些该藏起来。
2. 属性与字段:Kotlin 的贴心设计
Java 程序员转过来会觉得很爽。在 Java 里你要写 getter/setter,在 Kotlin 里?不存在的。Kotlin 的属性本身就是自带 getter 和 setter 的。
class User {
var username: String = ""
get() = field.uppercase() // 读取时自动转大写
set(value) {
field = value.trim() // 赋值时自动去空格
}
}
这里有个关键概念叫 field。它是个「幕后字段」,你只能在 getter/setter 里访问它。为什么要这么设计?为了防止死循环。你想想看,如果在 setter 里直接写 this.username = value,那又会触发 setter,无限递归下去。用 field 就绕开了这个问题。
核心要点:
- 属性 = 字段 + getter + setter
field是幕后字段,只在 getter/setter 中可用- 用
val声明只读属性(只有 getter) - 用
var声明可变属性(有 getter 和 setter)
3. 构造函数:造对象的入口
Kotlin 的构造函数分两种:主构造函数和次构造函数。主构造函数直接写在类名后面:
class Student(val name: String, var age: Int)
就一行?对,就一行。Kotlin 就是这么简洁。主构造函数的参数可以直接声明为属性——用 val 或 var 修饰就行。
次构造函数用 constructor 关键字:
class Student(val name: String, var age: Int) {
constructor(name: String) : this(name, 0) // 委托给主构造函数
}
我曾经在项目里看到有人写了五六个次构造函数,看得我头皮发麻。其实大部分场景下,用默认参数就能搞定:
class Student(val name: String, var age: Int = 18)
这样调用的时候可以 Student("李四"),也可以 Student("李四", 20),灵活得很。
4. 初始化块:对象诞生时的「洗礼」
初始化块用 init 关键字,它在主构造函数之后、对象创建完成之前执行。说白了,就是给对象做「初始化仪式」的地方。
class DatabaseConfig(host: String, port: Int) {
private val connectionString: String
init {
// 这里可以做校验、计算、日志等操作
require(host.isNotBlank()) { "主机名不能为空" }
require(port in 1..65535) { "端口号不合法" }
connectionString = "jdbc:mysql://$host:$port"
println("数据库配置初始化完成: $connectionString")
}
}
注意,init 块可以有多个,按顺序执行。我个人习惯只写一个,除非逻辑确实需要拆分。写多了反而不好读。
小技巧:初始化块里适合做参数校验、资源初始化、日志记录。但别放耗时操作,比如网络请求——那应该放到专门的工厂方法或异步初始化里。
5. 成员函数:对象的行为
成员函数就是定义在类内部的函数,它描述了对象「能做什么」。
class Calculator {
fun add(a: Int, b: Int): Int = a + b
fun multiply(a: Int, b: Int): Int {
return a * b
}
}
调用的时候:
val calc = Calculator()
println(calc.add(3, 5)) // 8
println(calc.multiply(3, 5)) // 15
成员函数里可以访问类的属性和其他函数。这里有个细节——Kotlin 的函数默认是 final 的,不能被重写。如果你想允许子类重写,得加 open 关键字。这个咱们后面讲继承的时候再细说。
避坑指南:我曾经在项目里看到有人把成员函数写得巨长无比,一个函数几百行。记住,函数应该只做一件事。如果一个函数里既有计算、又有打印、还有网络请求,赶紧拆!
知识体系总览
下面这张图把本章的核心概念串起来了,你可以对照着理解:
完整示例:综合运用
最后,咱们来个综合例子,把今天讲的东西串起来:
class BankAccount(
val accountNumber: String,
val owner: String,
private var balance: Double = 0.0
) {
init {
require(accountNumber.isNotBlank()) { "账号不能为空" }
require(owner.isNotBlank()) { "户主不能为空" }
println("账户 $accountNumber 创建成功,户主:$owner")
}
// 只读属性,对外暴露余额
val currentBalance: Double
get() = balance
// 存款
fun deposit(amount: Double) {
require(amount > 0) { "存款金额必须大于0" }
balance += amount
println("存入 $amount,当前余额:$balance")
}
// 取款
fun withdraw(amount: Double): Boolean {
if (amount <= 0 || amount > balance) {
println("取款失败:金额不合法或余额不足")
return false
}
balance -= amount
println("取出 $amount,当前余额:$balance")
return true
}
}
// 使用
fun main() {
val account = BankAccount("6222021234567890", "王五", 1000.0)
account.deposit(500.0)
account.withdraw(200.0)
println("最终余额:${account.currentBalance}")
}
这个例子涵盖了类、主构造函数、初始化块、属性(包括私有属性和自定义 getter)、成员函数。你想想看,如果不用面向对象,这些逻辑散落在各个函数里,维护起来得多痛苦?
好了,今天的内容就到这里。记住,面向对象不是银弹,但它确实是组织代码的好工具。多用、多练,慢慢你就能体会到它的妙处了。