深入理解 Kotlin 类属性

Kotlin 类属性可以说是 Kotlin 类最难理解和掌握的部分了,变化多、关键字多、限制多,与 Java 差别相当大,很多人都会在这上面栽不少坑,今天我们就一起来深入看看 Kotlin 类属性。

Kotlin 中去除的类属性关键字

Kotlin 中去除了几个用于类属性的关键字,需要用注解实现。

  • transient:这个关键字表示该属性是「瞬时」的,不参与类的序列化。Kotlin 中用 @Transient 标注;

  • volatile:这个关键字用于多线程,可以视为「轻量化的 synchronized」。Kotlin 中用 @Volatile 标注。

@JvmField 的作用

@JvmField 是 kotlin.jvm.JvmPlatformAnnotations.kt 中提供的注解。

要理解 @JvmField 的作用,我们首先要明白一个 Kotlin 类属性编译后会变成什么。我们还是用老朋友 Person 类来举例:

class Person(val name: String)

虽然我们定义一个属性 name,但 Kotlin 并没有允许 Person 类对外(对 Java)暴露这个属性,编译后的 name 属性实际上是这样的:

@NotNull private final String name;
@NotNull public final String getName() {
  return name;
}

我们在外部并不能直接访问 name 属性,一定要通过 getName() 方法间接访问它,这也就是我们最开始就提到过的「标准化 getter / setter」。

但实际使用过程中,我们有时需要将 Kotlin 类的属性直接暴露出来(虽然不利于封装),这时就可以使用 @JvmField 注解:

class Person(@JvmField val name: String)

编译之后 name 属性将直接暴露:

@JvmField @NotNull
public final String name;

private 类属性的细节

用 private 修饰的类属性只能在这个类内部访问,外部无法访问,也就没有提供对外接口的必要。有以下一些细节:

  • private 类属性默认不生成 getter / setter,对它的访问都是直接访问;

  • 一旦 private 属性有自定义的 getter / setter,访问时就要通过 getter / setter 了;

  • @JvmField 注解无法用于 private 类属性,没有必要;

  • private 类属性可以放在主构造函数中定义。

幕后字段和 field

什么是幕后字段?

要理解这个概念,我们得再深一步探究:getter 和 setter 一定要与某个属性相关联吗? 答案当然是否定的。getter 是从对象中获取特定的值,这个值完全可能是每次访问时临时计算的,也可能是从其他对象那里得到的;setter 也可能是在设置其他对象的属性。事实上,Kotlin 只会为满足特定条件的类属性添加 JVM 意义上的类属性

我们为 Person 添加一个 nameHash 属性:

class Person(val name: String) {
  val nameHash get() = name.hashCode()
}

这个 nameHash 属性并没有初始化,只定义了一个 getter 方法。这个属性在编译之后就会消失,只留下一个 getNameHash() 方法:

public final int getNameHash() {
  return name.hashCode();
}

我们可以这样说:「nameHash 是一个没有幕后字段的属性」,它作为类属性只存在于 Kotlin 代码中,并没有一个真正的 JVM 类属性与之对应。怎么给 nameHash 添加一个幕后字段呢?Kotlin 并不允许我们手动添加,但会自动为满足条件的类属性添加幕后字段:

  • 使用默认 getter / setter 的属性,一定有幕后字段。对于 var 属性来说,只要 getter / setter 中有一个使用默认实现,就会生成幕后字段;

  • 在自定义 getter / setter 中使用了 field 的属性,一定有幕后字段。这个 field 就是我们访问幕后字段的「关键字」,它与 Lambda 表达式中的 it 类似,并不是一个真正的关键字,只在特定的语句内有特殊的意思,在其他语句内都不是关键字。

比如,我们可以写:

class Person(val name: String, val gender: Gender, age: Int) {
    val age = age
        get() = when (gender) {
            Gender.FEMALE -> (field * 0.8).toInt()
            Gender.MALE -> field
        }
}

enum class Gender {
    MALE, FEMALE
}

我们这里通过幕后字段实现了 age 属性根据性别的不同行为。(´・ω・`)

虽然我们为 age 属性自定义了 getter,但因为在 getter 中用了 field 关键字,Kotlin 就会为我们生成一个 private final int age 属性作为幕后字段。这个幕后字段同时需要初始化,我们就用主构造函数中传入的 age 来初始化它。

幕后属性大用处

很多时候,我们希望定义这样的属性:

  • 对外表现为 val 属性,只能读不能写;

  • 在类内部表现为 var 属性,也就是说只能在类内部改变它的值。

这在 Kotlin 集合框架中应用十分广泛。比如 Collection 接口定义的 size 属性就是一个 val 属性,对外只读;但对于一个 MutableCollection 来说,size 的值在内部一定是能改变的,也只允许内部修改它。我们可以这样设计:

override val size get() = _size
private var _size: Int = 0

我们在内部增删元素时,改动的就是 _size 属性的值;外部只能访问到 size 属性,不能修改 _size 的值。这里的 _size 就叫做「幕后属性」。

kotlin.collections.AbstractMap.kt 中也用到了幕后属性,我们看一下:

private @Volatile var _keys: Set<K>? = null
override val keys: Set<K> get() {
  if (_keys == null) {
    _keys = object : AbstractSet<K>() {
      override operator fun contains(element: K): Boolean = containsKey(element)

      override operator fun iterator(): Iterator<K> {
        val entryIterator = entries.iterator()
        return object : Iterator<K> {
          override fun hasNext(): Boolean = entryIterator.hasNext()
          override fun next(): K = entryIterator.next().key
        }
      }

      override val size: Int get() = this@AbstractMap.size
    }
  }
  return _keys!!
}

keys 是对外的 API,只读且非空,每次访问都被委托到访问 _keys 属性上,_keys 属性在内部可以改动也可以为 null,给我们带来方便。

在设计类属性时,可以通过幕后属性灵活地实现功能。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值