参考:

类属性

类属性是定义在类中的变量或不变量,分别使用varval定义。这里把Kotlin中的不变量和常量作下区分,不变量只是在第一次初始化后便不可改变,其余特性和变量完全一样,但它不是常量,常量的说明在后面的小节给出。

Kotlin对类属性的完整声明是:

(var|val) <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]

方括号[]表示该部分是可选的,其中属性初始化器、getter、setter在任何情况下都是可选的,如果属性类型可以从属性初始化器或getter中推断出,那么它也是可选的。使用val所声明的不变量是没有setter的。

类属性可以不显式地初始化,但未显式初始化的属性必须在构造器中初始化:

    var allByDefault: Int? // error: explicit initializer required, default getter and setter implied
    var initialized = 1    // has type Int, default getter and setter

    val simple: Int?     // has type Int, default getter, must be initialized in constructor
    val inferredType = 1 // has type Int and a default getter

属性访问器

所谓访问,就是对属性的读与写,类会为每个属性提供默认的访问器。称读访问器为getter,写访问器为setter。对于属性变量两者都会提供,不变量只有getter,显然是由于它不可改变。访问器可以在声明属性时进行重写以替换默认访问器:

    val isEmpty: Boolean
        private get() = this.size == 0

    var stringRepresentation: String
        get() = this.toString()
        @Inject
        set(value) {
            setDataFromString(value) // parses the string and assigns values to other properties
        }

Kotlin中访问器虽然有像get和set方法一样的形式,但它们并不是方法,不能显式地调用。如果存在默认的或重写了的访问器,所有对属性的读和写会都会直接调用访问器,包括在访问器的声明中进行的读写。例如,重写类A的两个变量的getter和setter,IDE会提示递归调用导致栈溢出。读x和调用调用x的getter是等价的,对y的写和调用y的setter是等价的。

    class A {
        var x = 0
            get() {
                return x //recursive getter call
            }

        var y = 0
            set(value) {
                if (value > 0) {
                    y = value //recursive setter call
                }
            }
    }

支撑域(Backing Fields)与支撑属性(Backing Properties)

上面这个例子说明访问器有问题,用Java写POJO类时通常用IDE生成set和get方法就完事了,但有时候也会在这些方法中添加一些额外的逻辑对变量作进一步的操作。但上面的示例告诉我们,在Kotlin中这似乎做不到,因为我们拿不到真正的实例变量,只能通过访问器对变量进行操作,然而直接访问变量的需求还是在的,Kotlin也并不想让人抓狂,给出了两个解决方法,那便是支撑域和支撑属性。

Kotlin明确地告诉我们它是没有域(fields)的,Java中我们一直称对象的变量为域(field),而Kotlin一直称之为属性(property),那么这两者有什么区别呢?

在面向Google与面向爆栈编程实践中,我搜到了关于这个问题的一个问答和一份Java官方文档中对这些词汇的说明:

  • field

    A data member of a class. Unless specified otherwise, a field is not static.

  • instance variable

    Any item of data that is associated with a particular object. Each instance of a class has its own copy of the instance variables defined in the class. Also called a field. See also class variable.

  • class variable

    A data item associated with a particular class as a whole–not with particular instances of the class. Class variables are defined in class definitions. Also called a static field. See also instance variable.

  • property

    Characteristics of an object that users can set, such as the color of a window.

也就是说除非特别声明,域指的是类的实例变量(非静态的类变量),属性的概念说的比较抽象,它是一个用户可以改变的对象特征。可以这样理解,由于域可以是私有的和公开的,而上面property词条表明属性是公开的,那么定义为private的域可以通过声明public的get或set方法使其成为对象的一个属性。那么简而言之可以认为属性本质上而言是可以从对象外部公开访问的域。这反应了Kotlin的一种设计意图,即一致性。和Java域相比Kotlin属性的语义约束性更强,Kotlin属性是对外公开的。Kotlin的属性提供了这样一种一致性,即对象的属性只能通过访问器访问。

所以Kotlin声称它的类没有域,即不能直接访问对象的实例变量。对于public变量,编译器会自动生成访问器,任何对变量的读写,包括访问器自身内部都使用访问器来进行。

那么类中的private变量算什么呢?根据上述属性的一般性概念,可以认为是的,private实例变量不属于属性,它仅是类内部的一种状态,如果该变量没有明确声明访问器,编译器也不会为其默认生成,此时对变量的访问是直接的,对Kotlin编译出的字节码文件的观察也证实了这一点。但如果为private变量明确声明访问器又会是什么情况?为了保持该变量是对象私有的这种一致性,不允许像Java一样声明public的getter和setter来绕开私有可见性的声明,但不妨碍通过声明其它public方法来访问该变量,而这与对象方法可以访问对象的所有成员是一致的。为了保证这种一致性,这种情况下对该实例变量的读写将分别通过对应的访问器进行访问。

这种设计原由可以这样理解,在对象内部,对于提供了访问器的私有实例变量而言,访问器是对象中其它成员访问它的唯一途径,那么相对于其它成员而言,它自身是也一个对象,它的值就是它的属性(这里的意思和上面英文词条中的一样,即用户可以改变的对象的特征),这里用户成了对象内部的其它成员,这个可以改变的特征就是它的值,即它的值就是它的属性(Kotlin中的属性),而它的值就是它自身,所以它成了一个属性,这里要加上一个必要条件,即相对对象内部成员。

那么现在可以将两种情况统一起来,可以认为属性就是指提供了访问器的实例变量

那么当真正需要使用域时该怎么办。Kotlin提供了一个自动支撑域,可以通过field标识符在访问器中使用。

    var counter = 0 // the initializer value is written directly to the backing field
        set(value) {
            if (value >= 0) field = value
        }

但支撑域只能在访问器中使用,如果想像Java那样自由地在任何地方访问原始实例变量又该怎么办,即不通过访问器读写实例变量?从上面的分析中可以看到,这样的变量不是属性,声明一个没有访问器的实例变量就可以了,通过将变量声明为private并且不重写其访问器就可以了,kotlin称其为支撑属性。如下所示:

    private var _table: Map<String, Int>? = null
    public val table: Map<String, Int>
        get() {
            if (_table == null) {
                _table = HashMap() // Type parameters are inferred
            }
            return _table ?: throw AssertionError("Set to null by another thread")
        }

懒初始化

上面的类属性必须在声明时或者构造器中进行初始化,但这样有许多不便之处,例如使用依赖注入进行对象初始化或者单元测试中不能提供一个非空的初始化器,但你仍然想在引用属性时避免null检查。此时,可以使用lateinit关键词修饰属性:

    public class MyTest {
        lateinit var subject: TestSubject

        @SetUp fun setup() {
            subject = TestSubject()
        }

        @Test fun test() {
            subject.method()  // dereference directly
        }
    }

它只能用于属性变量,并且属性没有自定义访问器,属性的类型必须是不可为null的,并且不能是基础数据类型。

编译时常量

使用const关键词修饰,并且满足以下条件的属于编译时常量:

  • 对象的顶级成员
  • 使用String或基本类型进行初始化
  • 没有自定义getter
    const val constant: String = "Constant string."