溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Kotlin中屬性與字段的示例分析

發(fā)布時間:2022-01-04 09:53:42 來源:億速云 閱讀:142 作者:小新 欄目:互聯(lián)網(wǎng)科技

這篇文章主要介紹了Kotlin中屬性與字段的示例分析,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

一、概述
??前面已經(jīng)為大家講解了類的使用以及屬性的相關(guān)知識,在一個類中基本上都會出現(xiàn)屬性和字段的,屬性在變量和常量的文章中有詳細(xì)講解到,這里會重新簡單介紹到。

1.1 聲明屬性
Java 類中的變量聲明為成員變量,而 Kotlin 中聲明為屬性,Kotlin 類中的屬性可以使用 var 關(guān)鍵字聲明為可變,也可以使用 val 關(guān)鍵字聲明為只讀。類中的屬性必須初始化值,否則會報錯。

class Person {
    val id: String = "0" //不可變,值為0
    var nameA: String? = "Android" //可變,允許為null
    var age: Int = 22 //可變,非空類型
}

前面有提到 Kotlin 能有效解決空指針問題,實際上定義類型時增加了可空和非空的標(biāo)志區(qū)分,如上面聲明的類型后面有?表示屬性可為空,類型后面沒有有?表示屬性不可為空。如 name: String? 中 name 可以為 null,age: Int 中 age 不可為 null。在使用時,編譯器會根據(jù)屬性是否可為空做出判斷,告知開發(fā)者是否需要處理,從而避免空指針異常。

Kotlin 中使用類中的屬性和 Java 中的一樣,通過類名來引用:

    var person = Person()//實例化person,在Kotlin中沒有new關(guān)鍵字
    view1.text = person.id //調(diào)用屬性

實際上上面定義的屬性是不完整的,在 Java 中的屬性定義還會涉及到 get() 和 set() 方法,那么在 Kotlin 怎么表示呢?

二、Getter()與Setter()
Kotlin 中 getter() 對應(yīng) Java 中的 get() 函數(shù),setter() 對應(yīng) Java 中的 set() 函數(shù),不過注意這僅僅是 Kotlin 的叫法而已,真正的寫法還是 get() 和 set()。

2.1 完整語法
在 Kotlin 中普通類中一般不提供 get() 和 set() 函數(shù),因為普通的類中基本用不到,這點和 Java 相同,但是 Java 在定義純粹的數(shù)據(jù)類時,會用到 get() 和 set() 函數(shù),但是 Kotlin 這種情況定義了數(shù)據(jù)類,已經(jīng)為我們實現(xiàn)了 get() 和 set() 函數(shù)。

聲明屬性的完整語法如下:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

這是官方的標(biāo)準(zhǔn)語法,我們來翻譯一下:

var <屬性名> : <屬性類型> = 初始化值
    <getter>
    <setter>

其中,初始化器(property_initializer),getter 和 setter 都是可選的,如果可以從初始化器(或者 getter 返回類型)推斷出屬性的類型,那么屬性類型(PropertyType)是可選的,如下所示:

//var weight: Int?//報錯,需要初始化,默認(rèn)getter和setter方法是隱藏的

var height = 172 //根據(jù)初始化值推斷類型為Int,屬性類型可以不需要顯示,默認(rèn)實現(xiàn)了getter和setter方法

只讀屬性聲明與可變屬性聲明不同,它是 val 開始而不是 var,不允許設(shè)置 setter 函數(shù),因為它只是只讀。

val type: Int?//類型為Int,必需初始化,默認(rèn)實現(xiàn)了getter方法

val cat = 10//類型為Int,默認(rèn)實現(xiàn)getter方法

init {//初始化屬性,也可以在構(gòu)造函數(shù)中初始化
    type = 0
}

Kotlin 中屬性的 getter 和 setter 函數(shù)是可以省略的,系統(tǒng)有默認(rèn)實現(xiàn),如下:

class Person {        
    //用var修飾時,必須為其賦初始化值,即使有g(shù)etter()也必須初始化,不過獲取的數(shù)值是getter()返回的值
    var name: String? = "Android"
        get() = field //默認(rèn)實現(xiàn)方式,可省略
        set(value) { //默認(rèn)實現(xiàn)方式,可省略
            field = value //value是setter()方法參數(shù)值,field是屬性本身
        }
}

其中,field 表示屬性本身,后端變量,下面會詳細(xì)講到,value 是 set() 的參數(shù)值,也可以修改你喜歡的名稱。set(value){field = value} 的意思是 set() 方法將設(shè)置的參數(shù) value 賦值給屬性 field,上面的 getetter() 與 setter() 均為默認(rèn)實現(xiàn)方式,可以省略。

2.2 自定義
上面的屬性我們都省略了 getter 和 setter 方法。我們可以為屬性定義訪問器,自定義 getetter() 與 setter() 可以根據(jù)自身的實際情況來制定方法值的規(guī)則。好比如 Java 中自定義 get() 和 set() 方法。

(1)val 修飾的屬性的 getter() 函數(shù)自定義

如果定義一個自定義 getter,那么 getter() 方法會在屬性每次被訪問時調(diào)用,下面是自定義 getter 的例子:

    //用val修飾時,用getter()函數(shù)屬性指定其他值時可以不賦默認(rèn)值,但是不能有setter()函數(shù),等價 val id: String = "0"
    val id: String 
        get() = "0"   //為屬性定義get方法

如果屬性的類型可以從 getter 方法中推斷出來,那么類型可以省略:

class Person {
    //color根據(jù)條件返回值
    val color = 0
        get() = if (field > 0) 100 else -1  //定義get方法
    
    //isEmpty屬性是判斷 color是否等于0
    val isEmpty get() = this.color == 0 //Boolean類型,getter方法中推斷出來,可省
}

    //調(diào)用
    var person = Person()
    Log.e(TAG, "get()和set(): color == ${person.color} | isEmpty == ${person.isEmpty}")

color 默認(rèn)為0,get() 的數(shù)據(jù)為-1,isEmpty 為 false,打印數(shù)據(jù)如下:

get()和set(): color == -1 | isEmpty == false
1
(2)var 修飾的屬性的 getter() 和 setter() 函數(shù)自定義

自定義一個setter,將在每次為屬性賦值的時候被調(diào)用,如下:

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() 函數(shù)值已經(jīng)固定了,不會再改變,打印數(shù)據(jù)如下:

get()和set(): hair == Android | nose == Kotlin
1
總結(jié)一下:
1.使用了 val 修飾的屬性不能有 setter() 方法;
2.屬性默認(rèn)實現(xiàn)了 getter() 和 setter() 方法,如果不重寫則可以省略。

2.3 可見性
如果你需要改變訪問器的可見性或者注釋它,但不需要改變默認(rèn)實現(xiàn),你可以定義訪問器而不定義它的主體:

class Person {
    val tea: Int = 0
        //private set  報錯,val修飾的屬性不能有setter

    var soup: String = "Java"
        //@Inject set   用Inject注解去實現(xiàn)setter()

    var dish: String = "Android"
        //private get   報錯,不能有g(shù)etter()訪問器的可見性

    var meal = "Kotlin"
        private set   //setter訪問器私有化,并且它擁有kotlin的默認(rèn)實現(xiàn)
}

    var person = Person()
    //person.meal = "HelloWord"    報錯,setter已經(jīng)聲明為私有,不能重新賦值

如果屬性訪問器的可見性修改為 private 或者該屬性直接使用 private 修飾時,只能手動提供一個公有的函數(shù)去改其屬性,類似 Java 中的 Bean.setXXX()。

三、后備字段與屬性
3.1 后備字段(Backing Fields)
后備字段相對 Java 來說是一種新的定義,不能在 Kotlin 類中直接聲明字段,但是當(dāng)屬性需要后備字段時,Kotlin 有后端變量機(jī)制(Backing Fields)會自動提供,可以在訪問器中使用后備字段標(biāo)識符 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" 賦值,但是打印的依然是默認(rèn)的 “HelloWord” 值,打印數(shù)據(jù)如下:

后備字段: name == HelloUniverse
1
上面的問題顯而易見,因為我們定義了 Person 中的 name 的 getter() 方法,每次讀取 name 的值都會執(zhí)行 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}")

那么上面代碼執(zhí)行后打印什么? “HelloUniverse”? 正確答案:不是。上面的寫法是錯誤的,在運(yùn)行時會造成無限遞歸,直到j(luò)ava.lang.StackOverflowError棧溢出異常,為什么?

因為在我們獲取 person.name 這個值的時候,都會調(diào)用 get() 方法,而 get()方法訪問了name 屬性(即 return name),這又會去調(diào)用 name 屬性的 get() 方法,如此反復(fù)直到棧溢出。同樣,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 會無限觸發(fā) 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}")

打印數(shù)據(jù)如下:

后備字段: name == HelloUniverse
1
如果屬性至少使用一個訪問器的默認(rèn)實現(xiàn),或者自定義訪問器通過 field 標(biāo)識符引用該屬性,則將生成該屬性支持的字段。也就是說只有使用了默認(rèn)的 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() 方法,會被編譯器優(yōu)化成直接訪問其實際字段,不會引入函數(shù)調(diào)用的開銷。

四、編譯時常量
所謂編譯時常量,就是在編譯時就能確定值的常量。

4.1 編譯時常量與運(yùn)行時常量的區(qū)別
與編譯時常量對應(yīng)的還有運(yùn)行時常量,在運(yùn)行時才能確定值,編譯時無法確定其值,并放入運(yùn)行常量池中。針對運(yùn)行時常量,編譯器只能確定其他代碼段無法對其進(jìn)行修改賦值。關(guān)于二者的區(qū)別,看下 Java 代碼:

    private static final String mark = "HelloWord";
    private static final String mark2 = String.valueOf("HelloWord");

定義了兩個常量:mark 和 mark2,那么你覺得他們有區(qū)別嗎?大部分人認(rèn)為沒啥區(qū)別,都是常量。但是實際上是不一樣的,來看看它們的字節(jié)碼:

private final static Ljava/lang/String; mark = "HelloWord"
private final static Ljava/lang/String; mark2

我們發(fā)現(xiàn),編譯后的 mark 直接賦值了 “HelloWord”,而 mark2 卻沒有賦值,實際上 mark2 在類構(gòu)造方法初始化的時候才進(jìn)行賦值,也就是運(yùn)行時才進(jìn)行賦值。這就是編譯時常量(mark)和運(yùn)行時常量 (mark2)的區(qū)別!

4.2 編譯時常量
在 Kotlin 中,編譯時常量使用 const 修飾符修飾,它必須滿足以下要求:

必須屬于頂層Top-level,或?qū)ο舐暶骰虬樯鷮ο蟮某蓡T;
被基本數(shù)據(jù)類型或者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關(guān)鍵字修飾,表示Instance是個單例,Kotlin其實已經(jīng)為我們內(nèi)置了單例,無需向 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 的應(yīng)用場景。但是有人會問,Kotlin 既然提供了 val 修飾符為什么還要提供 const 修飾符? 按理來說 val 已經(jīng)可以表示常量了,為什么提供 const ?

4.3 const 與 val 的區(qū)別
下面代碼屬于 Kotlin 代碼,頂層的常量位于位于 kotlin 文件 Person.kt 中,屬于 top-level 級別:

const val NAME = "HelloWord"
val age = 20

class Person {
}

下面這段代碼是 Java 代碼,用于測試,建立一個類,在 main 函數(shù)數(shù)據(jù):

public class ConstCompare {
    public static void main(String[] args) {
        //注意下面兩種的調(diào)用方式
        System.out.println(PersonKt.NAME);//這里注意:kotlin文件會默認(rèn)生成kotlin文件名+Kt的java類文件
        System.out.println(PersonKt.getAge());

        //編譯報錯,PersonKt.age的調(diào)用方式是錯誤的
        //System.out.println(PersonKt.age);
    }
}

上面的代碼證明了 const 修飾的字段和 val 修飾的字段的區(qū)別:使用 const 修飾的字段可以直接使用 類名+字段名來調(diào)用,類似于 Java 的 private static final 修飾,而 val 修飾的字段只能用get方法的形式調(diào)用。

那么 const 的作用僅僅是為了標(biāo)識公有靜態(tài)字段?

不是,實際是 const 修飾字段 NAME 才會變成公有字段(即public),這是 Kotlin 的實現(xiàn)機(jī)制,但不是因為 const 才產(chǎn)生的 static 變量,我們來查看 Person 類的字節(jié)碼:

//PersonKt是 Kotlin 生成與之對應(yīng) 的 Java 類文件
public final class com/suming/kotlindemo/blog/PersonKt {
  //注意下面兩個字段 NAME 和 age 的字節(jié)碼
  
  // 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生成靜態(tài)構(gòu)造方法
   L0
    LINENUMBER 14 L0
    BIPUSH 20
    PUTSTATIC com/suming/kotlindemo/blog/PersonKt.age : I
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 0
  // compiled from: Person.kt
}

從上面的字節(jié)碼中可以看出:

1.Kotlin為 NAME 和 age 兩個字段都生成了 final static 標(biāo)識,只不過NAME 是 public 的,age 是 private 的,所以可以通過類名來直接訪問 NAME 而不能通過類名訪問 age ;
2.Kotlin 為 age 生成了一個 public final static 修飾的 getAge() 方法,所以可以通過 getAge() 來訪問 age 。
總之, const val 與 val 的總結(jié)如下:
const val與 val 都會生成對應(yīng) Java 的static final修飾的字段,而 const val 會以 public 修飾,而 val 會以 private 修飾。同時,編譯器還會為 val 字段生成 get 方法,以便外部訪問。

注意:通過 Android studio >Tools > Kotlin > Show Kotlin ByteCode 來查看字節(jié)碼。

五、延遲初始化的屬性和變量
??通常,聲明為非空類型的屬性必須(在構(gòu)造函數(shù)中)初始化。然而,這通常很不方便。例如:在單元測試中,一般在setUp方法中初始化屬性;在依賴注入框架時,只需要使用到定義的字段不需要立刻初始化等。

在這種情況下,不能在構(gòu)造函數(shù)中提供一個非空初始化器,但是希望在引用類體中的屬性時避免null檢查。kotlin 針對這種場景設(shè)計了延遲初始化的機(jī)制,你可以使用 lateinit 修飾符來標(biāo)記屬性,延遲初始化,即不必立即進(jìn)行初始化,也不必在構(gòu)造方法中初始化,可以在后面某個適合的實際初始化。使用 lateinit 關(guān)鍵字修飾的變量需要滿足以下幾點:

不能修飾 val 類型的變量;
不能聲明于可空變量,即類型后面加?,如String?;
修飾后,該變量必須在使用前初始化,否則會拋 UninitializedPropertyAccessException 異常;
不能修飾基本數(shù)據(jù)類型變量,例如:Int,F(xiàn)loat,Double 等數(shù)據(jù)類型,String 類型是可以的;
不需額外進(jìn)行空判斷處理,訪問時如果該屬性還沒初始化,則會拋出空指針異常;
只能修飾位于class body中的屬性,不能修飾位于構(gòu)造方法中的屬性。
public class MyTest {
    lateinit var subject: TestSubject //非空類型

    @SetUp fun setup() {
        subject = TestSubject()//初始化TestSubject
    }

    @Test fun test() {
        subject.method()  //構(gòu)造方法調(diào)用
    }
}

lateinit 修飾符可以用于類主體內(nèi)聲明的 var 屬性(不在主構(gòu)造函數(shù)中,并且只有當(dāng)屬性沒有自定義getter和setter時才用)。自 Kotlin 1.2以來,可以用于頂級屬性和局部變量。屬性或變量的類型必須是非空的,而且不能是原始類型。在 lateinit 修飾的屬性被初始化之前訪問它會拋出異常,該異常清楚地標(biāo)識被訪問的屬性以及沒有被初始化的事實。

自 Kotlin 1.2以來,可以檢查lateinit 修飾的變量是否被初始化,在屬性引用使用 this::變量名.isInitialized,this可?。?/p>

    lateinit var person: Person //lateinit 表示延遲初始化,必須是非空

    fun method() {
        person = Person()
        if (this::person.isInitialized) {//如果已經(jīng)賦值返回true,否則返回false
            //TODO
        }
        Log.e(TAG, "延遲初始化: person.isInitialized == ${::person.isInitialized}")
    }

打印數(shù)據(jù)如下:

延遲初始化: person.isInitialized == true
1
注意:這種檢查只能對詞法上可訪問的屬性可用,例如:在相同類型或外部類型聲明的屬性,或在同一個文件的頂層聲明的屬性。但是不能用于內(nèi)聯(lián)函數(shù),為了避免二進(jìn)制兼容性問題。

感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“Kotlin中屬性與字段的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識等著你來學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI