简介
经过上一章节,我们对Kotlin基础语法有了大致的了解,了解了什么是基本类型、集合、控制流程、函数、类、空安全,接下来我们将更近一步的去了解Kotlin的相关知识,本章节将详细的学习Kotlin中面向对象编程(OOP)的相关知识。
目录
- 类相关
- 继承相关
- 接口相关
- 属性相关
- 声明属性
- [Getter 和 Setter](#Getter 和 Setter "#19")
- 编译时常量
- 延迟初始化的属性和变量
- 数据类相关
- 密封类和接口相关
- 嵌套类和内部类相关
- 枚举类相关
- 内联值类
- 对象声明和表达式
- 委托属性
类相关
-
类
Kotlin中的类是使用关键字class声明的:
kotlinclass Person { /*...*/ }
类声明由类名、类头(指定其类型参数、主构造函数和其他一些东西)和用花括号括起来的类体组成。页眉和正文都是可选的;如果类没有主体,则可以省略花括号。
kotlinclass Empty
-
构造函数(主构造函数)
Kotlin中的类有一个主构造函数,可能还有一个或多个辅助构造函数。主构造函数在类标头中声明,它位于类名和可选类型参数之后。
kotlinclass Person constructor(firstName: String) { /*...*/ }
如果主构造函数没有任何注释或可见性修饰符,则可以省略构造函数关键字:
kotlinclass Person(firstName: String) { /*...*/ }
主构造函数在类标头中初始化类实例及其属性。类标头不能包含任何可运行的代码。如果我们想在对象创建过程中运行一些代码,可以在类体内使用初始化器块。初始化器块是用 init 关键字后跟花括号声明的。编写任何我们想在花括号内运行的代码。
在实例初始化过程中,初始化器块按照它们在类体中出现的顺序执行,与属性初始化器交错:
kotlinclass InitOrderDemo(name: String) { // also 函数会返回调用者本身,函数内容是println,因此会打印firstProperty,这里知道also函数是干嘛的就行了,在之后的学习中会单独讲解这些高阶函数 val firstProperty = "第一属性: $name".also(::println) init { println("第一次输出 $name 的初始化块") } val secondProperty = "第二属性: ${name.length}".also(::println) init { println("第二次输出 ${name.length} 的初始化块") } } fun main() { InitOrderDemo("hello") // 依次输出 // 第一属性: hello // 第一次输出 hello 的初始化块 // 第二属性: 5 // 第二次输出 5 的初始化块 }
主构造函数参数可以在初始化器块中使用。它们也可以在类体中声明的属性初始化器中使用:
kotlinclass Customer(name: String) { val customerKey = name.uppercase() }
Kotlin有一个简洁的语法来声明属性并从主构造函数初始化它们:
kotlinclass Person(val firstName: String, val lastName: String, var age: Int)
此类声明还可以包括类属性的默认值:
kotlinclass Person(val firstName: String, val lastName: String, var isEmployed: Boolean = true)
在声明类属性时,可以使用尾随逗号:
kotlinclass Person( val firstName: String, val lastName: String, var age: Int ) { /*...*/ }
与常规属性非常相似,在主构造函数中声明的属性可以是可变的(var)或只读的(val)。
如果构造函数有注释或可见性修饰符,则需要构造函数关键字,修饰符位于其之前:
kotlinclass Customer public @Inject constructor(name: String) { /*...*/ }
我们需要知道的是,在Kotlin类的主构造函数中,给参数加上 val 或 var 的意义在于
- 将参数声明为类的属性,避免手动在类中声明属性。
- 自动生成访问器(getter 和 setter),简化代码。
- 控制属性的可变性:val 用于只读属性,var 用于可变属性
kotlinclass Person(name: String, age: Int) class Person2(val name: String, val age: Int) class Person3(name: String, age: Int) { val name: String val age: Int init { this.name = name this.age = age } } fun main() { val person = Person("Jones", 22) println(person.name) // 编译器报错无法找到引用属性: name val person2 = Person2("Jones", 22) println(person2.name) // 输出 Jones val person3 = Person3("Jones", 22) println(person3.name) // 输出 Jones }
上述代码中我们可知道 Person2和Person3的输出结果是一样的,但是Person2的代码更加简洁。
这种语法糖让 Kotlin 的类定义更简洁、更直观,同时保持了强大的表达能力。
-
次构造函数
类还可以声明次要构造函数,其前缀为
constructor
:kotlinclass Person(val pets: MutableList<Pet> = mutableListOf()) class Pet { constructor(owner: Person) { owner.pets.add(this) // 将当前 Pet 类 添加到 Person 的 pet 集合中 } }
如果类有一个主构造函数,则每个次构造函数都需要委托给主构造函数,可以直接委托,也可以通过另一个次构造函数间接委托。委托给同一类的另一个构造函数是使用
this
关键字完成的:kotlinclass Person(val name: String) { // 主构造函数接受一个参数 name,并将其声明为类的只读属性(val)。 val children: MutableList<Person> = mutableListOf() // children 是一个可变列表(MutableList<Person>),用于存储当前 Person 对象的子节点。 // 次构造函数接受两个参数:name 和 parent。 constructor(name: String, parent: Person) : this(name) { // : this(name) 委托给主构造函数,确保 name 属性被正确初始化。 parent.children.add(this) // 在次构造函数的主体中,将当前对象(this)添加到 parent 的 children 列表中。 } }
初始化块中的代码实际上成为主构造函数的一部分。委托给主构造函数发生在访问次构造函数的第一条语句时,因此所有初始化块和属性初始化器中的代码都在次构造函数主体之前执行。
即使类没有主构造函数,委托仍然会隐式发生,并且仍会执行初始化块:
kotlinclass Constructors { init { println("Init block") } constructor(i: Int) { println("Constructor $i") } } fun main() { Constructors(1) // 依次输出 // Init block // Constructor 1 }
如果非抽象类未声明任何构造函数(主构造函数或次构造函数),则将生成一个无参数的主构造函数。构造函数的可见性将是公共的。
如果不希望类具有公共构造函数,请声明一个具有非默认可见性的空主构造函数:
kotlinclass DontCreateMe private constructor() { /*...*/ }
继承相关
-
继承
Kotlin中的所有类都有一个共同的超类Any,这是没有声明超类型的类的默认超类:
kotlinclass Example // 隐式继承自Any
Any 有三个方法:equals()、hashCode()和toString()。因此,这些方法是为所有Kotlin类定义的。
默认情况下,Kotlin类是 final 的,它们不能被继承。要使类可继承,请用 open 关键字标记它:
kotlinopen class Base // 类已开放继承
要声明显式父类型,请将该类型放在类标头中的冒号后面:
kotlinopen class Base(p: Int) class Derived(p: Int) : Base(p)
如果派生类有一个主构造函数,则基类可以(并且必须)根据其参数在该主构造函数中初始化。
如果派生类没有主构造函数,那么每个辅助构造函数都必须使用 super关键字初始化基类型,或者必须委托给另一个有主构造函数的构造函数。请注意,在这种情况下,不同的辅助构造函数可以调用基类型的不同构造函数:
kotlinclass MyView : View { constructor(ctx: Context) : super(ctx) constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) }
-
重写方法
Kotlin要求显式修饰符用于可重写的成员和重写:
kotlinopen class Shape { open fun draw() { println("Shape 类的 draw() 方法") } fun fill() { println("Shape 类的 fill() 方法") } } class Circle() : Shape() { override fun draw() { println("继承 Shape 类的 Circle 类的 draw() 方法") } } fun main() { val shape = Shape() shape.draw() // 输出为 Shape 类的 draw() 方法 shape.fill() // 输出为 Shape 类的 fill() 方法 val circle = Circle() /** * 重写父类方法后 * 父类原有内容被子类重写内容替换 * */ circle.draw() // 输出为 继承 Shape 类的 Circle 类的 draw() 方法 /** * 没有重写的话还是输出父类默认内容 * */ circle.fill() // 输出为 Shape 类的 fill() 方法 /** * 子类重写父类方法,不会影响到父类本身 * */ shape.draw() // 输出为 Shape 类的 draw() 方法 }
Circle.draw()需要重写修饰符。如果它丢失了,编译器会抱怨。如果函数上没有打开修饰符,如Shape.fill(),则不允许在子类中声明具有相同签名的方法,无论是否有覆盖。打开修饰符在添加到最终类(没有打开修饰符的类)的成员时都没有效果。
标记为覆盖的成员本身是打开的,因此可以在子类中覆盖。如果要禁止重新覆盖,请使用final:
kotlinopen class Shape { open fun draw() { println("Shape 类的 draw() 方法") } fun fill() { println("Shape 类的 fill() 方法") } } open class Circle() : Shape() { final override fun draw() { println("继承 Shape 类的 Circle 类的 draw() 方法") } } class Rectangle() : Circle() { override fun draw() { // 编译器报错 "Circle"中的"draw"是 final 的,不能被覆盖 super.draw() } }
-
重写属性
重写机制对属性的作用方式与对方法的作用方式相同。在超类上声明的属性,然后在派生类上重新声明,必须以重写作为前缀,并且它们必须具有兼容的类型。每个声明的属性都可以被带有初始化器的属性或带有get方法的属性覆盖:
kotlinopen class Shape { open val area: Int = 666 } open class Circle() : Shape() { override val area: Int = 999 } fun main() { val shape = Shape() println(shape.area) // 输出为 666 val circle = Circle() println(circle.area) // 输出为 999 println(shape.area) // 输出为 666 }
我们还可以用var属性覆盖val属性,但反之则不然。这是允许的,因为val属性本质上声明了一个get方法,而将其重写为var会在派生类中额外声明一个set方法。
请注意,您可以在主构造函数的属性声明中使用override关键字:
kotlinopen class Shape { open val area: Int = 666 open val width: Int = 111 open var height: Int = 222 } /** * 编译器报错 * Val属性不能覆盖var属性 * public open var-height:在Shape中定义的整数 * */ open class Circle(override val height: Int = 4) : Shape() { override var area: Int = 999 override var width: Int = 222 }
-
派生类初始化顺序
在构造派生类的新实例期间,基类初始化是第一步(之前仅对基类构造函数的参数进行求值),这意味着它发生在派生类的初始化逻辑运行之前。
kotlinopen class Base(val name: String) { init { println("初始化基类") // 3 } open val size: Int = name.length.also { println("初始化基类中的size为:$it") // 4 } } class Derived( name: String, val lastName: String ) : Base(name.replaceFirstChar { it.uppercase() }.also { println("派生类的参数: $it") }) { // 2 init { println("初始化派生类") // 5 } override val size: Int = (super.size + lastName.length).also { println("初始化派生类中的大小: $it") // 6 } } fun main() { println("构造派生类(\"hello\", \"world\")") // 1 Derived("hello", "world") // 依次输出为: // 构造派生类("hello", "world") // 派生类的参数: Hello // 初始化基类 // 初始化基类中的size为:5 // 初始化派生类 // 初始化派生类中的大小: 10 }
这意味着在执行基类构造函数时,派生类中声明或重写的属性尚未初始化。在基类初始化逻辑中使用这些属性中的任何一个(直接或间接通过另一个重写的开放成员实现)都可能导致不正确的行为或运行时失败。因此,在设计基类时,应避免在构造函数、属性初始化器或init块中使用开放成员。
-
调用超类实现
派生类中的代码可以使用super关键字调用其超类函数和属性访问器实现:
kotlinopen class Rectangle { open fun draw() { println("正在绘制长方形") } val borderColor: String get() = "black" } class FilledRectangle: Rectangle() { override fun draw() { super.draw() // 通过 super 关键字调用基类的方法 println("填充矩形") } val fillColor: String get() = super.borderColor // 通过 super 关键字获取到基类的属性 } fun main() { val filledRectangle = FilledRectangle() filledRectangle.draw() println("边框颜色: ${filledRectangle.borderColor}") // 输出为 边框颜色: black println("填充颜色: ${filledRectangle.fillColor}") // 输出为 填充颜色: black }
在内部类中,访问外部类的超类是使用以外部类名限定的super关键字完成的:super@Outer:
kotlinopen class Rectangle { open fun draw() { println("正在绘制长方形") } val borderColor: String get() = "black" } class FilledRectangle: Rectangle() { // 对于 inner 关键字声明的 Filler 类来说, FilledRectangle 是一个外部类 override fun draw() { val filler = Filler() filler.drawAndFill() } inner class Filler { // inner 关键字声明一个内部类 fun fill() { println("正在填充") } fun drawAndFill() { super@FilledRectangle.draw() // 调用外部类的父类方法 fill() println("用颜色绘制一个填充矩形:${super@FilledRectangle.borderColor}") // 获取外部类的父类属性 } } } fun main() { val fr = FilledRectangle() fr.draw() // 依次输出 // 正在绘制长方形 // 正在填充 // 用颜色绘制一个填充矩形 black }
-
重写规则
在Kotlin中,实现继承受以下规则的约束:如果一个类从其直接超类继承了同一成员的多个实现,则它必须重写此成员并提供自己的实现(可能使用继承的实现之一)。
要表示继承实现所来自的超类型,请使用尖括号中的超类型名称进行超级限定,例如super:
kotlinopen class Rectangle { open fun draw() { println("正在绘制长方形") } } interface Polygon { fun draw() { println("正在绘制多边形") } } class Square(): Rectangle(), Polygon { // 编译器要求重写draw(): override fun draw() { super<Rectangle>.draw() super<Polygon>.draw() } } fun main() { val square = Square() square.draw() // 依次输出 // 正在绘制长方形 // 正在绘制多边形 }
从Rectangle和Polygon继承是可以的,但它们都有draw()的实现,所以你需要在Square中重写draw((),并为它提供一个单独的实现来消除歧义。
接口相关
-
接口
Kotlin中的接口可以包含抽象方法的声明以及方法实现。它们与抽象类的不同之处在于接口不能存储状态。它们可以具有属性,但这些属性需要是抽象的或提供访问器实现。
使用关键字interface定义接口:
kotlininterface MyInterface { fun bar() fun foo() { // optional body } }
-
接口的实现
一个类或对象可以实现一个或多个接口:
kotlininterface MyInterface { fun bar() // 没有实现,类必须实现这个方法 fun foo() { // 默认实现该方法,类可以不用实现该方法 println("接口中实现的的 foo()") } } class Child : MyInterface { override fun bar() { println("实现 MyInterface 的 bar()") } } fun main() { Child().bar() // 输出为 实现 MyInterface 的 bar() Child().foo() // 输出为 接口中实现的的 foo() }
-
接口继承
接口可以从其他接口派生,这意味着它既可以为其成员提供实现,也可以声明新的函数和属性。很自然地,实现此类接口的类只需定义缺少的实现:
kotlininterface Named { val name: String } interface Person : Named { val firstName: String val lastName: String override val name: String get() = "$firstName $lastName" } data class Employee( // 不需要实现"name" 因为父类已经实现了 override val firstName: String, override val lastName: String, ) : Person fun main() { val employee = Employee("John", "Doe",) println(employee.name) // 输出 John Doe println(employee.firstName) // 输出 John println(employee.lastName) // 输出 Doe }
-
解决重写冲突
当你在超类型列表中声明许多类型时,你可能会继承同一方法的多个实现:
kotlininterface A { fun foo() { print("A") } fun bar() } interface B { fun foo() { print("B") } fun bar() { print("bar") } } class C : A { override fun bar() { print("bar") } } class D : A, B { override fun foo() { super<A>.foo() super<B>.foo() } override fun bar() { super<B>.bar() } } fun main() { val d = D() d.foo() // 输出为 AB println() d.bar() // 输出为 bar println() val c = C() c.foo() // 输出为 A println() c.bar() // 输出为 bar }
接口A和B都声明了函数foo()和bar()。它们都实现了foo(),但只有B实现了bar()(bar()在A中没有标记为抽象,因为如果函数没有正文,这是接口的默认值)。现在,如果你从a派生出一个具体的类C,你必须重写bar()并提供一个实现。
但是,如果你从A和B派生出D,你需要实现从多个接口继承的所有方法,并且你需要指定D应该如何实现它们。此规则适用于继承了单个实现(bar())的方法和继承了多个实现(foo())。
属性相关
-
声明属性
Kotlin类中的属性可以使用var关键字声明为可变,也可以使用val关键字声明为只读。要使用属性,只需按其名称引用它:
kotlinclass Address { var name: String = "Holmes, Sherlock" var street: String = "Baker" var city: String = "London" var state: String? = null val zip: String = "123456" } fun copyAddress(address: Address): Address { val result = Address() // Kotlin中没有"new"关键字 result.name = address.name // 访问者被调用 result.street = address.street result.city = address.city result.state = address.state // result.zip = address.zip // 编译器报错 Val不能重新分配 return result } fun main() { val address = Address() address.name = "NPC" address.street = "PanYu" address.city = "GuangZhou" address.state = "Single" // address.zip = "12312312312" // 编译器报错 Val不能重新分配 println("name: ${copyAddress(Address()).name}") // 输出为 name: Holmes, Sherlock println("street: ${copyAddress(Address()).street}") // 输出为 street: Baker println("city: ${copyAddress(Address()).city}") // 输出为 city: London println("state: ${copyAddress(Address()).state}") // 输出为 state: null println("zip: ${copyAddress(Address()).zip}") // 输出为 zip: 123456 println() println("name: ${copyAddress(address).name}") // 输出为 name: NPC println("street: ${copyAddress(address).street}") // 输出为 street: PanYu println("city: ${copyAddress(address).city}") // 输出为 city: GuangZhou println("state: ${copyAddress(address).state}") // 输出为 state: Single println("zip: ${copyAddress(address).zip}") // 输出为 zip: 123456 }
-
Getter 和 Setter
声明属性的完整语法如下:
kotlinvar <propertyName>[: <PropertyType>] [= <property_initializer>] [<getter>] [<setter>]
初始化器、getter和setter是可选的。如果可以从初始化器或getter的返回类型推断出属性类型,则属性类型是可选的,如下所示:
kotlinvar initialized = 1 // 具有Int类型、默认getter和setter // var allByDefault // ERROR: 需要显式初始化器,隐含默认getter和setter
只读属性声明的完整语法与可变属性声明有两个不同之处:它以val而不是var开头,并且不允许使用setter:
kotlinval simple: Int? // 具有Int类型,默认getter,必须在构造函数中初始化 val inferredType = 1 // 具有Int类型和默认getter
我们可以为属性定义自定义访问器。如果你定义了一个自定义getter,每次访问该属性时都会调用它(这样你就可以实现一个计算属性)。以下是一个自定义getter的示例:
kotlinclass Rectangle(val width: Int, val height: Int) { val area: Int // 属性类型是可选的,因为它可以从getter的返回类型中推断出来 get() = this.width * this.height } fun main() { val rectangle = Rectangle(3, 4) println("Width=${rectangle.width}, height=${rectangle.height}, area=${rectangle.area}") }
如果可以从getter推断出属性类型,则可以省略它:
kotlinval area get() = this.width * this.height
如果你定义了一个自定义设置器,除了初始化之外,每次为属性赋值时都会调用它。自定义设置程序如下:
kotlinvar stringRepresentation: String get() = this.toString() set(value) { setDataFromString(value) // 解析字符串并为其他属性赋值 }
按照惯例,setter参数的名称是value,但如果您愿意,可以选择其他名称。
如果需要注释访问器或更改其可见性,但不想更改默认实现,则可以定义@Inject访问器而不定义其主体:
kotlinvar setterVisibility: String = "abc" private set // setter是私有的,具有默认实现 var setterWithAnnotation: Any? = null @Inject set // 用Inject注释设置器
-
幕后字段
在 Kotlin 中,幕后字段(Backing Fields) 是与属性相关联的一个隐式字段,用于存储属性的实际值。理解 Backing Fields 是掌握 Kotlin 属性机制的关键。让我们详细分析它的作用、使用场景以及注意事项。
默认的幕后字段
kotlinclass Person { var name: String = "Default Name" // 这里 name 属性有一个默认的 Backing Field,用于存储 "Default Name"。 // 编译器会自动生成 getter 和 setter,并通过 Backing Field 访问值。 }
自定义 getter 和 setter
kotlinclass Person { var name: String = "Default Name" get() = field // field 是 Backing Field 的关键字,用于访问属性的实际值。 在 getter 中,field 返回当前值。 set(value) { field = value // 使用 Backing Field 在 setter 中,field 用于存储新值。 } }
如果自定义的
getter
或setter
中没有使用field
,则不会生成 Backing Field。例如:kotlinclass Person { val fullName: String get() = "John Doe" // 没有 Backing Field }
Backing Field 是 Kotlin 属性的隐式存储字段,用于保存属性的实际值。
它通过 field 关键字在自定义 getter 和 setter 中访问。
Backing Field 的生成条件是:在 getter 或 setter 中使用了 field。
理解 Backing Field 的作用有助于更好地设计 Kotlin 类,并避免常见的错误(如递归调用)。
-
幕后属性
幕后属性(Backing Properties) 是一种设计模式,通过定义一个私有的属性(通常命名为 _propertyName)来存储实际数据,并提供一个公有的属性(通常命名为 propertyName)来访问和修改数据。
这种方式允许开发者完全控制属性的行为,例如添加额外的逻辑、验证或延迟初始化。
如果想实现延迟初始化,我们可以:
kotlinclass Person { private var _name: String? = null // 私有的 Backing Properties,用于存储实际数据 val name: String // 是公有的只读属性,对外暴露数据 get() { if (_name == null) { // 当 name 第一次被访问时,_name 会被初始化为 "Defaule Name" _name = "Default Name" } return _name!! } }
如果想实现数据验证,我们可以:
kotlinclass Person { private var _age: Int = 0 // 私有 Backing Properties,用于存储实际数据 var age: Int // 公有的可读写属性,对外暴露数据 get() = _age // get 返回 _age 存储的实际数据 set(value) { // set 根据条件进行验证 满足在存储 if (value >= 0) { _age = value } else { println("Invalid age") } } }
如果想要实现,只读属性的可变支持,我们可以:
kotlinclass Person { private var _messages: List<String> = mutableListOf() // 私有的 Backing Properties,用于存储实际数据 val messages: List<String> // 公有的只读属性,对外暴露数据 get() = _messages // get 返回 _messages 返回实际存储的数据 fun addMessage(message: String) { // 通过公有方法 addMessage 修改 _message,确保外部代码无法直接修改代码 (_messages as MutableList).add(message) } }
幕后属性(Backing Properties) 是一种强大的设计模式,通过显式定义私有属性来存储数据,并提供公有属性来访问和修改数据。
它适用于需要精细控制属性行为的场景,例如延迟初始化、数据验证、线程安全等。
与 幕后字段(Backing Fields )相比,幕后属性(Backing Properties) 提供了更高的灵活性和封装性,但可能需要更多的代码。
在实际开发中,可以根据需求选择使用 Backing Fields 或 Backing Properties。
-
-
编译时常量 在 Kotlin 中,编译时常量(Compile-time Constants) 是一种特殊的常量,其值在编译时就已经确定,并且可以直接嵌入到代码中。编译时常量在性能优化、代码简化以及跨模块使用中非常有用。让我们详细分析 Kotlin 中的编译时常量,包括其定义、使用场景、限制以及最佳实践。
如果在编译时已知只读属性的值,请使用const修饰符将其标记为编译时常量。此类属性需要满足以下要求:
-
它必须是顶级属性,或者是对象声明或伴随对象的成员。
-
它必须用String类型或基元类型的值初始化
-
它不能是自定义getter
-
它的值必须是在编译时确定,不能是运行时的计算结果
编译器将内联常量的用法,用实际值替换对常量的引用。然而,该常量不会被移除,因此可以使用反射进行交互。
这些属性也可用于注释:
kotlinconst val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated" @Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }
数组长度:
kotlinconst val ARRAY_SIZE: Int = 10 fun main() { val numbers = IntArray(ARRAY_SIZE) println(numbers.size) // 输出 10 }
跨模块共享:
kotlin// 模块 A object Constants { const val APP_NAME: String = "MyApp" } // 模块 B fun printAppName() { println(Constants.APP_NAME) // 输出 MyApp }
编译时常量(const val) 是一种在编译时确定值的常量,适用于注解参数、数组长度等场景。
编译时常量必须定义在顶层或对象声明中,且只能是基本类型或字符串。
与普通常量(val)相比,编译时常量的值在编译时确定,可以直接嵌入到字节码中,减少运行时的开销。
在实际开发中,合理使用编译时常量可以提高代码的可读性和性能。
-
-
延迟初始化属性和变量
在 Kotlin 中,Late-initialized Properties and Variables(延迟初始化属性和变量) 是一种允许在声明时不初始化属性或变量,而在后续某个时间点进行初始化的机制。这种机制主要用于解决某些场景下无法在构造函数中初始化属性的问题。让我们详细分析延迟初始化的作用、使用场景、限制以及最佳实践。
延迟初始化的语法示例如下:
kotlinclass MyClass { lateinit var name: String fun initializeName() { name = "Kotlin" } fun printName() { if (::name.isInitialized) { println(name) // 属性已初始化 } else { println("Name is not initialized") // 属性未初始化 } } }
-
只能用于
var
属性:lateinit
只能用于可变属性(var
),不能用于只读属性(val
)。
-
不能用于可空类型:
lateinit
属性必须是非空类型,不能是String?
这样的可空类型。
-
不能用于基本类型:
lateinit
不能用于Int
、Boolean
等基本类型。
-
必须在使用前初始化:
- 如果在访问
lateinit
属性时未初始化,会抛出UninitializedPropertyAccessException
。
- 如果在访问
依赖注入:
kotlinclass MyService { lateinit var dependency: MyDependency fun execute() { dependency.doSomething() } }
Android开发:
kotlinclass MyActivity : AppCompatActivity() { lateinit var button: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) button = findViewById(R.id.button) button.setOnClickListener { // 处理点击事件 } } }
单元测试:
kotlinclass MyTest { lateinit var myObject: MyClass @Before fun setUp() { myObject = MyClass() } @Test fun testMethod() { myObject.doSomething() } }
延迟初始化(lateinit) 是一种允许在声明时不初始化属性或变量的机制,适用于依赖注入、生命周期回调等场景。
lateinit 属性必须是非空类型,且不能是基本类型。
在使用 lateinit 属性前,必须确保其已初始化,否则会抛出 UninitializedPropertyAccessException。
通过反射 API 可以检查 lateinit 属性是否已初始化。
在实际开发中,合理使用 lateinit 可以提高代码的灵活性和可读性,但应避免滥用。
-
数据类相关
-
数据类 在 Kotlin 中,数据类(Data Class) 是一种特殊的类,专门用于存储数据。它自动生成一些标准方法(如
toString()
、equals()
、hashCode()
等),从而简化了数据的处理和操作。数据类是用 data 关键字标记的类,主要用于存储数据。
编译器会自动为数据类生成以下方法:
- toString():返回对象的字符串表示。
- equals():比较两个对象是否相等。
- hashCode():返回对象的哈希值。
- copy():复制对象,并允许修改部分属性。
- componentN():用于解构声明(Destructuring Declarations)。
语法如下:
kotlindata class ClassName(val property1: Type1, val property2: Type2, ...)
-
数据类的特性
自动生成的方法:
-
toString():
kotlindata class User(val name: String, val age: Int) // 返回对象的字符串表示,格式为 ClassName(property1=value1, property2=value2, ...)。 fun main() { val user = User("Alice", 25) println(user) // 输出:User(name=Alice, age=25) }
-
equals() 和 hashCode():
kotlindata class User(val name: String, val age: Int) // equals() 比较两个对象的属性值是否相等。 // hashCode() 根据属性值生成哈希值。 fun main() { val user1 = User("Alice", 25) val user2 = User("Alice", 25) println(user1 == user2) // 输出:true }
-
copy():
kotlindata class User(val name: String, val age: Int) fun main() { val user = User("Alice", 25) val newUser = user.copy(age = 30) // 复制对象,并允许修改部分属性。 println(newUser) // 输出:User(name=Alice, age=30) }
-
componentN():
kotlindata class User(val name: String, val age: Int) fun main() { val user = User("Alice", 25) // 用于解构声明,将对象的属性解构为多个变量。 val (name, age) = user println("Name: $name, Age: $age") // 输出:Name: Alice, Age: 25 }
-
-
数据类的使用场景
- DTO(数据传输对象):
- 数据类非常适合用于表示从网络或数据库获取的数据。
- 模型类:
- 在 MVC、MVP 或 MVVM 架构中,数据类常用于表示模型。
- 解构声明:
- 数据类的解构声明可以方便地提取属性值。
- 不可变数据:
- 数据类的属性通常是
val
(不可变),适合表示不可变数据。
- 数据类的属性通常是
- DTO(数据传输对象):
-
数据类的限制
-
主构造函数必须至少有一个参数:
kotlindata class User(val name: String) // 正确 data class EmptyUser() // 编译错误
-
主构造函数的参数必须用 val 或 var 标记:
kotlindata class User(val name: String) // 正确 data class User(name: String) // 编译错误
-
不能是抽象类、密封类或内部类:
- 数据类不能是 abstract、sealed 或 inner 类。
-
自动生成的方法仅基于主构造函数的属性:
- 自动生成的
toString()
、equals()
、hashCode()
和copy()
方法仅基于主构造函数的属性。 - 如果需要包含其他属性,可以手动重写这些方法。
- 自动生成的
-
密封类和接口相关
在 Kotlin 中,密封类(Sealed Class) 和 密封接口(Sealed Interface) 是一种用于限制类或接口继承层次的特性。它们允许开发者定义一个受限的类型层次结构,从而提高代码的安全性和可读性。让我们详细分析密封类和密封接口的特性、使用场景、限制以及最佳实践。
-
密封类和密封接口
- 定义:
- 密封类和密封接口都是用 sealed 关键字标记
- 密封类和密封接口的直接子类和实现必须定义在同一个文件中
- 密封类和密封接口本身不能直接实例化
- 特点:
- 密封类和密封接口的基础层次是受限的,所有子类和实现在编译时已知
- 定义:
-
密封类和密封接口的使用场景
- 状态管理:
- 密封类非常适用于表示有限的状态集合(例如网络请求状态、UI状态等)
- 操作类型:
- 密封类可以用于表示有限的操作类型(例如用户操作、命令等)
- 类型安全的表达式:
- 密封类和密封接口可以与 when 表达式结合,实现类型安全的模式匹配
- API设计:
- 密封接口可以用于定义有限的 API 实现集合,确保 API 的扩展性可控
- 状态管理:
-
密封类和密封接口的语法
-
密封类
kotlinsealed class Result { data class Success(val data: String) : Result() data class Error(val message: String) : Result() object Loading : Result() }
-
密封接口
kotlinsealed interface Operation { class Add(val value: Int) : Operation class Subtract(val value: Int) : Operation }
-
-
密封类和密封接口最佳实践
-
表示有限的状态集合:
- 使用密封类表示有限的状态集合(例如网络请求状态、UI 状态等)。
-
结合 when 表达式:
- 利用 when 表达式实现类型安全的模式匹配。
-
避免过度扩展:
- 密封类和密封接口的继承层次应尽量简单,避免过度扩展。
-
API 设计:
- 使用密封接口定义有限的 API 实现集合,确保 API 的扩展性可控。
-
-
密封类和密封接口的示例
-
密封类表示网络请求状态
kotlinsealed class Result { data class Success(val data: String) : Result() data class Error(val message: String) : Result() object Loading : Result() } fun handleResult(result: Result) { // 在结合 when 使用的时候,使用密封类的主要好处就会显现出来 // when表达式与密封类一起使用,允许Kotlin编译器彻底检查是否涵盖了所有可能的情况。在这种情况下,就不需要添加else子句 when (result) { is Result.Success -> println("Success: ${result.data}") is Result.Error -> println("Error: ${result.message}") Result.Loading -> println("Loading...") } } fun main() { val result = Result.Success("Data loaded") handleResult(result) // 输出:Success: Data loaded }
-
密封接口表示操作类型
kotlinsealed interface Operation { class Add(val value: Int) : Operation class Subtract(val value: Int) : Operation } fun performOperation(op: Operation, initialValue: Int): Int { return when (op) { is Operation.Add -> initialValue + op.value is Operation.Subtract -> initialValue - op.value } } fun main() { val op = Operation.Add(10) println(performOperation(op, 5)) // 输出:15 }
-
密封类表示UI状态 可以使用密封类来表示应用程序中的不同UI状态。这种方法允许对UI更改进行结构化和安全的处理。此示例演示了如何管理各种UI状态:
kotlinsealed class UiState { object Loading : UiState() data class Success(val data: List<String>) : UiState() data class Error(val message: String) : UiState() } fun renderUi(state: UiState) { when (state) { is UiState.Loading -> showLoading() is UiState.Success -> showData(state.data) is UiState.Error -> showError(state.message) } } fun showLoading() { println("Loading...") } fun showData(data: List<String>) { println("Data: $data") } fun showError(message: String) { println("Error: $message") } fun main() { val state = UiState.Success(listOf("Item 1", "Item 2")) renderUi(state) // 输出:Data: [Item 1, Item 2] }
-
API请求响应处理
kotlin// 密封类表示 API 请求状态 sealed class ApiState { object Loading : ApiState() // 加载中 data class Success(val data: String) : ApiState() // 请求成功 data class Error(val message: String) : ApiState() // 请求失败 } // 密封接口表示 API 操作 sealed interface WeatherApi { data class GetCurrentWeather(val city: String) : WeatherApi // 获取当前天气 data class GetWeatherForecast(val city: String, val days: Int) : WeatherApi // 获取天气预报 } // 模拟 API 请求 fun fetchWeather(api: WeatherApi): ApiState { return when (api) { is WeatherApi.GetCurrentWeather -> { // 模拟网络请求 if (api.city == "Unknown") { ApiState.Error("City not found") } else { ApiState.Success("Current weather in ${api.city}: Sunny") } } is WeatherApi.GetWeatherForecast -> { // 模拟网络请求 if (api.city == "Unknown") { ApiState.Error("City not found") } else { ApiState.Success("Weather forecast in ${api.city} for ${api.days} days: Sunny, Rainy, Cloudy") } } } } // 处理 API 响应 fun handleApiState(state: ApiState) { when (state) { is ApiState.Loading -> println("Loading...") is ApiState.Success -> println("Success: ${state.data}") is ApiState.Error -> println("Error: ${state.message}") } } fun main() { // 模拟获取当前天气 val currentWeatherRequest = WeatherApi.GetCurrentWeather("New York") val currentWeatherState = fetchWeather(currentWeatherRequest) handleApiState(currentWeatherState) // 输出:Success: Current weather in New York: Sunny // 模拟获取天气预报 val forecastRequest = WeatherApi.GetWeatherForecast("London", 3) val forecastState = fetchWeather(forecastRequest) handleApiState(forecastState) // 输出:Success: Weather forecast in London for 3 days: Sunny, Rainy, Cloudy // 模拟错误请求 val errorRequest = WeatherApi.GetCurrentWeather("Unknown") val errorState = fetchWeather(errorRequest) handleApiState(errorState) // 输出:Error: City not found }
-
嵌套类和内部类相关
在 Kotlin 中,嵌套类(Nested Class) 和 内部类(Inner Class) 是两种用于在类内部定义其他类的方式。它们的主要区别在于是否持有外部类的引用以及访问权限的不同
-
嵌套类
- 嵌套类是定义在另一个类内部的类,默认情况下是静态的(不持有外部类的引用)。
- 使用 class 关键字定义
- 不持有外部类的引用
- 嵌套类类似 Java 中的静态内部类
- 嵌套类可以访问外部类的私有成员,但需要通过外部类的实例
- 当内部类不需要访问外部类的成员时,使用嵌套类。
- 嵌套类适合用于逻辑分组和代码组织。
kotlinclass Outer { private val outerProperty: String = "我是外部属性" class Nested { fun print(outer: Outer) { println("访问外部属性: ${outer.outerProperty}") } } } fun main() { val outer = Outer() val nested = Outer.Nested() nested.print(outer) // 输出:访问外部属性: 我是外部属性 }
使用场景:
-
当内部类不需要访问外部类的成员时。
-
用于逻辑分组,将相关的类组织在一起。
-
内部类
- 内部类是定义在另一个类内部的类,默认情况下是非静态的(持有外部类的引用)
- 使用 inner 关键字定义
- 内部类可以直接访问外部类的成员(属性和方法)
- 内部类类似于 Java 中的非静态内部类
- 内部类可以访问外部类的私有成员
- 当内部类需要访问外部类的成员时,使用内部类。
- 内部类适合用于实现紧密耦合的逻辑(如事件监听器、回调等)。
kotlinclass Outer { private val outerProperty: String = "我是外部属性" inner class Inner { fun print() { println("访问外部属性: $outerProperty") } } } fun main() { val outer = Outer() val inner = outer.Inner() inner.print() // 输出:访问外部属性: 我是外部属性 }
使用场景:
-
当内部类需要访问外部类的成员时。
-
用于实现紧密耦合的逻辑(如事件监听器、回调等)。
-
匿名内部类
Kotlin 还支持 匿名内部类,通常用于实现接口或抽象类的单次使用实例。
当需要快速实现接口或抽象类的单次使用实例时,使用匿名内部类。
kotlininterface OnClickListener { fun onClick() } fun setOnClickListener(listener: OnClickListener) { listener.onClick() } fun main() { setOnClickListener(object : OnClickListener { override fun onClick() { println("Button clicked") } }) }
枚举类相关
Kotlin 的 枚举类(Enum Class)是一种特殊的类,用于定义一组固定的常量。枚举类在 Kotlin 中非常强大,不仅可以定义常量,还可以为每个常量添加属性、方法,甚至实现接口或继承抽象类。
枚举类使用 enum
关键字定义,常量之间用逗号分隔。每个常量都是枚举类的一个实例:
kotlin
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
-
枚举类的特性:
-
枚举常量是实例
kotlinenum class Direction { NORTH, SOUTH, EAST, WEST } fun main() { val direction = Direction.NORTH println(direction) // 输出 NORTH }
-
枚举常量有序
枚举常量在定义时是有序的,可以通过
ordinal
属性获取常量的索引(从 0 开始):kotlinenum class Direction { NORTH, SOUTH, EAST, WEST } fun main() { println(Direction.NORTH.ordinal) // 输出 0 println(Direction.SOUTH.ordinal) // 输出 1 }
-
枚举常量可以定义属性和方法
枚举类可以像普通类一样定义属性和方法,每个常量可以有自己的属性值。
kotlinenum class Direction(val degrees: Int) { NORTH(0), SOUTH(180), EAST(90), WEST(270); fun description(): String { return "Direction $name is at $degrees degrees." } } println(Direction.NORTH.description()) // 输出 "Direction NORTH is at 0 degrees."
-
枚举类可以实现接口
枚举类可以实现接口,为所有常量提供统一的行为。
kotlininterface Printable { fun print() } enum class Direction : Printable { NORTH { override fun print() { println("Facing North") } }, SOUTH { override fun print() { println("Facing South") } }; } fun main() { Direction.NORTH.print() // 输出 "Facing North" }
-
枚举类可以定义抽象方法
枚举类可以定义抽象方法,每个常量必须实现该方法。
kotlinenum class Direction { NORTH { override fun move() { println("Moving North") } }, SOUTH { override fun move() { println("Moving South") } }; abstract fun move() } fun main() { Direction.NORTH.move() // 输出 "Moving North" }
-
-
枚举类的限制:
-
不能继承其他类
枚举类不能继承其他类(单继承限制),但可以实现接口。
-
常量数量固定
枚举类的常量在编译时就已经确定,不能在运行时动态添加或删除。
-
内存开销
每个枚举常量都是枚举类的一个实例,可能会占用更多内存(相比于普通常量)。
-
内联值类
Kotlin 中的 内联值类(Inline Value Classes)是一种特殊的类,用于在不引入额外运行时开销的情况下,封装单一值并提供类型安全。
内联值类使用 value
关键字定义,并且必须包含一个只读属性(val
)。这个属性是内联值类的唯一存储值。
kotlin
@JvmInline
value class Password(val value: String)
-
内联值类的特性
-
类型安全
内联值类的主要目的是提供类型安全。例如,
Password
类封装了一个String
,但它与普通的String
类型不同,可以避免误用。kotlin@JvmInline value class Password(val value: String) fun login(password: Password) { println("Logging in with password: ${password.value}") } fun main() { val rawPassword = "123456" val securePassword = Password(rawPassword) login(securePassword) // Logging in with password: 123456 // login(rawPassword) // 编译报错 : 类型不匹配 }
-
运行时无额外开销
内联值类在运行时会被内联为它的底层类型(如
String
、Int
等),不会引入额外的对象分配开销。kotlinval securePassword = Password("123456")
-
只能包含一个属性
内联值类只能包含一个只读属性(
val
),不能包含其他属性或状态。kotlin@JvmInline value class Name(val value: String) { // 若是声明 var 属性 就会出现编译错误:内联值类只能包含一个属性 var length: Int get() = value.length }
-
可以定义方法和拓展函数
内联值类可以定义方法和扩展函数,但这些方法在运行时会被静态调用,不会引入额外的对象分配。
kotlin@JvmInline value class Name(val value: String) { fun greet() { println("Hello, $value!") } } fun main() { val name = Name("Alice") name.greet() // 输出 "Hello, Alice!" }
-
可以实现接口
内联值类可以实现接口,但不能继承其他类。
kotlininterface Printable { fun print() } @JvmInline value class Name(val value: String) : Printable { override fun print() { println("Name: $value") } } fun main() { val name = Name("Bob") name.print() // 输出 "Name: Bob" }
-
-
内联值类的限制
-
只能包含一个属性
内联值类只能包含一个只读属性,不能包含其他属性或状态
-
不能继承其他类
内联值类不能继承其他类,但可以实现接口
-
不能用于数组
内联值类不能直接用于数组类型。例如,
Array<Password>
是不允许的。 -
不能用于泛型
内联值类不能直接用作泛型类型参数。
-
对象声明和表达式
在Kotlin中,对象允许我们定义一个类并在一个步骤中创建它的实例。当我们需要可重用的单例实例或一次性对象时,这很有用。为了处理这些情况,Kotlin提供了两种关键方法:用于创建单例的对象声明和用于创建匿名一次性对象的对象表达式。
-
对象声明和对象表达式适用场景
-
使用单例来管理共享资源:
你需要确保整个应用程序中只有一个类的实例。例如,管理数据库连接池。
-
创建工厂方法:
您需要一种方便的方式来高效地创建实例。伴生对象允许您定义与类绑定的类级函数和属性,从而简化这些实例的创建和管理。
-
临时修改现有类的行为:
希望在不创建新子类的情况下修改现有类行为。例如,为特定操作向对象添加临时功能。
-
需要类型安全的设计:
需要使用对象表达式一次性实现接口或抽象类。这对于按钮单击处理程序等场景非常有用。
-
-
对象声明
我们可以使用对象声明在Kotlin中创建对象的单个实例,对象声明的名称总是跟在object关键字后面。这允许您在一个步骤中定义一个类并创建它的实例,这对于实现单例非常有用:
kotlin// 声明一个Singleton对象来管理数据提供程序 object DataProviderManager { private val providers = mutableListOf<DataProvider>() // 注册新的数据提供程序 fun registerDataProvider(provider: DataProvider) { providers.add(provider) } // 检索所有已注册的数据提供程序 val allDataProviders: Collection<DataProvider> get() = providers } // 数据提供程序接口示例 interface DataProvider { fun provideData(): String } // 数据提供程序实现示例 class ExampleDataProvider : DataProvider { override fun provideData(): String { return "Example data" } } fun main() { // 创建ExampleDataProvider的实例 val exampleProvider = ExampleDataProvider() val exampleProvider2 = ExampleDataProvider() // 要引用该对象,直接使用其名称 DataProviderManager.registerDataProvider(exampleProvider) DataProviderManager.registerDataProvider(exampleProvider2) // 检索并打印所有数据提供程序 println(DataProviderManager.allDataProviders.map { it.provideData() }) // 输出为: [Example data, Example data] }
-
数据对象
在Kotlin中打印纯对象声明时,字符串表示形式包含其名称和对象的哈希值:
kotlinobject MyObject fun main() { println(MyObject) // 输出: MyObject@hashcode }
但是,通过用data修饰符标记对象声明,您可以指示编译器在调用toString()时返回对象的实际名称,这与数据类的工作方式相同:
kotlindata object MyDataObject { val number: Int = 3 } fun main() { println(MyDataObject) // 输出为: MyDataObject }
此外,编译器还会为您的生成几个函数
data object
:toString()
返回数据对象的名称equals()
/hashCode()
启用相等性检查和基于哈希的集合
数据对象的equals()函数确保所有具有数据对象类型的对象都被视为相等的。在大多数情况下,我们在运行时只有一个数据对象的实例,因为数据对象声明了单例。然而,在运行时生成另一个相同类型的对象的边缘情况下(例如,通过使用带有java.lang.reflect的平台反射或在后台使用此API的JVM序列化库),这确保了对象被视为相等。
kotlinimport java.lang.reflect.Constructor data object MySingleton fun main() { val evilTwin = createInstanceViaReflection() println(MySingleton) // 输出为: MySingleton println(evilTwin) // 输出为: MySingleton // 即使库强制创建MySingleton的第二个实例, // 其equals()函数返回true: println(MySingleton == evilTwin) // true // 不要使用以下方式比较数据对象=== println(MySingleton === evilTwin) // false } fun createInstanceViaReflection(): MySingleton { // Kotlin反射不允许实例化数据对象。 // 这将"强制"创建一个新的MySingleton实例(使用Java平台反射) return (MySingleton.javaClass.declaredConstructors[0].apply { isAccessible = true } as Constructor<MySingleton>).newInstance() }
生成的hashCode()函数具有与equals()函数一致的行为,因此数据对象的所有运行时实例都具有相同的哈希码。
数据对象和数据类之间的差异
虽然数据对象和数据类声明经常一起使用并且有一些相似之处,但也有一些函数不是为数据对象生成的:
-
没有copy()函数。因为数据对象声明旨在用作单例,所以不会生成copy()函数。Singleton将类的实例化限制为单个实例,允许创建实例的副本会违反这一规定。
-
没有componentN()函数。与数据类不同,数据对象没有任何数据属性。由于试图在没有数据属性的情况下破坏这样的对象是没有意义的,因此不会生成componentN()函数。
使用具有密封层次结构的数据对象
数据对象声明对于密封层次结构(如密封类或密封接口)特别有用。它们允许您与对象旁边定义的任何数据类保持对称。
在这个例子中,将EndOfFile声明为数据对象而不是普通对象意味着它将获得toString()函数,而不需要手动覆盖它:
kotlinsealed interface ReadResult data class Number(val number: Int) : ReadResult data class Text(val text: String) : ReadResult data object EndOfFile : ReadResult fun main() { println(Number(7)) // 输出 Number(number=7) println(EndOfFile) // 输出 EndOfFile }
-
伴生对象
伴随对象允许您定义类级函数和属性。这使得创建工厂方法、保持常量和访问共享实用程序变得容易。
类中的对象声明可以用伴随关键字标记:
kotlinclass MyClass { companion object Factory { fun create(): MyClass = MyClass() } }
通过使用类名作为限定符,可以简单地调用伴随对象的成员:
kotlinclass User(val name: String) { // 定义一个作为创建用户实例的工厂的伴随对象 companion object Factory { fun create(name: String): User = User(name) } } fun main(){ // 使用类名作为限定符调用伴随对象的工厂方法。 // 创建新的用户实例 val userInstance = User.create("John Doe") println(userInstance.name) // 输出 John Doe }
可以省略伴随对象的名称,在这种情况下使用名称companion:
kotlinclass User(val name: String) { companion object { fun create(name: String): User = User(name) } } val companionUser = User.Companion fun main(){ val userInstance = companionUser.create("John Doe") println(userInstance.name) // 输出 John Doe }
类成员可以访问其相应伴随对象的私有成员:
kotlinclass User(val name: String) { companion object { private val defaultGreeting = "Hello" } fun sayHi() { println(defaultGreeting) } } fun main(){ User("Nick").sayHi() // Hello }
当类名单独使用时,它充当对类的伴随对象的引用,而不管伴随对象是否命名:
kotlinclass User1 { // 定义一个命名的伴随对象 companion object Named { fun show(): String = "User1's Named Companion Object" } } // 使用类名引用User1的伴随对象 val reference1 = User1 class User2 { // 定义一个未命名的伴随对象 companion object { fun show(): String = "User2's Companion Object" } } // 使用类名引用User2的伴随对象 val reference2 = User2 fun main() { // 从User1的伴随对象调用show()函数 println(reference1.show()) // User1's Named Companion Object // 从User2的伴随对象调用show()函数 println(reference2.show()) // User2's Companion Object }
尽管Kotlin中伴随对象的成员看起来像其他语言中的静态成员,但它们实际上是伴随对象的实例成员,这意味着它们属于对象本身。这允许伴随对象实现接口:
kotlininterface Factory<T> { fun create(name: String): T } class User(val name: String) { // 定义实现Factory接口的伴随对象 companion object : Factory<User> { override fun create(name: String): User = User(name) } } fun main() { // 将伴随对象用作Factory val userFactory: Factory<User> = User val newUser = userFactory.create("Example User") println(newUser.name) // 输出 Example User }
但是,在JVM上,如果使用@JvmStatic注释,则可以将伴随对象的成员生成为真正的静态方法和字段。
-
-
对象表达式
对象表达式声明一个类并创建该类的实例,但不命名任何一个类。这些类对于一次性使用非常有用。它们可以从头开始创建,从现有类继承,或实现接口。这些类的实例也称为匿名对象,因为它们是由表达式而不是名称定义的。
-
从头创建匿名对象
对象表达式以Object关键字开头。
如果对象不扩展任何类或实现接口,则可以在object关键字后的花括号内直接定义对象的成员:
kotlinfun main() { val helloWorld = object { val hello = "Hello" val world = "World" // 对象表达式扩展了Any类,该类已经有一个toString()函数, // 因此,它必须被覆盖 override fun toString() = "$hello $world" } print(helloWorld) // 输出 Hello World }
-
从超类型继承匿名对象
要创建从某个(或多个)类型继承的匿名对象,请在对象和冒号后指定此类型:。然后实现或重写这个类的成员,就像你从它继承一样。
如果超类型有构造函数,请向其传递适当的构造函数参数。可以在冒号后指定多个超类型,用逗号分隔:
kotlin// 创建具有余额属性的开放类 BankAccount open class BankAccount(initialBalance: Int) { open val balance: Int = initialBalance } // 使用execute()函数定义接口事务 interface Transaction { fun execute() } // 在 BankAccount 上执行特殊交易的功能 fun specialTransaction(account: BankAccount) { // 创建一个从 BankAccount 类继承并实现 Transaction 接口的匿名对象 // 所提供账户的余额被传递给 BankAccount 超类构造函数 val temporaryAccount = object : BankAccount(account.balance), Transaction { override val balance = account.balance + 500 // 临时奖金 // 从Transaction接口实现execute()函数 override fun execute() { println("执行特殊交易。新的余额为 $balance.") } } // 执行交易 temporaryAccount.execute() } fun main() { // 创建初始余额为1000的银行账户 val myAccount = BankAccount(1000) // 在创建的帐户上执行特殊交易 specialTransaction(myAccount) // 执行特殊交易。1500。 }
-
使用匿名对象座位返回值和值类型
当我们从本地或私有函数或属性返回匿名对象时,该匿名对象的所有成员都可以通过该函数或属性访问:
kotlinclass UserPreferences { private fun getPreferences() = object { val theme: String = "Dark" val fontSize: Int = 14 } fun printPreferences() { val preferences = getPreferences() println("主题: ${preferences.theme}, 字体大小: ${preferences.fontSize}") } } fun main() { val userPreferences = UserPreferences() userPreferences.printPreferences() // 输出 主题: Dark, 字体大小: 14 }
这允许我们返回具有特定属性的匿名对象,提供了一种简单的方法来封装数据或行为,而无需创建单独的类。
如果返回匿名对象的函数或属性具有公共、受保护或内部可见性,则其实际类型为:
-
任意,如果匿名对象没有声明的超类型。
-
匿名对象的声明超类型(如果只有一个这样的类型)。
-
显式声明的类型(如果有多个声明的超类型)。
在所有这些情况下,匿名对象中添加的成员都是不可访问的。如果覆盖的成员在函数或属性的实际类型中声明,则可以访问它们。例如:
kotlininterface Notification { // 在Notification接口中声明notifyUser() fun notifyUser() } interface DetailedNotification class NotificationManager { // 返回类型为Any。消息属性不可访问。 // 当返回类型为Any时,只有Any类的成员是可访问的。 fun getNotification() = object { val message: String = "一般通知" } // 返回类型为Notification,因为匿名对象只实现了一个接口 // notifyUser()函数是可访问的,因为它是Notification接口的一部分 // 消息属性不可访问,因为它未在Notification接口中声明 fun getEmailNotification() = object : Notification { override fun notifyUser() { println("发送电子邮件通知") } val message: String = "你有邮件!" } // 返回类型为DetailedNotification。notifyUser()函数和message属性不可访问 // 只有在DetailedNotification接口中声明的成员才可访问 fun getDetailedNotification(): DetailedNotification = object : Notification, DetailedNotification { override fun notifyUser() { println("发送详细通知") } val message: String = "详细消息内容" } } fun main() { // 这不会产生任何输出 val notificationManager = NotificationManager() // 此处无法访问message属性,因为返回类型为Any // 这不会产生任何输出 val notification = notificationManager.getNotification() // notifyUser()函数是可访问的 // 此处无法访问message属性,因为返回类型为Notification val emailNotification = notificationManager.getEmailNotification() emailNotification.notifyUser() // 发送详细通知 // notifyUser()函数和message属性在此处不可访问,因为返回类型为DetailNotification // 这不会产生任何输出 val detailedNotification = notificationManager.getDetailedNotification() }
-
-
从匿名对象访问变量
对象表达式体中的代码可以访问封闭作用域中的变量:
kotlin// 定义一个 Printer 接口,包含一个 printVariables 方法 interface Printer { fun printVariables() } // 定义一个 Outer 类 class Outer { // 在 Outer 类中定义一个成员变量 outerProperty val outerProperty: String = "Outer Property" // 定义一个方法 createAnonymousObject,返回一个实现了 Printer 接口的匿名对象 fun createAnonymousObject(): Printer { // 在方法内部定义一个局部变量 localVariable val localVariable: String = "Local Variable" // 返回一个匿名对象,该对象实现了 Printer 接口 return object : Printer { // 在匿名对象中定义一个属性 anonymousProperty val anonymousProperty: String = "Anonymous Property" // 实现 Printer 接口的 printVariables 方法 override fun printVariables() { // 打印匿名对象的属性 println("Anonymous Property: $anonymousProperty") // 打印外部类的成员变量 outerProperty println("Outer Property: $outerProperty") // 打印局部变量 localVariable println("Local Variable: $localVariable") } } } } // 主函数 fun main() { // 创建 Outer 类的实例 val outer = Outer() // 调用 createAnonymousObject 方法,获取一个实现了 Printer 接口的匿名对象 val anonymousObject = outer.createAnonymousObject() // 调用匿名对象的 printVariables 方法 anonymousObject.printVariables() }
-
委托属性
Kotlin 中的 委托属性(Delegated Properties)是一种强大的特性,允许你将属性的 getter 和 setter 逻辑委托给另一个对象。通过委托属性,你可以将属性的访问和修改行为抽象出来,从而实现代码复用、懒加载、观察属性变化等功能
-
委托属性的基本语法
kotlinval/var <propertyName>: <Type> by <delegate>
by
关键字用于指定委托对象。- 委托对象必须实现
getValue()
方法(对于val
属性)或getValue()
和setValue()
方法(对于var
属性)。
-
Kotlin 标准库中的委托属性
Kotlin 标准库提供了几种常用的委托属性实现:
-
lazy:懒加载属性
lazy 用于实现懒加载属性,即属性值在第一次访问时才计算。
kotlinval lazyValue: String by lazy { println("Computed!") "Hello" } fun main() { println(lazyValue) // 输出 "Computed!" 和 "Hello" println(lazyValue) // 只输出 "Hello"(值已缓存) }
- lazy 接受一个 Lambda 表达式,用于初始化属性值。
- 属性值在第一次访问时计算,之后会缓存结果。
-
observable:可观察属性
observable 用于在属性值发生变化时执行一些操作。
kotlinimport kotlin.properties.Delegates var observableValue: String by Delegates.observable("Initial Value") { _, old, new -> println("Value changed from $old to $new") } fun main() { observableValue = "New Value" // 输出 "Value changed from Initial Value to New Value" }
-
vetoable:可否决属性
vetoable 类似于 observable,但可以在属性值变化前否决修改。
kotlinimport kotlin.properties.Delegates var vetoableValue: Int by Delegates.vetoable(0) { _, old, new -> println("Attempt to change value from $old to $new") new > old // 只有新值大于旧值时才允许修改 } fun main() { vetoableValue = 10 // 修改成功 println(vetoableValue) // 输出 10 vetoableValue = 5 // 修改被否决 println(vetoableValue) // 输出 10 }
-
notNull:非空属性
notNull 用于定义一个非空属性,但在初始化前可以暂时为 null。
kotlinimport kotlin.properties.Delegates var notNullValue: String by Delegates.notNull() fun main() { // notNullValue 未初始化时访问会抛出 IllegalStateException notNullValue = "Initialized" println(notNullValue) // 输出 "Initialized" }
-
-
自定义委托属性
你可以通过实现 ReadOnlyProperty 或 ReadWriteProperty 接口来创建自定义委托。
kotlinimport kotlin.reflect.KProperty class StringDelegate(private var value: String) { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { println("Getting value: $value") return value } operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) { println("Setting value from $value to $newValue") value = newValue } } fun main() { var delegatedValue: String by StringDelegate("Initial Value") println(delegatedValue) // 输出 "Getting value: Initial Value" 和 "Initial Value" delegatedValue = "New Value" // 输出 "Setting value from Initial Value to New Value" println(delegatedValue) // 输出 "Getting value: New Value" 和 "New Value" }
-
getValue
和setValue
是委托属性的核心方法:getValue
用于获取属性值。setValue
用于设置属性值。
-
thisRef
是属性所属的对象(如果是扩展属性,则为接收者对象)。 -
property
是属性的元数据(如属性名)。
-
-
委托属性的使用场景
-
懒加载 使用 lazy 实现懒加载,适用于初始化成本较高的属性。
-
观察属性变化 使用 observable 或 vetoable 监听属性值的变化,适用于需要响应属性变化的场景。
-
非空属性 使用 notNull 确保属性在使用前已被初始化。
-
自定义逻辑 通过自定义委托实现复杂的属性访问逻辑,例如:
- 属性值的验证。
- 属性值的缓存。
- 属性值的日志记录。
-