昨天公眾號後臺收到一位小夥伴的留言詢問,他對於 Kotlin 為何沒有 Java 的 final
關鍵字感到困惑,這應該是很多初學者都會遇到的問題,所以我就寫了這篇博文從更底層的角度來解析 Kotlin 宣告變數時用到的三個關鍵字:var
、val
和 const
。
其實,Java 的 final
就等價於 Kotlin 的 val
, 雖然通過 javap 反編譯可以看到兩者的底層實現不一樣,但是從語義上講,它們兩者的確是等價的。具體原因,我們來逐一分析。
什麼是屬性
我們知道,在 Kotlin 的世界中,class 已經不再是唯一的一等公民,我們可以直接在程式碼檔案的最頂層(top-level)宣告類、函式和變數。
class Address {
// class properties
var province = "zhejiang"
val city = "hangzhou"
}
fun prettify(address: Address): String {
// local variable
val district = "xihu"
return district + ',' + address.city + ',' + address.province
}
// top-level property
val author = "liangfei"
複製程式碼
上例中的 Address
是一個類,prettify
是一個函式,author
是一個變數,它們都是一等公民,也就是說,函式和變數可以單獨存在,不會像 Java 那樣依附於類。
首先,var
和 val
可分為三種型別:
- 類的屬性(class property),例如上例中的
var province = "zhejiang"
,它是Address
類的一個屬性; - 頂層屬性(top-level property),例如上例中的
val author = "liangfei"
,它是檔案(module)的一個屬性; - 區域性變數(local variable),例如上例中的
val district = "xihu"
,它是函式prettify
的一個區域性變數。
類的屬性和頂層屬性都是屬性,所以可以統一來看待,屬性本身不會儲存值,也就是說它不是一個欄位(field),那它的值是哪裡來的呢?我們先來看一下宣告一個屬性的完整語法:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
複製程式碼
可以看出,一個屬性的宣告可以分解為五個部分:屬性名、屬性型別、initializer、getter、setter。
- 屬性的名就是就是我們用來引用屬性的方式;
- 屬性的型別可以顯示宣告,因為 Kotlin 支援型別推導,如果型別能夠從上下文推導得出,那麼它也可以省略;
- initializer 是型別推導的線索之一,例如
val author = "liangfei"
,根據= "liangfei"
可以得出它是一個String
型別; - getter 也是型別推導的線索之一,所有使用屬性名獲取值的操作,都是通過 getter 來完成的;
- setter 用於給屬性賦值。
以上只是宣告瞭一個屬性,如果我們要賦值,它的值會儲存在哪裡呢?其實,編譯器還會自動為屬性生成一個用於儲存值的欄位(field),因為寫程式碼時感知不到到它的存在,所以稱為幕後欄位(backing field)。具體可以參考幕後欄位,因為與本文關係不大,所以此處不做介紹。
var
和 val
所宣告的屬性,其最本質的區別就是:val
不能有 setter,這就達到了 Java 中 final
的效果。
例如,上面 Kotlin 程式碼中的 Address
類:
class Address {
var province = "zhejiang"
val city = "hangzhou"
}
複製程式碼
它在 JVM 平臺上的實現是下面這樣的(通過 javap
命令檢視):
public final class Address {
public final java.lang.String getProvince();
public final void setProvince(java.lang.String);
public final java.lang.String getCity();
public Address();
}
複製程式碼
可以看出,針對 var province
屬性,生成了 getProvince()
和 setProvince(java.lang.String)
兩個函式。但是 val city
只生成了一個 getCity()
函式。
對於區域性變數來說,var
或者 val
都無法生成 getter 或 setter,所以只會在編譯階段做檢查。
Classes in Kotlin can have properties. These can be declared as mutable, using the var keyword or read-only using the val keyword.
對於類的屬性來說:var
表示可變(mutable),val
表示只讀(read-only)。對於頂層屬性來說也是一樣的。
可變和只讀
var
表示可變,val
表示只讀,而不是不可變(immutable)。我們已經知道了 val
屬性只有 getter,但是這並不能保證它的值是不可變的。例如,下面的程式碼:
class Person {
var name = "liangfei"
var age = 30
val nickname: String
get() {
return if (age > 30) "laoliang" else "xiaoliang"
}
fun grow() {
age += 1
}
}
複製程式碼
屬性 nickname
的值並非不可變,當呼叫 grow()
方法時,它的值會從 "laoliang"
變為 "xiaoliang"
,但是無法直接給 nickname
賦值,也就是說,它不能位於賦值運算的左側,只能位於右側,這就說明了為什麼它是隻讀(read-only),而不是不可變(immutable)。
其實,Kotlin 有專門的語法來定義可變和不可變的變數,後面會專門寫一篇博問來分析,這裡不再深入。
我們知道,Java 中可以使用 static final
來定義常量,這個常量會存放於全域性常量區,這樣編譯器會針對這些變數做一些優化,例如,有三個字串常量,他們的值是一樣的,那麼就可以讓這個三個變數指向同一塊空間。我們還知道,區域性變數無法宣告為 static final
,因為區域性變數會存放在棧區,它會隨著呼叫的結束而銷燬。
Kotlin 引入一個新的關鍵字 const
來定義常量,但是這個常量跟 Java 的 static final
是有所區別的,如果它的值無法在編譯時確定,則編譯不過,因此 const
所定義的常量叫編譯時常量。
編譯時常量
首先,const
無法定義區域性變數,除了區域性變數位於棧區這個原因之外,還因為區域性變數的值無法在編譯期間確定,因此,const
只能修飾屬性(類屬性、頂層屬性)。
因為 const
變數的值必須在編譯期間確定下來,所以它的型別只能是 String
或基本型別,並且不能有自定義的 getter。
所以,編譯時常量需要滿足如下條件:
- 頂層或者
object
的成員(object
也是 Kotlin 的一個新特性,具體可參考物件宣告)。 - 初始化為一個 String 或者基本型別的值
- 沒有自定義 getter
總結
最後,總結一下:
var
、val
宣告的變數分為三種型別:頂層屬性、類屬性和區域性變數;var
屬性可以生成 getter 和 setter,是可變的(mutable),val
屬性只有 getter,是隻讀的(read-only,注意不是 immutable);- 區域性變數只是一個普通變數,不會有 getter 和 setter,它的
val
等價於 Java 的final
,在編譯時做檢查。 const
只能修飾沒有自定義 getter 的val
屬性,而且它的值必須在編譯時確定。