目录

Kotlin系列(十三):类的属性与字段

一、前言:

  • 我们在前面学习Kotlin中类的定义以及花式构造函数的使用。
  • 本章节我们将来学习Kotlin中类中属性和字段的定义和使用。

二、属性

  • Kotlin 中的属性(field)比 Java 更加复杂,你没有听错哦,Kotlin中的属性确实要比Java中的属性使用起来要复杂一些。
  • 主要因为以下几点:
  1. Kotlin 中允许 包级属性 的存在,即属性不一定在类里面定义(Java的属性必须都定义在类里面);
  2. 所有非抽象属性都强制要求初始化,没有初始化的属性无法通过编译(除标记为 lateinit var 的属性外),而Java的属性可以不初始化也可以编译通过;
  3. 标准化的 getter 和 setter;
  4. 大量的高级属性修饰符。

1、在类内声明的属性

  • 属性声明语法

      [修饰符] val|var 属性名[: 属性类型][= 初始化语句]
      		[get() = getter 语句]
      		[set() = setter 语句]
    
  • 语法详解:

  1. 在Kotlin中属性不能缺少初始化语句,可以写在定义属性的地方,可以写在 init 语句里,如果都不写就会通不过编译
  1. 标记为 val 的属性不能有自定义的 setter,为什么呢?因为val声明的属性只能赋值一次,既然要控制只能赋值一次,Kotlin当然是要控制在对象创建的时候,这样控制是最简单直接的。所以val类型的字段必须在初始化时赋值,不能带setter字段(不给后面赋值的机会)
  • OK,我们修改一下上章节的demo //ow3d01r1a.bkt.clouddn.com//file/2017/10/f46d6aacdc14470d83fcde057538f022-image.png

  • 可以看到,当我们修改name和age为val时,编辑器直接报错了。

  • 正确的代码修改如下:

      package com.anjie.demo
    
      /**
       * Created by 安杰 on 2017/10/23. */class Person {
      		val name: String;
      		val age: Int = 0;
    
      		init {
      				println("init be called")
      				this.name = "anjie"
    
      		}
    
      		override fun toString(): String {
      				return "Person(name='$name', age=$age)"
      	}
    
      }
    
  • 也就是我们可以在声明属性的时候赋初始值,也可以在init语句里面。

三、getter 和 setter

  • 我们都知道,在Java中按照规范我们应该为每个属性创建setter、getter方法,当然,如果你开心,你也可以不创建,而是把属性声明为public的直接方法。

  • 但是在Kotlin 中,Kotlin标准化了Java中没有固定标准的 getter 和 setter 方法,并且规定调用 Kotlin 类的属性时强制使用 setter 和 getter 方法,不会直接操作类的属性。是不是更安全了呢?

  • Ok,那么我们示例代码来看下Kotlin中的setter和getter如何定义和使用

      package com.anjie.demo
    
      /**
       * Created by 安杰 on 2017/10/23. */class Person {
      		constructor(name: String = "anjie", age: Int = 10) {
      				println("constructor be called")
    
      				this.name = name
      				this.age = age
      		}
    
      		var name: String = ""
      	get() {
      						println("name'get be called value is $field")
      						return field.toString();
      				}
      				set(value) {
      						println("name'set be called value is $value")
      						field = value;
      				};
      		var age: Int = 0
      	set(value) {
      						println("age'set be called value is $value")
      						field = value
    
      				}
      				get() {
      						println("age'get be called value is $field")
      						return field;
      				}
    
      		override fun toString(): String {
      				return "Person(name='${name}', age=$age)"
      	}
    
      }
    
  • 测试代码为:

      @Test
      fun test2() {
      		var person1 = Person();
      		println(person1.toString())
    
      }
    
  • 结果输出为:

      constructor be called
      name'set be called value is anjie
      age'set be called value is 10
      name'get be called value is anjie
      age'get be called value is 10
      Person(name='anjie', age=10)
    
  • 看到这里小伙伴可能很惊讶,前面小编还说Kotlin 不允许直接操作类属性,这不是逗我么。大家稍安勿躁。

  • 上面虽然是使用对象.属性的写法,但是 Kotlin 中的“对象.属性”会视情况自动编译为调用 getter 或 setter 方法。也就是kotlin编译后会自动替换为setter和getter

  • 首先介绍一个字段,field,其代表setter或getter生成器对应的属性字段。

  • 可以看到,即使是在构造函数内对属性进行赋值,其也是强制调用setter函数的。

  • Kotlin有点我不是很理解,就是如果属性设置来setter生成器,你必须对其设置初始值或者在主构造函数对其进行初始化否则就报错不能编译通过,有知道的小伙伴可以给我留言,感激不尽。

  • 当然,我们自定义setter和getter可以做很多事情,如,我对于名字需要做个限制,首字母大写,其他的小写,不管用户输入的是大写还是小写。

  • 代码修改为:

      package com.anjie.demo
    
      /**
       * Created by 安杰 on 2017/10/23. */class Person {
      		constructor(name: String = "anjie", age: Int = 10) {
      				println("constructor be called")
    
      				this.name = name
      				this.age = age
      		}
    
      		var name: String = ""
      	get() {
      						println("name'get be called value is $field")
      						return field.toString();
      				}
      				set(value) {
      						println("name'set be called value is $value")
      						field = if (value.isEmpty()) "" else value[0].toUpperCase() + value.substring(1);
      				};
      		var age: Int = 0
      	set(value) {
      						println("age'set be called value is $value")
      						field = value
    
      				}
      				get() {
      						println("age'get be called value is $field")
      						return field;
      				}
    
      		override fun toString(): String {
      				return "Person(name='${name}', age=$age)"
      	}
    
      }
    
  • 结果输出为:

      constructor be called
      name'set be called value is anjie
      age'set be called value is 10
      name'get be called value is Anjie
      age'get be called value is 10
      Person(name='Anjie', age=10)
    
  • 可以看到,setter的value是 anjie,但最后我们输出的是Anjie,因为我们在setter生成器里面做了格式的转换。

setter、getter使用注意事项

  • 1、需要自定义 setter 或 getter 的属性,不能放在类头里定义,必须在类体内定义,要不然 Kotlin 怎么知道你自定义了哪个属性的 getter 和 setter 呢?所以这里我们把原本在类头里定义的 name 属性挪到了类体内定义,并使用传入的 String 类型参数初始化 name 属性,name 属性被自动推导为 String 类型;

  • 2、getter 是一个没有参数、返回类型与属性类型相同的函数。 完整的写法应该是这样的:

    1
    2
    3
    
    get(): 属性类型 {
      //……
    }
    
  • 但是一般 getter 的方法比较短,而且可以自动推导类型,所以如果只有一句的话可以写成“get() = 函数语句”的形式。需要注意一点,不能在 getter 里再调用本属性,因为 Kotlin 代码里所有对属性的访问都会被编译为 getter 方法,这样写就会出现无限迭代和 StackOverFlowError

  • 3、 setter 的参数列表一般有一个与属性类型相同的参数,没有返回值。Kotlin 中一般用 value 表示这个参数,当然也可以用其他关键字;这里的 field 是表示 幕后字段 的关键字,它在使用时相当于 this.name,但是只能用在 setter 方法内 当然,setter和getter能做的事情很多,大家可以在开发过程中结合实际情况灵活运用。

四、类外属性

  • 在 Kotlin 类外定义的属性有两种:
  • 1、直接写在类外并初始化的包级属性
  • 2、使用 const val 定义的 编译期常量

1、包级属性

  • 在Kotlin中,我们可以在类外面定义包级属性,类外定义的属性会被编译为一个“文件类”的静态变量。

  • 在类外定义的包级属性,Kotlin会将其编译为一个“文件类”的静态变量

  • 我们来看一个示例代码,创建一个User.kt文件,里面定义一个User类:

      package com.anjie.demo
    
      /**
       * Created by 安杰 on 2017/10/23. */
      class User constructor(var name: String)
      var maxID = Int.MAX_VALUE;
    
  • 编译后我们可以发现,Kotlin自动生成了一个

  • 截图如下:

//ow3d01r1a.bkt.clouddn.com//file/2017/10/6c7bd902b4784af8b894434b8e6fe5c3-image.png

  • 反编译为Java代码后如下:

//ow3d01r1a.bkt.clouddn.com//file/2017/10/720ee55f37784a248e76cd67ac275b83-image.png

  • 我们可以看到,其对应的java其实是以文件名+Kt来创建一个类,该类存在一个private 的static的字段。
包级属性的调用
  • 在Kotlin中调用包级属性

      import com.anjie.demo.maxID 
      val a = maxID  
    
  • 代码解析:

  • 因为是包级变量,所以使用“包名.属性名”的方式导入,所以是import com.anjie.demo.maxID

  • 在使用的时候,直接调用

  • 在Java中调用包级属性

      import com.anjie.demo.UserKt; 
      int a = UserKt.getMaxID(); 
    
  • 代码解析:

  • 通过import com.anjie.demo.UserKt; 导入自动生成的“文件类”,然后以Java静态类静态方法的方式直接调用

2、编译期常量

  • 用const 标记的常量我们称之为编译期常量,也就是其只能用val,不能用var。

  • 通过编译器常量,我们能做什么呢?

  • 比如我们希望在java中希望直接以UserKt.maxID的方式调用。

  • 代码可以这样写。

      package com.anjie.demo
    
      /**
       * Created by 安杰 on 2017/10/23. */
      class User constructor(var name: String)
    
      const val maxID = Int.MAX_VALUE;
    
  • 在maxID字段前声明为const类型,我们看下反编译后的java代码

//ow3d01r1a.bkt.clouddn.com//file/2017/10/cabf33dc262b4aa5a8d75d82e7fecff1-image.png

  • 可以看到,其变为了public类型的了,移除了setter、getter方法。
  • 其实,编译期常量其实就是 Java 中常用的常量。但与 Java 中的常量相比,Kotlin 的编译期常量有以下几点限制:
  • 位于顶层或者是 object 的一个成员
  • String 或原生类型 值初始化
  • 没有自定义 getter
  • 还有,Kotlin 中,使用在注解参数中的属性,只能是编译期常量(其他形式的属性都不能使用在注解的参数里)

      const val DEPRECATED_MESSAGE = "This is deprecated."
      @Deprecated(DEPRECATED_MESSAGE) fun foo() {……}
    

五、 延迟初始化属性

  • 一般地,属性声明为非空类型必须在构造函数中初始化。 然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检查。

  • 为处理这种情况,你可以用 lateinit 修饰符标记该属性:

      package com.anjie.demo
    
      /**
       * Created by 安杰 on 2017/10/23. */
      class User constructor() {
      		lateinit var password: String
      		fun initpassword() {
      				password = "password"
      	}
    
      		override fun toString(): String {
      				return "User(password='$password')"
      	}
    
      }
    
  • 测试代码如下:

      @Test
      fun test3(){
      		var user = User();
      		user.password;
      }
      @Test
      fun test4(){
      		var user = User();
      		user.initpassword()
      		println(user.password);
      }
    
  • 结果输出;

  • test3运行报错,因为我们并没有初始化password就调用了其getter方法,

  • test4输出password

  • 注意事项:

  • 该修饰符只能用于在类体中(不是在主构造函数中)声明的 var 属性,并且仅当该属性没有自定义 getter 或 setter 时。该属性必须是非空类型,并且不能是原生类型。

  • 在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。