Kotlin属性与字段
2023-02-216.9k 阅读
Kotlin 属性基础
在 Kotlin 中,属性是一种重要的编程概念,它提供了一种便捷的方式来封装数据,并通过访问器来控制对数据的访问。属性可以是可变的(var
)或只读的(val
)。
定义属性
- 只读属性(
val
): 只读属性一旦初始化后就不能再被重新赋值。例如,定义一个表示人的名字的只读属性:
在上述代码中,class Person { val name: String = "John" }
name
属性是只读的,类型为String
,初始值为"John"
。创建Person
实例后,不能再修改name
的值。 - 可变属性(
var
): 可变属性允许在初始化后重新赋值。比如定义一个表示人的年龄的可变属性:
这里class Person { var age: Int = 30 }
age
属性可以在Person
实例的生命周期内进行修改,例如:
上述代码将fun main() { val person = Person() person.age = 31 println(person.age) }
person
的age
属性从 30 修改为 31 并打印出来。
属性的访问器
- 自定义访问器:
Kotlin 允许为属性自定义访问器,以实现更灵活的逻辑。例如,定义一个只读属性,其值是通过计算得到的:
在class Rectangle(val width: Int, val height: Int) { val area: Int get() = width * height }
Rectangle
类中,area
是一个只读属性,它没有直接存储值,而是通过get
访问器计算得出。使用时:
上述代码创建一个fun main() { val rectangle = Rectangle(5, 10) println(rectangle.area) }
Rectangle
实例,并打印出其area
值,这里area
是通过width
和height
相乘得到的。 - 可变属性的
set
访问器: 对于可变属性,除了get
访问器,还可以自定义set
访问器。例如,定义一个属性,在设置值时进行范围检查:
在class Temperature { var value: Double = 0.0 set(newValue) { if (newValue in -273.15..1000.0) { field = newValue } else { throw IllegalArgumentException("Temperature out of range") } } }
Temperature
类中,value
属性的set
访问器检查新值是否在指定范围内。如果在范围内,就将新值赋给field
(后面会详细介绍field
);否则,抛出异常。使用时:
上述代码先设置fun main() { val temperature = Temperature() temperature.value = 25.0 println(temperature.value) try { temperature.value = -300.0 } catch (e: IllegalArgumentException) { println(e.message) } }
temperature
的value
为 25.0 并打印,然后尝试设置为 -300.0,会捕获到异常并打印错误信息。
幕后字段(Backing Fields)
在 Kotlin 中,当属性需要一个实际存储值的地方时,就会用到幕后字段。
field
标识符
- 基本用法:
当我们需要在访问器中直接访问属性的存储位置时,就使用
field
标识符。例如,在前面Temperature
类的set
访问器中,我们使用field = newValue
将新值赋给实际存储的位置。field
只能在属性的访问器内部使用。 再看一个更简单的例子,统计属性被读取的次数:
在class Counter { var count: Int = 0 get() { field++ return field } }
Counter
类中,每次读取count
属性时,field
会自增并返回。使用时:
上述代码会先打印 1,再打印 2,因为每次读取fun main() { val counter = Counter() println(counter.count) println(counter.count) }
count
时,field
都自增了。 field
与自定义访问器的关系: 只有当属性有自定义访问器时,field
才会被自动生成。如果属性没有自定义访问器,Kotlin 会自动生成默认的访问器,并且不会生成field
。例如:
这里class Simple { val simpleValue: String = "Hello" }
simpleValue
没有自定义访问器,所以不存在field
。如果尝试在代码中使用field
,会导致编译错误。
延迟初始化的幕后字段
lateinit
修饰符: 在某些情况下,我们可能希望延迟属性的初始化。Kotlin 提供了lateinit
修饰符来实现这一点。例如,在 Android 开发中,可能会有一些视图属性在onCreate
方法中初始化。
在上述代码中,class MyActivity : AppCompatActivity() { lateinit var myTextView: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) myTextView = findViewById(R.id.my_text_view) } }
myTextView
属性使用lateinit
修饰,这意味着它可以在类的构造函数之后初始化。使用lateinit
有一些限制,比如只能用于可变属性(var
),并且属性类型不能是可空类型或基本类型(如Int
、Double
等)。by lazy
委托(与幕后字段的关系):by lazy
也可以实现延迟初始化,并且它适用于只读属性(val
)。例如:
在class DataProvider { val expensiveValue: String by lazy { // 模拟一些耗时操作 Thread.sleep(2000) "Expensive result" } }
DataProvider
类中,expensiveValue
属性通过by lazy
委托实现延迟初始化。by lazy
内部也使用了幕后字段来存储初始化后的值。当第一次访问expensiveValue
时,才会执行lazy
块中的代码进行初始化。使用时:
上述代码会先打印 "Before accessing expensiveValue",然后等待 2 秒(模拟耗时操作),再打印 "Expensive result",最后打印 "After accessing expensiveValue"。fun main() { val provider = DataProvider() println("Before accessing expensiveValue") println(provider.expensiveValue) println("After accessing expensiveValue") }
幕后属性(Backing Properties)
幕后属性是 Kotlin 中一种通过自定义委托来实现属性存储和访问逻辑的机制。
自定义委托实现幕后属性
- 定义委托类:
假设我们要实现一个属性,它的值在设置时会被转换为大写。首先定义一个委托类:
在class UpperCaseDelegate { private var value: String = "" operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return value } operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) { value = newValue.toUpperCase() } }
UpperCaseDelegate
类中,getValue
方法用于获取属性的值,setValue
方法用于设置属性的值,并且在设置值时将其转换为大写。 - 使用委托类作为幕后属性:
然后在另一个类中使用这个委托类:
在class MyClass { var myString: String by UpperCaseDelegate() }
MyClass
类中,myString
属性通过UpperCaseDelegate
委托来实现存储和访问逻辑。使用时:
上述代码会打印 "HELLO",因为fun main() { val myClass = MyClass() myClass.myString = "hello" println(myClass.myString) }
myString
属性在设置值时被转换为大写了。
Kotlin 标准库中的幕后属性委托
Delegates.observable
:Delegates.observable
可以用来监听属性值的变化。例如,定义一个属性并监听其变化:
在import kotlin.properties.Delegates class ObservableClass { var observableValue: Int by Delegates.observable(0) { property, oldValue, newValue -> println("Property ${property.name} changed from $oldValue to $newValue") } }
ObservableClass
类中,observableValue
属性通过Delegates.observable
委托,初始值为 0。每次observableValue
的值变化时,都会打印出属性名、旧值和新值。使用时:
上述代码会打印:fun main() { val observableClass = ObservableClass() observableClass.observableValue = 1 observableClass.observableValue = 2 }
Property observableValue changed from 0 to 1 Property observableValue changed from 1 to 2
Delegates.vetoable
:Delegates.vetoable
可以用来在属性值变化前进行验证。例如,定义一个属性,只有当新值大于旧值时才允许变化:
在import kotlin.properties.Delegates class VetoableClass { var vetoableValue: Int by Delegates.vetoable(0) { property, oldValue, newValue -> newValue > oldValue } }
VetoableClass
类中,vetoableValue
属性通过Delegates.vetoable
委托,初始值为 0。只有当新值大于旧值时,vetoableValue
的值才会被更新。使用时:
上述代码会打印 1 和 1,因为第二次尝试将fun main() { val vetoableClass = VetoableClass() vetoableClass.vetoableValue = 1 println(vetoableClass.vetoableValue) vetoableClass.vetoableValue = 0 println(vetoableClass.vetoableValue) }
vetoableValue
设置为 0 时,由于不满足newValue > oldValue
的条件,设置被否决,值没有改变。
伴生对象中的属性
在 Kotlin 中,伴生对象可以包含属性,这些属性类似于 Java 中的静态属性。
伴生对象属性的定义与使用
- 定义伴生对象属性:
例如,定义一个包含伴生对象属性的类:
在class MathUtils { companion object { val PI: Double = 3.141592653589793 var count: Int = 0 } }
MathUtils
类中,伴生对象定义了一个只读属性PI
和一个可变属性count
。 - 使用伴生对象属性:
使用伴生对象属性时,不需要创建类的实例。例如:
上述代码直接通过类名访问伴生对象的属性,先打印fun main() { println(MathUtils.PI) MathUtils.count++ println(MathUtils.count) }
PI
的值,然后增加count
的值并再次打印。
伴生对象属性与普通属性的区别
- 内存模型:
普通属性是每个类实例都有一份独立的拷贝,而伴生对象属性是类级别的,所有实例共享。例如:
创建多个class InstancePropertyClass { var instanceProperty: Int = 0 companion object { var companionProperty: Int = 0 } }
InstancePropertyClass
实例:
上述代码中,fun main() { val instance1 = InstancePropertyClass() val instance2 = InstancePropertyClass() instance1.instanceProperty = 1 instance2.instanceProperty = 2 instance1.companionProperty = 3 println(instance1.instanceProperty) println(instance2.instanceProperty) println(instance1.companionProperty) println(instance2.companionProperty) }
instance1
和instance2
的instanceProperty
值不同,因为它们是各自实例的属性;而companionProperty
值相同,因为它是伴生对象属性,所有实例共享。 - 访问方式: 普通属性通过实例访问,而伴生对象属性通过类名访问。这种访问方式的差异也体现了它们在类结构中的不同角色。普通属性用于表示每个实例特有的状态,而伴生对象属性更适合表示类级别的常量或共享状态。
继承中的属性
在 Kotlin 继承体系中,属性的行为有其独特的规则。
属性的覆盖
- 重写只读属性:
当子类继承自父类时,可以重写父类的属性。例如,定义一个父类和子类,子类重写父类的只读属性:
在上述代码中,open class Animal { open val species: String = "Unknown" } class Dog : Animal() { override val species: String = "Canis lupus familiaris" }
Animal
类定义了一个species
只读属性,Dog
类继承自Animal
并通过override
关键字重写了species
属性。使用时:
上述代码会打印 "Canis lupus familiaris",表明子类成功重写了父类的属性。fun main() { val dog = Dog() println(dog.species) }
- 重写可变属性:
可变属性同样可以被重写。例如:
在open class Shape { open var color: String = "Black" } class Rectangle : Shape() { override var color: String = "Red" }
Shape
类中定义了color
可变属性,Rectangle
类继承自Shape
并重写了color
属性。使用时:
上述代码先打印 "Red",然后将fun main() { val rectangle = Rectangle() println(rectangle.color) rectangle.color = "Blue" println(rectangle.color) }
color
修改为 "Blue" 并再次打印。
属性访问器的重写
- 重写
get
访问器: 子类可以重写父类属性的get
访问器。例如:
在open class Parent { open val message: String get() = "Parent message" } class Child : Parent() { override val message: String get() = "Child message" }
Parent
类中,message
属性有一个自定义的get
访问器。Child
类继承自Parent
并重写了message
属性的get
访问器。使用时:
上述代码会打印 "Child message"。fun main() { val child = Child() println(child.message) }
- 重写
set
访问器: 对于可变属性,子类也可以重写set
访问器。例如:
在open class NumberHolder { open var number: Int = 0 set(newValue) { if (newValue >= 0) { field = newValue } } } class PositiveNumberHolder : NumberHolder() { override var number: Int = 0 set(newValue) { if (newValue > 0) { field = newValue } } }
NumberHolder
类中,number
属性的set
访问器确保值是非负的。PositiveNumberHolder
类继承自NumberHolder
并重写了number
属性的set
访问器,使其确保值是正的。使用时:
上述代码先设置fun main() { val positiveHolder = PositiveNumberHolder() positiveHolder.number = 5 println(positiveHolder.number) positiveHolder.number = -1 println(positiveHolder.number) }
number
为 5 并打印 5,然后尝试设置为 -1,由于不满足PositiveNumberHolder
中set
访问器的条件,number
值不变,再次打印仍为 5。
属性与字段的内存管理
在 Kotlin 中,理解属性和字段的内存管理对于编写高效的代码至关重要。
基本类型属性的内存管理
- 栈内存分配:
对于基本类型(如
Int
、Double
、Boolean
等)的属性,如果它们是局部变量或者类的成员属性且没有被装箱(例如在泛型中使用基本类型会导致装箱),它们通常会在栈上分配内存。例如:
在class BasicTypeClass { var intValue: Int = 10 }
BasicTypeClass
类中,intValue
属性是Int
类型,它在对象实例创建时,会在堆上的对象内存布局中为intValue
分配 4 个字节(假设是 32 位系统)的空间。如果intValue
是一个局部变量,它会直接在栈上分配空间。 - 内存释放:
当对象不再被引用(例如超出作用域或者被显式设置为
null
,对于可空引用类型),相关的内存会被垃圾回收机制回收。对于基本类型属性所在的对象,如果没有其他引用指向该对象,垃圾回收器会回收该对象占用的堆内存,包括其中基本类型属性占用的空间。
引用类型属性的内存管理
- 堆内存分配:
引用类型(如
String
、自定义类等)的属性,其引用本身(类似于指针)会和基本类型属性一样,在对象内存布局中分配空间(通常是 4 字节或 8 字节,取决于系统架构),而实际的对象数据则分配在堆上。例如:
在class ReferenceTypeClass { var stringValue: String = "Hello" }
ReferenceTypeClass
类中,stringValue
属性的引用在ReferenceTypeClass
对象实例的内存布局中,而"Hello"
字符串对象则在堆上。 - 内存释放与引用计数: Kotlin 使用垃圾回收机制来管理引用类型对象的内存释放。当一个引用类型对象没有任何活跃的引用指向它时,垃圾回收器会标记该对象为可回收,并在适当的时候回收其占用的堆内存。有些垃圾回收算法(如引用计数法)会为每个对象维护一个引用计数,当引用计数为 0 时,对象就可以被回收。虽然 Kotlin 可能不完全依赖引用计数法,但理解这种机制有助于理解内存释放的基本原理。例如,如果一个类中有多个引用类型属性,并且这些属性引用的对象之间存在复杂的引用关系,需要确保在不再需要这些对象时,正确地切断引用,以便垃圾回收器能够回收它们的内存。
延迟初始化属性的内存管理
lateinit
属性:lateinit
属性在未初始化时不会占用额外的内存(除了属性引用本身的空间)。例如:
在class LateinitClass { lateinit var lateinitValue: String }
LateinitClass
对象创建时,lateinitValue
只是一个未初始化的引用,不会为String
对象分配内存。只有在调用lateinitValue = "Some value"
进行初始化时,才会在堆上为"Some value"
字符串对象分配内存。by lazy
委托属性:by lazy
委托的属性在首次访问前不会初始化,也就不会占用额外的内存(除了委托对象本身可能占用的少量空间)。例如:
在class LazyClass { val lazyValue: String by lazy { "Lazy initialized value" } }
LazyClass
对象创建时,lazyValue
不会立即初始化,只有在第一次访问lazyValue
时,才会执行lazy
块中的代码,在堆上为"Lazy initialized value"
字符串对象分配内存。之后再次访问lazyValue
,直接返回已初始化的值,不会重新分配内存。这种延迟初始化机制在处理一些资源消耗较大的属性时,可以有效优化内存使用,特别是在某些属性可能根本不会被访问的情况下。
通过深入理解 Kotlin 中属性与字段的各个方面,包括定义、访问器、幕后机制、内存管理以及在继承中的行为等,开发者能够编写出更健壮、高效且易于维护的代码。无论是小型的实用程序还是大型的企业级应用,对这些概念的熟练掌握都是 Kotlin 编程的关键。