Kotlin 属性与字段

声明属性

Kotlin 类中的属性既可以用关键字 var 声明为可变的,也可以用关键字 val 声明为只读的。

  1. class Address {
  2. var name: String = "Ricky,Lee";
  3. var street: String = "Panjiayuan"
  4. var city: String = "BeiJing"
  5. var zip: String = "100000"
  6. }

如果要使用一个属性,只要用名称引用它即可:

  1. fun copyAddress(address: Address): Address {
  2. val copyResult = Address()
  3. copyResult.name = address.name
  4. copyResult.street = address.street
  5. copyResult.city = address.city
  6. copyResult.zip = address.zip
  7. return copyResult
  8. }

Getters 与 Setters

声明一个属性的完整语法是:

  1. var <propertyName>[: <PropertyType>] [= <property_initializer>]
  2. [<getter>]
  3. [<setter>]

其初始器(initializer)、getter 和 setter 都是可选的。属性类型如果可以从初始器 (或者从其 getter 返回值)中推断出来,也可以省略。

只读属性的语法和可变的属性的语法有两方面的不同:

  1. 只读属性的用 val开始代替var
  2. 只读属性不允许 setter

如果我们不显式的添加 setter/getter ,Kotlin 会默认添加,如下就是默认的 setter/getter:

  1. class Person {
  2. var name: String? = null
  3. //以下两种getter都可以
  4. // get() = field
  5. get() {
  6. return field
  7. }
  8. set(value) {
  9. field = value
  10. }
  11. val age: Int? = null
  12. // get() = field
  13. get() {
  14. return field
  15. }
  16. }

虽然 Kotlin 不提倡在代码中显式的添加默认的 setter/getter ,但是我们可以根据需求自定义 setter/getter 。

幕后字段(Backing Fields)

在上面代码中,我们用到一个关键字 field ,它一般只会在 setter/getter 中使用。它代表就是当前的变量。

  1. class Person {
  2. // 在初始化器中给变量赋值,无法触发 setter
  3. var name: String? = null
  4. // value 代表的是当前 name 的值
  5. set(value) {
  6. // field 代表的就是 name
  7. field = if (value == "LiTeng") {
  8. "Ricky"
  9. } else {
  10. "Ricky.Lee"
  11. }
  12. }
  13. val age: Int? = null
  14. get() {
  15. return if (name == "Ricky") {
  16. 20
  17. } else {
  18. field
  19. }
  20. }
  21. }
  22. fun main() {
  23. val person = Person()
  24. // 动态的给变量赋值,会触发 setter
  25. person.name = "LiTeng"
  26. println("name:${person.name}")
  27. println("age:${person.age}")
  28. }

上面的代码输出:

  1. name:Ricky
  2. age:20

通过上面的代码,我们可以看出,通过setter我们可以在变量赋值的时候改变变量的值。使用 val 修饰的变量并不是不可变的,我们可以通过自定义的 getter 动态改变其值。

编译期常量

在编译器在编译的时候就知道这个变量在实际运行时的具体值,这个变量就叫做编译期常量。我们一般使用 const 修饰编译器常量,其后面必须跟 val

编译期常量需要满足以下条件:

  1. 位于顶层或者使用 object声明或 companion object 的一个成员
  2. 必须是基本类型值或者String
  3. 没有自定义 getter

常量的写法

1.使用Kotlin 的顶层声明(top-level declaration)设计

所谓的顶层声明,就是把属性和函数声明直接写在 kt 文件中,而不是写在 class 中。

  1. package vip.liteng.kotlin.sample.classes
  2. const val TOP_LEVEL = "top"
  3. fun topLevelFun(){
  4. /* .... */
  5. }

这样写的属性和函数,不属于任何 class,而是直接属于 package,它和静态变量、静态函数一样是全局的。如果我们要使用这些属性和函数,只要我们导入对应的属性和函数,就可以在项目中的任意地方直接使用属性名或者函数名就可以直接调用。

  1. import vip.liteng.kotlin.sample.classes.TOP_LEVEL
  2. import vip.liteng.kotlin.sample.classes.topLevelFun
  3. fun main() {
  4. println(TOP_LEVEL)
  5. topLevelFun()
  6. }

2.使用 object声明或 companion object

使用 object 在 Kotlin 就是其本意,对象的意思。而在代码中它的意思是创建一个类,并且创建一个这个类的对象 。如下代码:

  1. object Car {
  2. // object 修饰的类中的变量默认为 编译期常量
  3. const val name: String = "黑色"
  4. }
  5. fun main() {
  6. // 我们可以使用 类名.属性 直接调用
  7. Car.name
  8. }

其实上面的代码就是一种饿汉式的单例形式。在 Kotlin 中只需使用 object 就可以很简单的创建一个单例,并且是线程安全的。

我们将上面的代码转换为 java 代码:

  1. public final class Car {
  2. // 使用 const val 声明的常量,直接被转换为 static final 的常量
  3. @NotNull
  4. public static final String name = "黑色";
  5. public static final Car INSTANCE;
  6. private Car() {
  7. }
  8. static {
  9. // 在静态代码块中直接创建了对象。
  10. Car var0 = new Car();
  11. INSTANCE = var0;
  12. }
  13. }

在 Java 更直观的看到,在静态代码块中直接创建了对象,并且我们使用 const val 声明的常量,直接被转换为 static final 的常量。

同样的我们使用伴生对象 companion object 跟上面的类似,但是一个类只能有一个伴生对象;并且 伴生对象必须声明类内;不像 object 也可以在 顶层声明中使用。

  1. class Car{
  2. companion object A {
  3. const val name: String = "黑色"
  4. }
  5. }
  6. fun main() {
  7. // 伴生对象可以像下面的方式通过 类名.伴生对象名.常量名 调用;但是其中伴生对象名是多余的,一般我们要忽略,直接使用下面的 类名.常量名 方式调用
  8. Car.A.name
  9. // 直接使用 类名.常量名 的方式调用
  10. Car.name
  11. }

延迟初始化属性与变量

有时候我们声明的变量,没有办法在声明的第一时间给它赋值,那么我们还可以使用 延迟初始化修饰符 lateinit

  1. // 使用 lateinit 无需对变量在声明时进行初始化,可以在需要的时候进行初始化
  2. lateinit var fileName: String
  3. fun main() {
  4. fileName = "/opt/home"
  5. }

延迟初始化修饰符 lateinit 只能修饰在类体中的属性,并且修饰使用 var 变量。 在 Kotlin 1.2 只有 lateinit 也可以修饰 顶层变量 和 局部变量。该属性或变量必须为非空类型,并且不能是原生类型。

在初始化前访问一个 lateinit 属性会抛出 kotlin.UninitializedPropertyAccessException

比如:

  1. lateinit var AB: String
  2. fun main() {
  3. println(AB)
  4. }

上面的代码会抛出以下的异常:

  1. Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property AB has not been initialized
  2. at vip.liteng.kotlin.sample.classes.PropertyAndFieldKt.main(PropertyAndField.kt:81)
  3. at vip.liteng.kotlin.sample.classes.PropertyAndFieldKt.main(PropertyAndField.kt)

检测一个 lateinit var 是否已初始化(自 1.2 起)

如果要检测 lateinit var 是否已经初始化,可以在该属性的引用上使用.isInitialized

  1. lateinit var AB: String
  2. fun main() {
  3. if (::AB.isInitialized) {
  4. println("AB 已经初始化")
  5. }else{
  6. println("AB 还没有初始化")
  7. }
  8. }

上面的方法只能对同一个类型内、位于其中一个外围类型中或者位于相同文件的顶层的属性。

总结:

  1. 使用 var 声明可变变量;使用 val 声明只读变量;
  2. var 变量存在 getter/setter ;而 val 变量只存在 getter,并且可以通过 getter 改变 val 变量的值;
  3. 要使用一个属性,只要用名称引用它即可;
  4. 常量使用 const val 修饰;
  5. 常量可以在 top-level 声明;
  6. 常量可以在 objectcompanion object 中声明;且默认为 const val 类型;
  7. 如果不想在声明的时候给变量赋值,可以使用 lateinit var 修饰;
  8. 如果lateinit var 修饰的变量没有初始化,在使用时户抛出异常