[kotlin] Kotlin类与成员:Java开发者的面向对象新体验
从Java的冗长样板代码到Kotlin的简洁表达,面向对象编程从未如此优雅。本文将带你彻底掌握Kotlin中类的精妙设计。
引言:为什么Kotlin的类设计更优秀?
如果你习惯了Java中冗长的getter/setter、复杂的构造器重载和有限的可见性控制,那么Kotlin的类设计会让你眼前一亮。Kotlin不仅减少了样板代码,更引入了许多创新特性,让面向对象编程变得更加直观和安全。让我们从构造函数开始这场升级之旅。
一、构造函数:主次分明,各司其职
1.1 主构造函数:简洁的初始化方式
在Java中,构造函数常常伴随着大量的重复代码。Kotlin通过主构造函数彻底简化了这一过程:
// Kotlin:一行代码完成属性声明和初始化
class Person(val name: String, var age: Int, private val id: String = "")
// 等效的Java代码需要大量样板代码
/*
public class Person {
private final String name;
private int age;
private final String id;
public Person(String name, int age) {
this(name, age, "");
}
public Person(String name, int age, String id) {
this.name = name;
this.age = age;
this.id = id;
}
public String getName() { return name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
*/
// 使用示例
val person = Person("张三", 25, "ID001")
println(person.name) // 可以直接访问,相当于调用getter
person.age = 26 // 可以直接修改,相当于调用setter
// person.id = "新ID" // 错误!id是val,且为private
主构造函数的特点:
- 定义在类头中,紧接类名之后
- 可以包含参数声明和默认值
- 参数可以标记为
val(只读)或var(可变) - 如果没有注解或可见性修饰符,
constructor关键字可以省略
1.2 次构造函数:灵活的补充机制
当需要多个构造方式时,Kotlin提供了次构造函数:
class User {
val name: String
var age: Int
val email: String?
// 主构造函数(如果类没有主构造函数,可以这样写)
constructor(name: String, age: Int, email: String? = null) {
this.name = name
this.age = age
this.email = email
}
// 次构造函数1:从名字创建,年龄默认
constructor(name: String) : this(name, 18)
// 次构造函数2:从Map创建
constructor(map: Map<String, Any>) : this(
name = map["name"] as String,
age = (map["age"] as? Int) ?: 0,
email = map["email"] as? String
)
// 次构造函数3:拷贝构造
constructor(original: User) : this(
original.name,
original.age,
original.email
)
}
// 使用示例
val user1 = User("李四") // 使用次构造函数1
val user2 = User("王五", 30) // 使用主构造函数
val user3 = User(mapOf("name" to "赵六")) // 使用次构造函数2
val user4 = User(user3) // 使用次构造函数3
次构造函数规则:
- 必须直接或间接委托给主构造函数(如果存在)
- 使用
constructor关键字定义 - 适合需要多种对象创建方式的场景
1.3 主次构造函数结合使用
更常见的情况是主次构造函数结合使用:
// 主构造函数声明主要属性
class Employee(
val id: String,
val name: String,
var salary: Double
) {
var department: String = "未分配"
var joinDate: String = ""
// 次构造函数:提供更多创建方式
constructor(id: String, name: String) : this(id, name, 5000.0) {
println("新员工创建:$name")
}
// 另一个次构造函数
constructor(map: Map<String, Any>) : this(
map["id"] as String,
map["name"] as String,
(map["salary"] as? Double) ?: 5000.0
) {
department = (map["department"] as? String) ?: "未分配"
}
fun printInfo() {
println("员工[$id]: $name, 薪资: $salary, 部门: $department")
}
}
// 使用
val emp1 = Employee("E001", "张三", 8000.0)
val emp2 = Employee("E002", "李四") // 使用默认薪资
val emp3 = Employee(mapOf(
"id" to "E003",
"name" to "王五",
"salary" to 7500.0,
"department" to "技术部"
))
二、初始化块:灵活的控制逻辑
2.1 init块:主构造函数的补充
init块在对象创建时执行,用于执行初始化逻辑:
class DatabaseConfig(
val host: String,
val port: Int
) {
val connectionString: String
// 第一个init块
init {
require(host.isNotBlank()) { "主机名不能为空" }
require(port in 1..65535) { "端口号必须在1-65535之间" }
connectionString = "jdbc:mysql://$host:$port/mydb"
println("数据库配置初始化完成: $connectionString")
}
var timeout: Int = 3000
// 可以有多个init块,按顺序执行
init {
println("连接超时设置为: ${timeout}ms")
}
// 次构造函数也会执行所有init块
constructor(host: String) : this(host, 3306) {
println("使用默认端口创建配置")
}
}
// 执行顺序示例
val config = DatabaseConfig("localhost")
// 输出:
// 数据库配置初始化完成: jdbc:mysql://localhost:3306/mydb
// 连接超时设置为: 3000ms
// 使用默认端口创建配置
2.2 属性初始化与init块的交互
class Order {
// 属性初始化器在init块之前执行
val orderId: String = generateOrderId()
init {
println("订单初始化开始,ID: $orderId")
}
var items: List<String> = emptyList()
init {
println("订单初始项目数: ${items.size}")
}
constructor(itemCount: Int) {
items = List(itemCount) { "商品${it + 1}" }
println("订单创建完成,包含${items.size}个商品")
}
companion object {
private var counter = 0
fun generateOrderId(): String {
return "ORD${++counter}"
}
}
}
// 使用
val order = Order(3)
// 输出:
// 订单初始化开始,ID: ORD1
// 订单初始项目数: 0
// 订单创建完成,包含3个商品
三、访问修饰符:精细的可见性控制
3.1 Kotlin的访问修饰符体系
Kotlin提供了比Java更精细的可见性控制:
// 不同模块:moduleA和moduleB
// moduleA中的代码
package com.example.modulea
open class Parent {
public val publicProp = "公共" // 默认,到处可见
internal val internalProp = "模块内" // 同一模块内可见
protected val protectedProp = "子类" // 子类中可见
private val privateProp = "私有" // 类内部可见
fun demonstrate() {
println(publicProp) // ✓ 可访问
println(internalProp) // ✓ 可访问
println(protectedProp) // ✓ 可访问
println(privateProp) // ✓ 可访问
}
}
internal class InternalClass { // 模块内可见的类
fun show() = println("我是模块内类")
}
// moduleA中的子类
class Child : Parent() {
fun accessParent() {
println(publicProp) // ✓ 可访问
println(internalProp) // ✓ 可访问(同一模块)
println(protectedProp) // ✓ 可访问(子类)
// println(privateProp) // ✗ 错误!private不可访问
}
}
// moduleB中的代码(不同模块)
package com.example.moduleb
import com.example.modulea.Parent
class ForeignClass {
fun test() {
val parent = Parent()
println(parent.publicProp) // ✓ 可访问
// println(parent.internalProp) // ✗ 错误!不同模块
// println(parent.protectedProp) // ✗ 错误!不是子类
// println(parent.privateProp) // ✗ 错误!private
// val internal = InternalClass() // ✗ 错误!类本身不可见
}
}
3.2 internal修饰符的实际应用
internal是Kotlin特有的修饰符,非常适合模块化开发:
// 在Android模块中
// network/internal/InternalApi.kt
internal class RetrofitConfig { // 模块内部使用,不对外暴露
internal val baseUrl = "https://api.internal.com"
internal fun createClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(InternalInterceptor())
.build()
}
}
internal class InternalInterceptor : Interceptor {
override fun intercept(chain: Chain): Response {
// 内部实现细节
return chain.proceed(chain.request())
}
}
// network/public/ApiClient.kt
class ApiClient { // 公开API
private val config = RetrofitConfig() // 内部使用
fun getUser(userId: String): User {
// 使用内部配置,但对外隐藏实现细节
return apiService.getUser(userId)
}
}
// 其他模块只能访问ApiClient,不知道RetrofitConfig的存在
// 这有利于封装和解耦
3.3 与Java互操作时的注意事项
// Kotlin代码
class VisibilitySample {
public val publicVal = "public" // 对应Java: public
internal val internalVal = "internal" // 对应Java: public(但名称会修改)
protected val protectedVal = "protected" // 对应Java: protected
private val privateVal = "private" // 对应Java: private
internal fun internalMethod() = println("内部方法")
}
// 从Java调用时
public class JavaCaller {
public void test() {
VisibilitySample sample = new VisibilitySample();
System.out.println(sample.getPublicVal()); // ✓ 可访问
System.out.println(sample.getInternalVal()); // ✓ 可访问(但编译器会警告)
// System.out.println(sample.getProtectedVal()); // ✗ 错误
// System.out.println(sample.getPrivateVal()); // ✗ 错误
sample.internalMethod(); // ✓ 可访问(但编译器会警告)
}
}
四、属性:智能的getter/setter
4.1 自动生成的访问器
Kotlin属性会自动生成getter和setter,无需手动编写:
class User {
// val属性:只有getter
val id: String = UUID.randomUUID().toString()
// var属性:有getter和setter
var name: String = ""
// 自定义getter
val displayName: String
get() = if (name.isNotBlank()) name else "匿名用户"
// 自定义setter
var age: Int = 0
set(value) {
require(value >= 0) { "年龄不能为负数" }
field = value // 使用field代表幕后字段
}
// 更复杂的自定义访问器
var email: String = ""
set(value) {
if (value.isNotEmpty() && !value.contains("@")) {
throw IllegalArgumentException("邮箱格式不正确")
}
field = value
}
get() {
return if (field.isEmpty()) "未设置邮箱" else field
}
}
// 使用
val user = User()
user.name = "张三"
println(user.displayName) // 输出: 张三
user.age = 25 // 正常
// user.age = -5 // 抛出异常: 年龄不能为负数
user.email = "zhangsan@example.com"
println(user.email) // 输出: zhangsan@example.com
4.2 lateinit:延迟初始化非空属性
当某些属性不能立即初始化,但又不想使用可空类型时,可以使用lateinit:
class UserService {
// 使用lateinit延迟初始化
lateinit var database: Database
lateinit var logger: Logger
fun initialize() {
// 必须在第一次使用前初始化
database = Database.connect()
logger = LoggerFactory.getLogger(UserService::class.java)
}
fun findUser(id: String): User? {
// 使用时检查是否已初始化
if (!::database.isInitialized) {
throw IllegalStateException("数据库未初始化")
}
return database.findUser(id)
}
}
// 测试中的使用
class TestService {
@Inject lateinit var userRepository: UserRepository
@Inject lateinit var configService: ConfigService
@Before
fun setup() {
// 在测试setup中初始化
userRepository = MockUserRepository()
configService = TestConfigService()
}
}
// lateinit的限制
class Limitations {
// lateinit var primitive: Int // 错误!不能用于基本类型
// lateinit var nullable: String? // 错误!不能用于可空类型
lateinit var list: List<String> // 正确
}
4.3 by lazy:惰性初始化
当属性的初始化开销较大,且可能不会被使用时,可以使用by lazy:
class ResourceManager {
// 惰性加载:第一次访问时才初始化
val heavyResource: HeavyResource by lazy {
println("正在初始化heavyResource...")
HeavyResource() // 这里会执行初始化
}
// 带参数的惰性初始化
val config: Map<String, Any> by lazy {
loadConfigFromFile("config.json")
}
// 线程安全的惰性初始化
val sharedResource: SharedResource by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
println("线程安全的初始化")
SharedResource()
}
// 不使用同步的惰性初始化(性能更高,但多线程可能多次初始化)
val cachedData: Cache by lazy(LazyThreadSafetyMode.NONE) {
Cache.load()
}
fun useResource() {
println("开始使用资源...")
// 第一次调用时会初始化
heavyResource.process()
// 第二次调用时不会重新初始化
heavyResource.process()
}
}
// 使用示例
val manager = ResourceManager()
println("ResourceManager创建完成")
// 此时heavyResource还未初始化
manager.useResource()
// 输出:
// ResourceManager创建完成
// 开始使用资源...
// 正在初始化heavyResource...
// 处理资源...
// 处理资源...
// 在Android中的典型应用
class MainActivity : AppCompatActivity() {
// ViewModel惰性初始化
private val viewModel: MainViewModel by lazy {
ViewModelProvider(this).get(MainViewModel::class.java)
}
// 视图绑定惰性初始化
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 第一次访问时初始化
setContentView(binding.root)
// 第一次访问时初始化
viewModel.data.observe(this) { data ->
updateUI(data)
}
}
}
4.4 lateinit vs by lazy 选择指南
| 特性 | lateinit |
by lazy |
|---|---|---|
| 适用类型 | var 可变属性 |
val 只读属性 |
| 初始化时机 | 手动控制,在使用前初始化 | 第一次访问时自动初始化 |
| 线程安全 | 不安全,需自己保证 | 可选同步模式 |
| 可空性 | 必须是非空类型 | 可以是任何类型 |
| 重新初始化 | 可以重新赋值(使用var) |
只能初始化一次 |
| 典型场景 | 依赖注入、Android生命周期、测试 | 开销大的资源、延迟计算、单例 |
// 正确选择的示例
class ApplicationComponent {
// 使用lateinit:需要手动初始化
lateinit var httpClient: HttpClient
// 使用by lazy:需要时才初始化
val expensiveCache: Cache by lazy {
Cache.build().apply {
loadFromDisk() // 开销大的操作
}
}
// 使用普通属性:立即初始化
val appName: String = "MyApp"
fun initialize() {
httpClient = HttpClient.create() // 手动初始化
}
fun processRequest() {
// httpClient必须先初始化
if (::httpClient.isInitialized) {
httpClient.send()
}
// expensiveCache在第一次访问时自动初始化
val data = expensiveCache.get("key")
}
}
五、综合实战:设计一个完整的Kotlin类
让我们用所有学到的知识设计一个完整的类:
// 用户管理系统中的用户类
data class UserProfile(
// 主构造函数参数
val userId: String,
var username: String,
private var _email: String? = null // 私有支持字段
) {
// 次构造函数
constructor(userId: String) : this(userId, "新用户")
// 初始化块
init {
require(userId.isNotBlank()) { "用户ID不能为空" }
require(username.length in 1..50) { "用户名长度必须在1-50之间" }
println("用户资料初始化: $username (ID: $userId)")
}
// 自定义getter/setter
var email: String?
get() = _email
set(value) {
if (value != null && !isValidEmail(value)) {
throw IllegalArgumentException("邮箱格式不正确")
}
_email = value
lastUpdated = System.currentTimeMillis()
}
// 内部使用的属性
internal var lastUpdated: Long = System.currentTimeMillis()
private set // setter私有
// 惰性初始化的属性
val preferences: Map<String, Any> by lazy {
println("加载用户偏好设置...")
loadPreferencesFromStorage()
}
// lateinit属性
lateinit var avatarService: AvatarService
// 保护性拷贝的列表
private val _roles: MutableList<String> = mutableListOf("USER")
val roles: List<String>
get() = _roles.toList() // 返回不可修改的副本
// 方法
fun addRole(role: String) {
if (role !in _roles) {
_roles.add(role)
lastUpdated = System.currentTimeMillis()
}
}
fun hasPermission(permission: String): Boolean {
return roles.any { it.getPermissions().contains(permission) }
}
// 私有辅助方法
private fun isValidEmail(email: String): Boolean {
return email.contains("@") && email.contains(".")
}
private fun loadPreferencesFromStorage(): Map<String, Any> {
// 模拟从数据库加载
Thread.sleep(100) // 模拟耗时操作
return mapOf("theme" to "dark", "language" to "zh-CN")
}
// 伴生对象
companion object {
private const val DEFAULT_AVATAR = "default_avatar.png"
fun createAdmin(userId: String): UserProfile {
return UserProfile(userId, "管理员").apply {
addRole("ADMIN")
}
}
}
}
// 扩展函数(虽然不是类的一部分,但常一起使用)
fun UserProfile.getDisplayInfo(): String {
return buildString {
append("用户: $username")
email?.let { append(" ($it)") }
if (roles.size > 1) {
append(" [${roles.joinToString()}]")
}
}
}
// 使用示例
fun main() {
val user = UserProfile("U001", "张三", "zhangsan@example.com")
println(user.getDisplayInfo()) // 触发preferences惰性初始化
user.addRole("EDITOR")
// user.roles.add("ADMIN") // 错误!roles是只读的
val admin = UserProfile.createAdmin("A001")
println(admin.roles) // 输出: [USER, ADMIN]
}
六、常见面试题
基础概念题
-
Kotlin中的主构造函数和次构造函数有什么区别?什么情况下应该使用次构造函数?
- 答案要点:主构造函数在类头中声明,用于主要初始化逻辑;次构造函数在类体中用
constructor定义,必须直接或间接委托给主构造函数。次构造函数适用于:1) 提供多种对象创建方式;2) 适配不同数据源;3) Java互操作时保持兼容性。
- 答案要点:主构造函数在类头中声明,用于主要初始化逻辑;次构造函数在类体中用
-
解释Kotlin的
internal访问修饰符,它与Java的protected和默认包私有有什么不同?- 答案要点:
internal表示"模块内可见",是Kotlin特有的修饰符。与Java的区别:1) 不同于protected(子类可见);2) 不同于包私有(Java默认,仅同包可见);3)internal作用于整个模块,适合模块化开发。编译后,internal成员在Java中变为public,但名称会被修改以限制访问。
- 答案要点:
-
Kotlin属性如何自动生成getter/setter?如何自定义访问器?
- 答案要点:Kotlin属性默认生成标准getter/setter。自定义时:1) 使用
get()和set(value)定义访问器;2) 在setter中使用field访问幕后字段;3) 可以添加验证逻辑、触发副作用等。只读属性(val)只有getter,可变属性(var)两者都有。
- 答案要点:Kotlin属性默认生成标准getter/setter。自定义时:1) 使用
实践应用题
-
lateinit和by lazy都可以用于延迟初始化,它们的主要区别是什么?各自适用于什么场景?- 答案要点:
lateinit用于var,需要手动初始化,适用于依赖注入、生命周期回调等场景;by lazy用于val,第一次访问时自动初始化,适用于开销大的资源、延迟计算。关键区别:lateinit可重新赋值,by lazy只初始化一次;lateinit必须是非空类型,by lazy可以是任意类型。
- 答案要点:
-
设计一个Kotlin数据类表示API请求参数,要求:有些参数必填,有些可选,有些有默认值,有些需要验证。如何利用Kotlin特性优雅实现?
- 答案要点:1) 主构造函数声明所有参数;2) 使用默认值处理可选参数;3) 使用
init块或自定义setter进行验证;4) 对复杂可选参数使用可空类型;5) 使用次构造函数提供便捷创建方式。示例:data class ApiRequest(val id: String, var page: Int = 1) { init { require(page > 0) } }
- 答案要点:1) 主构造函数声明所有参数;2) 使用默认值处理可选参数;3) 使用
-
在Kotlin中,如何实现一个只能通过工厂方法创建的对象?如何防止反射破坏这种限制?
- 答案要点:1) 将主构造函数设为
private;2) 在伴生对象中提供工厂方法;3) 对更严格的要求,可以将类设为sealed或使用internal构造函数。防止反射:在模块化项目中,将类放在internal包中;或使用运行时检查,但这不能完全阻止反射。
- 答案要点:1) 将主构造函数设为
深入原理题
-
Kotlin的
init块和Java的构造代码块有什么区别?多个init块的执行顺序是怎样的?- 答案要点:Kotlin的
init块在Java中编译为构造器代码,但更结构化。执行顺序:1) 主构造函数参数初始化;2) 类体内属性初始化器按声明顺序执行;3)init块按声明顺序执行;4) 次构造函数体执行。所有init块都在属性初始化后、构造函数体前执行。
- 答案要点:Kotlin的
-
解释Kotlin属性中的"幕后字段"(backing field)概念,什么时候需要它?
- 答案要点:幕后字段是属性实际存储值的地方。使用
field关键字访问。需要幕后字段的情况:1) 自定义getter/setter中需要引用当前值;2) 需要存储与计算属性不同的状态。不需要的情况:1) 只有getter的表达式属性(如val area get() = width * height);2) 委托属性。
- 答案要点:幕后字段是属性实际存储值的地方。使用
-
Kotlin的访问控制修饰符在跨模块Java调用时是如何处理的?有什么兼容性问题需要注意?
- 答案要点:Kotlin修饰符编译为Java修饰符:
public不变,internal变为public但名称被修饰,protected不变,private变为private。注意事项:1) Java代码可以访问Kotlin的internal成员(通过修饰后的名称);2) 跨模块时应使用公共API;3) 库开发时需要仔细设计API边界。
- 答案要点:Kotlin修饰符编译为Java修饰符:
总结
Kotlin的类设计体现了"简洁而不简单"的哲学。从减少样板代码的自动getter/setter,到精细控制的访问修饰符,再到灵活的初始化机制,每一处设计都旨在提升开发效率和代码质量。
对于Java开发者而言,适应Kotlin的类设计需要思维上的转变:
- 从冗长到简洁:告别重复的getter/setter代码
- 从隐式到显式:明确指定可见性,特别是模块级控制
- 从命令式到声明式:用属性和初始化块表达意图
- 从立即到延迟:合理使用延迟初始化优化性能
更多推荐




所有评论(0)