MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin属性与字段

2023-02-216.9k 阅读

Kotlin 属性基础

在 Kotlin 中,属性是一种重要的编程概念,它提供了一种便捷的方式来封装数据,并通过访问器来控制对数据的访问。属性可以是可变的(var)或只读的(val)。

定义属性

  1. 只读属性(val: 只读属性一旦初始化后就不能再被重新赋值。例如,定义一个表示人的名字的只读属性:
    class Person {
        val name: String = "John"
    }
    
    在上述代码中,name 属性是只读的,类型为 String,初始值为 "John"。创建 Person 实例后,不能再修改 name 的值。
  2. 可变属性(var: 可变属性允许在初始化后重新赋值。比如定义一个表示人的年龄的可变属性:
    class Person {
        var age: Int = 30
    }
    
    这里 age 属性可以在 Person 实例的生命周期内进行修改,例如:
    fun main() {
        val person = Person()
        person.age = 31
        println(person.age)
    }
    
    上述代码将 personage 属性从 30 修改为 31 并打印出来。

属性的访问器

  1. 自定义访问器: 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 是通过 widthheight 相乘得到的。
  2. 可变属性的 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)
        }
    }
    
    上述代码先设置 temperaturevalue 为 25.0 并打印,然后尝试设置为 -300.0,会捕获到异常并打印错误信息。

幕后字段(Backing Fields)

在 Kotlin 中,当属性需要一个实际存储值的地方时,就会用到幕后字段。

field 标识符

  1. 基本用法: 当我们需要在访问器中直接访问属性的存储位置时,就使用 field 标识符。例如,在前面 Temperature 类的 set 访问器中,我们使用 field = newValue 将新值赋给实际存储的位置。field 只能在属性的访问器内部使用。 再看一个更简单的例子,统计属性被读取的次数:
    class Counter {
        var count: Int = 0
            get() {
                field++
                return field
            }
    }
    
    Counter 类中,每次读取 count 属性时,field 会自增并返回。使用时:
    fun main() {
        val counter = Counter()
        println(counter.count)
        println(counter.count)
    }
    
    上述代码会先打印 1,再打印 2,因为每次读取 count 时,field 都自增了。
  2. field 与自定义访问器的关系: 只有当属性有自定义访问器时,field 才会被自动生成。如果属性没有自定义访问器,Kotlin 会自动生成默认的访问器,并且不会生成 field。例如:
    class Simple {
        val simpleValue: String = "Hello"
    }
    
    这里 simpleValue 没有自定义访问器,所以不存在 field。如果尝试在代码中使用 field,会导致编译错误。

延迟初始化的幕后字段

  1. 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),并且属性类型不能是可空类型或基本类型(如 IntDouble 等)。
  2. 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 块中的代码进行初始化。使用时:
    fun main() {
        val provider = DataProvider()
        println("Before accessing expensiveValue")
        println(provider.expensiveValue)
        println("After accessing expensiveValue")
    }
    
    上述代码会先打印 "Before accessing expensiveValue",然后等待 2 秒(模拟耗时操作),再打印 "Expensive result",最后打印 "After accessing expensiveValue"。

幕后属性(Backing Properties)

幕后属性是 Kotlin 中一种通过自定义委托来实现属性存储和访问逻辑的机制。

自定义委托实现幕后属性

  1. 定义委托类: 假设我们要实现一个属性,它的值在设置时会被转换为大写。首先定义一个委托类:
    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 方法用于设置属性的值,并且在设置值时将其转换为大写。
  2. 使用委托类作为幕后属性: 然后在另一个类中使用这个委托类:
    class MyClass {
        var myString: String by UpperCaseDelegate()
    }
    
    MyClass 类中,myString 属性通过 UpperCaseDelegate 委托来实现存储和访问逻辑。使用时:
    fun main() {
        val myClass = MyClass()
        myClass.myString = "hello"
        println(myClass.myString)
    }
    
    上述代码会打印 "HELLO",因为 myString 属性在设置值时被转换为大写了。

Kotlin 标准库中的幕后属性委托

  1. Delegates.observableDelegates.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
    
  2. Delegates.vetoableDelegates.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 的值才会被更新。使用时:
    fun main() {
        val vetoableClass = VetoableClass()
        vetoableClass.vetoableValue = 1
        println(vetoableClass.vetoableValue)
        vetoableClass.vetoableValue = 0
        println(vetoableClass.vetoableValue)
    }
    
    上述代码会打印 1 和 1,因为第二次尝试将 vetoableValue 设置为 0 时,由于不满足 newValue > oldValue 的条件,设置被否决,值没有改变。

伴生对象中的属性

在 Kotlin 中,伴生对象可以包含属性,这些属性类似于 Java 中的静态属性。

伴生对象属性的定义与使用

  1. 定义伴生对象属性: 例如,定义一个包含伴生对象属性的类:
    class MathUtils {
        companion object {
            val PI: Double = 3.141592653589793
            var count: Int = 0
        }
    }
    
    MathUtils 类中,伴生对象定义了一个只读属性 PI 和一个可变属性 count
  2. 使用伴生对象属性: 使用伴生对象属性时,不需要创建类的实例。例如:
    fun main() {
        println(MathUtils.PI)
        MathUtils.count++
        println(MathUtils.count)
    }
    
    上述代码直接通过类名访问伴生对象的属性,先打印 PI 的值,然后增加 count 的值并再次打印。

伴生对象属性与普通属性的区别

  1. 内存模型: 普通属性是每个类实例都有一份独立的拷贝,而伴生对象属性是类级别的,所有实例共享。例如:
    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)
    }
    
    上述代码中,instance1instance2instanceProperty 值不同,因为它们是各自实例的属性;而 companionProperty 值相同,因为它是伴生对象属性,所有实例共享。
  2. 访问方式: 普通属性通过实例访问,而伴生对象属性通过类名访问。这种访问方式的差异也体现了它们在类结构中的不同角色。普通属性用于表示每个实例特有的状态,而伴生对象属性更适合表示类级别的常量或共享状态。

继承中的属性

在 Kotlin 继承体系中,属性的行为有其独特的规则。

属性的覆盖

  1. 重写只读属性: 当子类继承自父类时,可以重写父类的属性。例如,定义一个父类和子类,子类重写父类的只读属性:
    open class Animal {
        open val species: String = "Unknown"
    }
    class Dog : Animal() {
        override val species: String = "Canis lupus familiaris"
    }
    
    在上述代码中,Animal 类定义了一个 species 只读属性,Dog 类继承自 Animal 并通过 override 关键字重写了 species 属性。使用时:
    fun main() {
        val dog = Dog()
        println(dog.species)
    }
    
    上述代码会打印 "Canis lupus familiaris",表明子类成功重写了父类的属性。
  2. 重写可变属性: 可变属性同样可以被重写。例如:
    open class Shape {
        open var color: String = "Black"
    }
    class Rectangle : Shape() {
        override var color: String = "Red"
    }
    
    Shape 类中定义了 color 可变属性,Rectangle 类继承自 Shape 并重写了 color 属性。使用时:
    fun main() {
        val rectangle = Rectangle()
        println(rectangle.color)
        rectangle.color = "Blue"
        println(rectangle.color)
    }
    
    上述代码先打印 "Red",然后将 color 修改为 "Blue" 并再次打印。

属性访问器的重写

  1. 重写 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 访问器。使用时:
    fun main() {
        val child = Child()
        println(child.message)
    }
    
    上述代码会打印 "Child message"。
  2. 重写 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,由于不满足 PositiveNumberHolderset 访问器的条件,number 值不变,再次打印仍为 5。

属性与字段的内存管理

在 Kotlin 中,理解属性和字段的内存管理对于编写高效的代码至关重要。

基本类型属性的内存管理

  1. 栈内存分配: 对于基本类型(如 IntDoubleBoolean 等)的属性,如果它们是局部变量或者类的成员属性且没有被装箱(例如在泛型中使用基本类型会导致装箱),它们通常会在栈上分配内存。例如:
    class BasicTypeClass {
        var intValue: Int = 10
    }
    
    BasicTypeClass 类中,intValue 属性是 Int 类型,它在对象实例创建时,会在堆上的对象内存布局中为 intValue 分配 4 个字节(假设是 32 位系统)的空间。如果 intValue 是一个局部变量,它会直接在栈上分配空间。
  2. 内存释放: 当对象不再被引用(例如超出作用域或者被显式设置为 null,对于可空引用类型),相关的内存会被垃圾回收机制回收。对于基本类型属性所在的对象,如果没有其他引用指向该对象,垃圾回收器会回收该对象占用的堆内存,包括其中基本类型属性占用的空间。

引用类型属性的内存管理

  1. 堆内存分配: 引用类型(如 String、自定义类等)的属性,其引用本身(类似于指针)会和基本类型属性一样,在对象内存布局中分配空间(通常是 4 字节或 8 字节,取决于系统架构),而实际的对象数据则分配在堆上。例如:
    class ReferenceTypeClass {
        var stringValue: String = "Hello"
    }
    
    ReferenceTypeClass 类中,stringValue 属性的引用在 ReferenceTypeClass 对象实例的内存布局中,而 "Hello" 字符串对象则在堆上。
  2. 内存释放与引用计数: Kotlin 使用垃圾回收机制来管理引用类型对象的内存释放。当一个引用类型对象没有任何活跃的引用指向它时,垃圾回收器会标记该对象为可回收,并在适当的时候回收其占用的堆内存。有些垃圾回收算法(如引用计数法)会为每个对象维护一个引用计数,当引用计数为 0 时,对象就可以被回收。虽然 Kotlin 可能不完全依赖引用计数法,但理解这种机制有助于理解内存释放的基本原理。例如,如果一个类中有多个引用类型属性,并且这些属性引用的对象之间存在复杂的引用关系,需要确保在不再需要这些对象时,正确地切断引用,以便垃圾回收器能够回收它们的内存。

延迟初始化属性的内存管理

  1. lateinit 属性lateinit 属性在未初始化时不会占用额外的内存(除了属性引用本身的空间)。例如:
    class LateinitClass {
        lateinit var lateinitValue: String
    }
    
    LateinitClass 对象创建时,lateinitValue 只是一个未初始化的引用,不会为 String 对象分配内存。只有在调用 lateinitValue = "Some value" 进行初始化时,才会在堆上为 "Some value" 字符串对象分配内存。
  2. by lazy 委托属性by lazy 委托的属性在首次访问前不会初始化,也就不会占用额外的内存(除了委托对象本身可能占用的少量空间)。例如:
    class LazyClass {
        val lazyValue: String by lazy {
            "Lazy initialized value"
        }
    }
    
    LazyClass 对象创建时,lazyValue 不会立即初始化,只有在第一次访问 lazyValue 时,才会执行 lazy 块中的代码,在堆上为 "Lazy initialized value" 字符串对象分配内存。之后再次访问 lazyValue,直接返回已初始化的值,不会重新分配内存。这种延迟初始化机制在处理一些资源消耗较大的属性时,可以有效优化内存使用,特别是在某些属性可能根本不会被访问的情况下。

通过深入理解 Kotlin 中属性与字段的各个方面,包括定义、访问器、幕后机制、内存管理以及在继承中的行为等,开发者能够编写出更健壮、高效且易于维护的代码。无论是小型的实用程序还是大型的企业级应用,对这些概念的熟练掌握都是 Kotlin 编程的关键。