温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Kotlin中属性与字段的示例分析

发布时间:2022-01-04 09:53:42 来源:亿速云 阅读:144 作者:小新 栏目:互联网科技

这篇文章主要介绍了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中属性与字段的示例分析”这篇文章对大家有帮助,同时也希望大家多多支持亿速云,关注亿速云行业资讯频道,更多相关知识等着你来学习!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI