Kotlin专题「八」:属性与字段(Getter()与Setter(),后备字段field)

  • Post author:
  • Post category:其他



前言:人生只有走出来的美丽,没有等出来的辉煌。




一、概述

前面已经为大家讲解了类的使用以及属性的相关知识,在一个类中基本上都会出现属性和字段的,属性在变量和常量的文章中有详细讲解到,这里会重新简单介绍到。



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!



版权声明:本文为m0_37796683原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。