您好,登录后才能下订单哦!
这篇文章主要介绍了Kotlin中属性与字段的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。
一、概述
前面已经为大家讲解了类的使用以及属性的相关知识,在一个类中基本上都会出现属性和字段的,属性在变量和常量的文章中有详细讲解到,这里会重新简单介绍到。
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 //调用属性
实际上上面定义的属性是不完整的,在 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
1
(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
总结一下:
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
1
上面的问题显而易见,因为我们定义了 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
1
如果属性至少使用一个访问器的默认实现,或者自定义访问器通过 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
1
注意:这种检查只能对词法上可访问的属性可用,例如:在相同类型或外部类型声明的属性,或在同一个文件的顶层声明的属性。但是不能用于内联函数,为了避免二进制兼容性问题。
感谢你能够认真阅读完这篇文章,希望小编分享的“Kotlin中属性与字段的示例分析”这篇文章对大家有帮助,同时也希望大家多多支持亿速云,关注亿速云行业资讯频道,更多相关知识等着你来学习!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。