一、属性声明
权限修饰符 var|val 属性名称:属性类型 = 初始化语句
- Kotlin中非抽象的属性必须初始化,因为空类型安全。
- 属性不能缺少初始化语句,要么在定义属性的地方、要么在构造中、要么在 init{ } 代码块里,否则会编译错误。
- 声明为 val 的属性不能有自定义的 set() 语句,因为不能再次赋值。
- 成员属性初始化顺序:主构造 > 声明处 = init{ } > 次构造。属性声明和init按顺序执行。
- 属性在直接或间接调用前,一定要完成初始化。
//id在主构造中初始化
class Demo(var id: Long, grade: Int) {
//grade在主构造中初始化
var grade = grade
//age在声明处初始化
var age = 18
//name在 init{} 中初始化
var name: String
init {
= "张三"
}
//hobbit在次构造中赋值
var hobbit: String? = null
constructor() : this(123L, 5) {
= "打球"
}
}
一些调用前未初始化的错误示范:
class Demo(str: String) {
//直接调用属性(编译器会提示)
init {
val age = num + 4 //提示num需要初始化
}
val num = 100
//间接调用属性1(编译器不会提示)
private val name: String
private fun show() = name[0]
init {
print(show()) //初始化前就调用了
name = "jack"
}
//间接调用2(编译器不会提示)
val text2: String = initText() //text1初始化前就调用了
val text1: String = str
private fun initText() = text1
}
二、幕后字段 Backing Field
在Kotlin中属性作为一级语言特性,通常情况下集幕后字段(field储值变量)+ 访问器(getter读访问器、setter写访问器)于一身,无论是声明【var age: Int】、赋值【 = 18】、取值【println()】,从字面上看都是 age 这个属性本身,是一个整体。而只有在我们需要自定义访问器的时候才会区分这三者。
例如自定义 setter 的时候,如果不写成幕后字段 field = value,不管是 age = value 还是 = value 都会报错,因为属性 age 的赋值就是setter,显然不能递归调用。
如果属性的访问器至少有一个使用默认实现,或者自定义的访问器中使用了 field ,那么就会提供幕后字段,用 field 关键字表示,主要用于自定义 getter/setter 时使用,也只能在 getter/setter 中访问。
class Demo{
var id: Long = 0
get() = field
set(value) { field = value }
}
三、访问器 getter & setter
- 一般用于让属性在不同条件下有不同值。
- 使用 var 修饰的属性默认拥有 getter 和 setter,使用 val 修饰的属性默认只有 getter。可见性默认和字段的可见性一致,也可以自定义。
- 使用 实例.属性名 的写法会自动编译为 getter 或 setter,同时允许自定义 getter 和 setter,只能在类体中定义。
- getter 没有参数列表,返回值类型与属性类型相同因此可以自动推导不写。不能在 getter 里再调用本属性,会出现无限循环导致堆栈溢出错误。
- setter 是一个没有返回值的函数。单个参数的时候一般使用 value 表示(非必须),field 表示本属性的值(相当于this.本属性名)。
- 属性初始化后,自定义 getter/setter 里才能使用 field。属性未初始化,getter&setter 必须手动声明且不能使用 field。
class Demo() {
//默认自动生成
var id: Long = 0
get() = field
set(value) { field = value }
//后接等号表达式或值
var age = 18
get() = 20
set(value) = if (value > 0) field = value else field = 0
//后接大括号语句
var name = "haha"
get() { return "getter" }
set(value) { field = "settet" + value }
}
四、类外的属性
在类外定义的属性是包级的,该属性会被编译为单独的一个类(原类名Kt)。单独想调用该类的话,导包后Kotlin直接使用字段,Java调用getXXX()同名方法。
//
val number = 10086 //类体外定义的包级属性
class Demo(){}
//编译会生成一个DemoKt类,反编译后内容如下
public final class DemoKt{
private static final int number = 10086; //number被编译为这个类的私有静态字段
public static final int getNumber(){ //并拥有一个默认的getXXX()同名方法
return number;
}
}
//Koltin使用
import //因为是包级变量,使用 包名.属性 名方式导入
val b = number //直接使用字段
//Java使用
import ; //导入自动生成的class
int b = (); //使用默认的getXXX()同名方法
编译期常量 const val 定义在类外,反编译后可以看到它就是Java中的常量,但是有几点限制:
- 只能定义在类外或对象(Object)内。
- 只能使用 String 或 基本数据类型 初始化。
- 不能自定义 getter(不需要可直接调用)。
//
const val number = 10086
class Demo(){}
//反编译内容如下:
public final class DemoKt{
public static final number = 10086;
}
Kotlin中使用在注解参数中的属性,只能是编译期常量(其他形式的属性都不能使用在注解的参数里)。
const val DEPRECATED_MESSAGE = "This is deprecated."
@Deprecated(DEPRECATED_MESSAGE) fun foo() {……}
五、延迟初始化 lateinit
当有些属性不需要在声明的时候初始化(局部变量抽成类中全局变量如findViewById的时候)或者需要外部来初始化,可以使用 latrinit 来延迟初始化。
- 使用 lateinit 修饰属性后,编译器不会再做非空检查,不初始化也不会报错,所以需要做到自己可控(无论是在类中还是类外要对属性赋值),否则在使用之前未初始化,调用会报错 UninitializedPropertyAccessException。
- 不可修饰基本数据类型和可空类型。因为非空的数据类型可用 null 标记未初始化并在访问的时候引发异常,而原生数据类型和可空的类型没办法用null标记。
- 不能有自定义的 getter/setter。
- 1.2以后可以定义在主构造和局部,尽量还是在类中定义。
- lateinit 只能用在 var 类型上,并可在任意位置初始化多次,有幕后字段(Backing Fields)。
初始化方式 | 适用的数据类型 | 缺点 |
声明属性的时候就给一个默认值 | 基本类型 | |
使用() | 基本类型、引用类型 | ①属性初始化必须在属性使用之前,否则报错 ②不支持外部注入工具将它直接注入到Java字段中 |
使用 lateinit | 引用类型 | 属性初始化必须在属性使用之前,否则报错 |
class Demo() {
//lateinit初始化
lateinit var name: String
fun initName(){ name = "张三" } //函数名可以随便取
fun show() = :: //检查是否初始化
//by lazy初始化
val id: Long by lazy {
println("初始化:" + ())
123L
}
}
fun main() {
val aa = Demo()
println(()) // 打印:false
}
六、懒加载
关联知识点 :属性委托
- val属性:本身只读,懒加载用 by lazy。详见:by是委托,lazy是高阶函数
- var属性:使用 private 修饰 getter 实现外部只读,懒加载可在 getter 中实现。
class Demo {
//var属性
var num: MutableLiveData<Int>? = null
get() {
println("打印就说明var加载了")
if (field == null) { //实现懒加载
field = MutableLiveData<Int>(0)
}
return field
}
private set //对外暴露只读
//val属性,本身只读,使用 by lazy懒加载
val num2 by lazy {
println("打印就说明val加载了")
MutableLiveData<Int>()
}
}
val model = Demo() //什么也不会打印
七、后备属性(幕后属性)
类似于 Java 类中私有化成员变量并提供公共访问的写法(封装)。
如果一个类有两个概念上相同的属性,一个是公共 API 的一部分,另一个是实现细节,那么使用下划线作为私有属性名称的前缀。
幕后属性 _num 是私有的,它用来存储一个MutableLivedata,再定义一个 num 用来提供外部对 _num 的访问。
private var _num = mutableLiveData<Int>(0) //幕后属性用于私有化属性
val num = _nam as LiveData<Int> //val让对象只读,as强转让对象内数据不可变