前言:人生只有走出来的美丽,没有等出来的辉煌。
一、概述
前面已经为大家讲解了类的使用以及属性的相关知识,在一个类中基本上都会出现属性和字段的,属性在变量和常量的文章中有详细讲解到,这里会重新简单介绍到。
1.1 声明属性
Java 类中的变量声明为成员变量,而 Kotlin 中声明为属性,Kotlin 类中的属性可以使用
var
关键字声明为可变,也可以使用
val
关键字声明为只读。类中的属性必须初始化值,否则会报错。
class Person {
val id: String = "0" //不可变,值为0
var nameA: String? = "Android" //可变,允许为null
var age: Int = 22 //可变,非空类型
}
前面有提到 Kotlin 能有效解决空指针问题,实际上定义类型时增加了可空和非空的标志区分,如上面声明的类型后面有
?
表示属性可为空,类型后面没有有
?
表示属性不可为空。如
name: String?
中 name 可以为
null
,
age: Int
中 age 不可为
null
。在使用时,编译器会根据属性是否可为空做出判断,告知开发者是否需要处理,从而避免空指针异常。
Kotlin 中使用类中的属性和 Java 中的一样,通过类名来引用:
var person = Person()//实例化person,在Kotlin中没有new关键字
view1.text = person.id //调用属性
view2.text = person.nameA
实际上上面定义的属性是不完整的,在 Java 中的属性定义还会涉及到
get()
和
set()
方法,那么在 Kotlin 怎么表示呢?
二、Getter()与Setter()
Kotlin 中
getter()
对应 Java 中的
get()
函数,
setter()
对应 Java 中的
set()
函数,不过注意这仅仅是 Kotlin 的叫法而已,真正的写法还是
get()
和
set()
。
2.1 完整语法
在 Kotlin 中普通类中一般不提供
get()
和
set()
函数,因为普通的类中基本用不到,这点和 Java 相同,但是 Java 在定义纯粹的数据类时,会用到
get()
和
set()
函数,但是 Kotlin 这种情况定义了数据类,已经为我们实现了
get()
和
set()
函数。
声明属性的完整语法如下:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
这是官方的标准语法,我们来翻译一下:
var <属性名> : <属性类型> = 初始化值
<getter>
<setter>
其中,初始化器(
property_initializer
),getter 和 setter 都是可选的,如果可以从初始化器(或者 getter 返回类型)推断出属性的类型,那么属性类型(
PropertyType
)是可选的,如下所示:
//var weight: Int?//报错,需要初始化,默认getter和setter方法是隐藏的
var height = 172 //根据初始化值推断类型为Int,属性类型可以不需要显示,默认实现了getter和setter方法
只读属性声明与可变属性声明不同,它是
val
开始而不是
var
,不允许设置 setter 函数,因为它只是只读。
val type: Int?//类型为Int,必需初始化,默认实现了getter方法
val cat = 10//类型为Int,默认实现getter方法
init {//初始化属性,也可以在构造函数中初始化
type = 0
}
Kotlin 中属性的 getter 和 setter 函数是可以省略的,系统有默认实现,如下:
class Person {
//用var修饰时,必须为其赋初始化值,即使有getter()也必须初始化,不过获取的数值是getter()返回的值
var name: String? = "Android"
get() = field //默认实现方式,可省略
set(value) { //默认实现方式,可省略
field = value //value是setter()方法参数值,field是属性本身
}
}
其中,field 表示属性本身,后端变量,下面会详细讲到,value 是
set()
的参数值,也可以修改你喜欢的名称。
set(value){field = value}
的意思是
set()
方法将设置的参数 value 赋值给属性 field,上面的
getetter()
与
setter()
均为默认实现方式,可以省略。
2.2 自定义
上面的属性我们都省略了 getter 和 setter 方法。我们可以为属性定义访问器,自定义
getetter()
与
setter()
可以根据自身的实际情况来制定方法值的规则。好比如 Java 中自定义 get() 和 set() 方法。
(1)
val
修饰的属性的
getter()
函数自定义
如果定义一个自定义 getter,那么
getter()
方法会在属性每次被访问时调用,下面是自定义 getter 的例子:
//用val修饰时,用getter()函数属性指定其他值时可以不赋默认值,但是不能有setter()函数,等价 val id: String = "0"
val id: String
get() = "0" //为属性定义get方法
如果属性的类型可以从 getter 方法中推断出来,那么类型可以省略:
class Person {
//color根据条件返回值
val color = 0
get() = if (field > 0) 100 else -1 //定义get方法
//isEmpty属性是判断 color是否等于0
val isEmpty get() = this.color == 0 //Boolean类型,getter方法中推断出来,可省
}
//调用
var person = Person()
Log.e(TAG, "get()和set(): color == ${person.color} | isEmpty == ${person.isEmpty}")
color
默认为0,
get()
的数据为-1,isEmpty 为 false,打印数据如下:
get()和set(): color == -1 | isEmpty == false
(2)
var
修饰的属性的
getter()
和
setter()
函数自定义
自定义一个setter,将在每次为属性赋值的时候被调用,如下:
class Person {
var hair: String = ""
get() = field //定义get方法
set(value) {//定义set方法
field = if (value.isNotEmpty()) value else "" //如果为不为空则返回其值,否则返回""
}
var nose: String = ""
get() = "Kotlin"//值一直是Kotlin,不会改变
set(value) {
field = if (value.isNotEmpty()) value else "" //如果为不为空则返回其值,否则返回""
}
}
var person = Person()
person.hair = "Android"
person.nose = "Android"
Log.e(TAG, "get()和set(): hair == ${person.hair} | nose == ${person.nose}")
nose 中的
getter()
函数值已经固定了,不会再改变,打印数据如下:
get()和set(): hair == Android | nose == Kotlin
总结一下:
1.使用了 val 修饰的属性不能有 setter() 方法;
2.属性默认实现了 getter() 和 setter() 方法,如果不重写则可以省略。
2.3 可见性
如果你需要改变访问器的可见性或者注释它,但不需要改变默认实现,你可以定义访问器而不定义它的主体:
class Person {
val tea: Int = 0
//private set 报错,val修饰的属性不能有setter
var soup: String = "Java"
//@Inject set 用Inject注解去实现setter()
var dish: String = "Android"
//private get 报错,不能有getter()访问器的可见性
var meal = "Kotlin"
private set //setter访问器私有化,并且它拥有kotlin的默认实现
}
var person = Person()
//person.meal = "HelloWord" 报错,setter已经声明为私有,不能重新赋值
如果属性访问器的可见性修改为
private
或者该属性直接使用
private
修饰时,只能手动提供一个公有的函数去改其属性,类似 Java 中的 Bean.setXXX()。
三、后备字段与属性
3.1 后备字段(Backing Fields)
后备字段相对 Java 来说是一种新的定义,不能在 Kotlin 类中直接声明字段,但是当属性需要后备字段时,Kotlin 有后端变量机制(Backing Fields)会自动提供,可以在访问器中使用后备字段标识符
field
来引用此字段,即 Kotlin 中的后备字段用
field
来表示。
为什么提供后备字段?
class Person {
var name: String = ""
get() = "HelloWord"//值一直是Kotlin,不会改变
}
var person = Person()
person.name = "HelloUniverse"
Log.e(TAG, "后备字段: goose == ${person.name}")
注意,我们明明通过
person.name= "HelloUniverse"
赋值,但是打印的依然是默认的 “HelloWord” 值,打印数据如下:
后备字段: name == HelloUniverse
上面的问题显而易见,因为我们定义了 Person 中的
name
的
getter()
方法,每次读取
name
的值都会执行 get,而 get 只是返回了 “HelloWord”,那么是不是直接用
name
替换掉 “HelloWord” 就可以了呢?我们来改造一下:
class Person {
var name: String = ""
//get() = "HelloWord"
get() = name //为name定义了get方法,这里返回name
}
var person = Person()
person.name = "HelloUniverse"
Log.e(TAG, "后备字段: name == ${person.name}")
那么上面代码执行后打印什么? “HelloUniverse”? 正确答案:不是。上面的写法是错误的,在运行时会造成无限递归,直到
java.lang.StackOverflowError
栈溢出异常,为什么?
因为在我们获取
person.name
这个值的时候,都会调用
get()
方法,而
get()
方法访问了name 属性(即 return name),这又会去调用 name 属性的
get()
方法,如此反复直到栈溢出。同样,
set()
方法也是如此,通过自定义改变 name 的值:
class Person {
var name: String = ""
set(value) {//为name定义了set方法
name = value//为name赋值一个新值,即value
}
}
var person = Person()
person.name = "HelloUniverse"
Log.e(TAG, "后备字段: name == ${person.name}")
同理:上面的代码会抛出栈溢出异常,因为
name = value
会无限触发 name 的
set()
方法。
那么我们怎么在自定义属性的get和set方法的时候在外部修改其值呢?
这就是后备字段的作用了,通过
field
可以有效解决上面的问题,代码如下:
var name: String? = "" //注意:初始化器直接分配后备字段
get() = field//直接返回field
set(value) {
field = value//直接将value赋值给field
}
var person = Person()
person.name = "HelloUniverse"
Log.e(TAG, "后备字段: name == ${person.name}")
打印数据如下:
后备字段: name == HelloUniverse
如果属性至少使用一个访问器的默认实现,或者自定义访问器通过
field
标识符引用该属性,则将生成该属性支持的字段。也就是说只有使用了默认的
getter()
或
setter()
以及显示使用
field
字段的时候,后备字段
field
才会存在。下面这段代码就不存在后备字段:
val isEmpty: Boolean
get() = this.color == 0
这里定义了
get()
方法,但是没有通过后备字段
field
去引用。
注意:后备字段
field
只能用于属性的访问器。
3.2 后备属性(Backing Properties)
如果你想做一些不适合后备字段来操作的事情,那么你可以使用后备属性来操作:
private var _table: Map<String, Int>? = null//后备属性
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap()//初始化
}
//如果_table不为空则返回_table,否则抛出异常
return _table ?: throw AssertionError("Set to null by another thread")
}
_table
属性是私有的
private
,我们不能直接使用,所以提供一个公有的后备属性
table
去初始化
_table
属性。这和 Java 定义 bean 属性的方式是一样的,因为访问私有属性的 get() 和 set() 方法,会被编译器优化成直接访问其实际字段,不会引入函数调用的开销。
四、编译时常量
所谓编译时常量,就是在编译时就能确定值的常量。
4.1 编译时常量与运行时常量的区别
与编译时常量对应的还有运行时常量,在运行时才能确定值,编译时无法确定其值,并放入运行常量池中。针对运行时常量,编译器只能确定其他代码段无法对其进行修改赋值。关于二者的区别,看下 Java 代码:
private static final String mark = "HelloWord";
private static final String mark2 = String.valueOf("HelloWord");
定义了两个常量:mark 和 mark2,那么你觉得他们有区别吗?大部分人认为没啥区别,都是常量。但是实际上是不一样的,来看看它们的字节码:
private final static Ljava/lang/String; mark = "HelloWord"
private final static Ljava/lang/String; mark2
我们发现,编译后的
mark
直接赋值了 “HelloWord”,而
mark2
却没有赋值,实际上
mark2
在类构造方法初始化的时候才进行赋值,也就是运行时才进行赋值。这就是编译时常量(mark)和运行时常量 (mark2)的区别!
4.2 编译时常量
在 Kotlin 中,编译时常量使用
const
修饰符修饰,它必须满足以下要求:
- 必须属于顶层Top-level,或对象声明或伴生对象的成员;
- 被基本数据类型或者String类型修饰的初始化变量;
-
没有自定义
getter()
方法; -
只有
val
修饰的才能用
const
修饰。
//顶层top-level
const val CONST_STR = "" //正确,属于top-level级别的成员
//const val CONST_USER = User() //错误,const只能修饰基本类型以及String类型的值,Point是对象
class Person {
//const val CONST_TYPE_A = 0 //编译错误,这里没有违背只能使用val修饰,但是这里的属性不属于top-level级别的,也不属于object里面的
object Instance { //这里的Instance使用了object关键字修饰,表示Instance是个单例,Kotlin其实已经为我们内置了单例,无需向 Java那样手写单例
//const var CONST_TYPE_B = 0 //编译错误,var修饰的变量无法使用const修饰
const val CONST_TYPE_C = 0 //正确,属于object类
}
}
这些属性还可以在注释中使用:
const val CONST_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(CONST_DEPRECATED) fun foo() {
//TODO
}
这里基本包括了
const
的应用场景。但是有人会问,
Kotlin 既然提供了
val
修饰符为什么还要提供
const
修饰符?
按理来说
val
已经可以表示常量了,为什么提供
const
?
4.3 const 与 val 的区别
下面代码属于 Kotlin 代码,顶层的常量位于位于 kotlin 文件
Person.kt
中,属于 top-level 级别:
const val NAME = "HelloWord"
val age = 20
class Person {
}
下面这段代码是 Java 代码,用于测试,建立一个类,在 main 函数数据:
public class ConstCompare {
public static void main(String[] args) {
//注意下面两种的调用方式
System.out.println(PersonKt.NAME);//这里注意:kotlin文件会默认生成kotlin文件名+Kt的java类文件
System.out.println(PersonKt.getAge());
//编译报错,PersonKt.age的调用方式是错误的
//System.out.println(PersonKt.age);
}
}
上面的代码证明了
const
修饰的字段和
val
修饰的字段的区别:使用
const
修饰的字段可以直接使用
类名+字段名
来调用,类似于 Java 的
private static final
修饰,而
val
修饰的字段只能用get方法的形式调用。
那么
const
的作用仅仅是为了标识公有静态字段?
不是,实际是
const
修饰字段
NAME
才会变成公有字段(即public),这是 Kotlin 的实现机制,但不是因为
const
才产生的 static 变量,我们来查看
Person
类的字节码:
//PersonKt是 Kotlin 生成与之对应 的 Java 类文件
public final class com/suming/kotlindemo/blog/PersonKt {
//注意下面两个字段 NAME 和 age 的字节码
// access flags 0x19
//Kotlin实际上为 NAME 生成了public final static修饰的 Java 字段
public final static Ljava/lang/String; NAME = "HelloWord"
@Lorg/jetbrains/annotations/NotNull;() // invisible
// access flags 0x1A
//Kotlin 实际上为 age 生成了private final static修饰的 Java 字段
private final static I age = 20
//注意:这里生成getAge()的方法
// access flags 0x19
public final static getAge()I
L0
LINENUMBER 14 L0
GETSTATIC com/suming/kotlindemo/blog/PersonKt.age : I
IRETURN
L1
MAXSTACK = 1
MAXLOCALS = 0
// access flags 0x8
static <clinit>()V //Kotlin生成静态构造方法
L0
LINENUMBER 14 L0
BIPUSH 20
PUTSTATIC com/suming/kotlindemo/blog/PersonKt.age : I
RETURN
MAXSTACK = 1
MAXLOCALS = 0
// compiled from: Person.kt
}
从上面的字节码中可以看出:
-
1.Kotlin为
NAME
和
age
两个字段都生成了 final static 标识,只不过
NAME
是 public 的,
age
是 private 的,所以可以通过类名来直接访问
NAME
而不能通过类名访问
age
; -
2.Kotlin 为
age
生成了一个 public final static 修饰的
getAge()
方法,所以可以通过
getAge()
来访问
age
。
总之,
const val
与
val
的总结如下:
const val
与
val
都会生成对应 Java 的static final修饰的字段,而
const val
会以 public 修饰,而
val
会以 private 修饰。同时,编译器还会为
val
字段生成 get 方法,以便外部访问。
注意:通过 Android studio >Tools > Kotlin > Show Kotlin ByteCode 来查看字节码。
五、延迟初始化的属性和变量
通常,声明为非空类型的属性必须(在构造函数中)初始化。然而,这通常很不方便。例如:在单元测试中,一般在setUp方法中初始化属性;在依赖注入框架时,只需要使用到定义的字段不需要立刻初始化等。
在这种情况下,不能在构造函数中提供一个非空初始化器,但是希望在引用类体中的属性时避免
null
检查。kotlin 针对这种场景设计了延迟初始化的机制,你可以使用
lateinit
修饰符来标记属性,延迟初始化,即不必立即进行初始化,也不必在构造方法中初始化,可以在后面某个适合的实际初始化。使用
lateinit
关键字修饰的变量需要满足以下几点:
-
不能修饰
val
类型的变量;
-
不能声明于可空变量,即类型后面加
?
,如String?;
-
修饰后,该变量必须在使用前初始化,否则会抛
UninitializedPropertyAccessException
异常;
-
不能修饰基本数据类型变量,例如:
Int
,
Float
,
Double
等数据类型,
String
类型是可以的;
-
不需额外进行空判断处理,访问时如果该属性还没初始化,则会抛出空指针异常;
-
只能修饰位于class body中的属性,不能修饰位于构造方法中的属性。
public class MyTest {
lateinit var subject: TestSubject //非空类型
@SetUp fun setup() {
subject = TestSubject()//初始化TestSubject
}
@Test fun test() {
subject.method() //构造方法调用
}
}
lateinit
修饰符可以用于类主体内声明的
var
属性(不在主构造函数中,并且只有当属性没有自定义getter和setter时才用)。自 Kotlin 1.2以来,可以用于顶级属性和局部变量。属性或变量的类型必须是非空的,而且不能是原始类型。在
lateinit
修饰的属性被初始化之前访问它会抛出异常,该异常清楚地标识被访问的属性以及没有被初始化的事实。
自 Kotlin 1.2以来,可以检查
lateinit
修饰的变量是否被初始化,在属性引用使用
this::变量名.isInitialized
,this可省:
lateinit var person: Person //lateinit 表示延迟初始化,必须是非空
fun method() {
person = Person()
if (this::person.isInitialized) {//如果已经赋值返回true,否则返回false
//TODO
}
Log.e(TAG, "延迟初始化: person.isInitialized == ${::person.isInitialized}")
}
打印数据如下:
延迟初始化: person.isInitialized == true
注意:这种检查只能对词法上可访问的属性可用,例如:在相同类型或外部类型声明的属性,或在同一个文件的顶层声明的属性。但是不能用于内联函数,为了避免二进制兼容性问题。
源码地址:
https://github.com/FollowExcellence/KotlinDemo-master
点关注,不迷路
好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是
人才
。
我是suming,感谢各位的支持和认可,您的
点赞、评论、收藏
【一键三连】就是我创作的最大动力,我们下篇文章见!
如果本篇博客有任何错误,请批评指教,不胜感激 !
要想成为一个优秀的安卓开发者,这里有必须要掌握的
知识架构
,一步一步朝着自己的梦想前进!Keep Moving!