初次接觸Kotlin的時候,覺得這才是一門真正的OOP語言,就連基本型別,它也是一個類。後來遇到了一些在Java裡面用靜態成員實現很方便的場景,完全的OOP讓我無所適從,於是我找到了(Companion object)伴生物件。
使用方法大概如下:
class Main private constructor(){
private var id: Int? = null
companion object {
var previousId = -1
fun newInstance(): Main {
val instance = Main()
instance.id = previousId++
}
}
fun main(args: Array<String>) {
val main = Main.newInstance()
print((Main.previousId)
}
}
複製程式碼
這是一個工廠方法,用起來還是跟Java的靜態成員很相似的,但是我們得記住了,這些欄位其實是其他物件的成員。(不用說,編譯器又偷偷地幫我們做了一些事)
乍一看好像沒什麼問題,我們Java程式碼也是這麼寫的,讀者們可能要問我了,怎麼就只知道伴生物件就不行了,不就這點兒用法嗎?
別急,我們來扒一扒位元組碼:
// access flags 0x8
static <clinit>()V
NEW Main$Companion
DUP
ACONST_NULL
INVOKESPECIAL Main$Companion.<init> (Lkotlin/jvm/internal/DefaultConstructorMarker;)V
PUTSTATIC Main.Companion : LMain$Companion;
L0
LINENUMBER 5 L0
ICONST_M1
PUTSTATIC Main.previousId : I
RETURN
MAXSTACK = 3
MAXLOCALS = 0
複製程式碼
我們看到這一段,Main類在載入的時候,建立了一個Main$Companion類的物件,這也就證實了,伴生物件確實是一個物件,我們當成主類靜態成員使用的那些成員,都是這個物件的成員。
那我們來看看編譯器給我們生成的這個類的位元組碼:
// access flags 0x2
private <init>()V
L0
LINENUMBER 4 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this LMain$Companion; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1001
public synthetic <init>(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
L0
LINENUMBER 4 L0
ALOAD 0
INVOKESPECIAL Main$Companion.<init> ()V
RETURN
L1
LOCALVARIABLE this LMain$Companion; L0 L1 0
LOCALVARIABLE $constructor_marker Lkotlin/jvm/internal/DefaultConstructorMarker; L0 L1 1
MAXSTACK = 1
MAXLOCALS = 2
複製程式碼
我們可以看到,除了預設的建構函式,編譯器還給它合成了一個新的建構函式。
此外它還生成了get,set方法來訪問previousId欄位,給物件成員生成get,set函式,這也都是正常的。
// access flags 0x11
public final getPreviousId()I
L0
LINENUMBER 5 L0
INVOKESTATIC Main.access$getPreviousId$cp ()I
IRETURN
L1
LOCALVARIABLE this LMain$Companion; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x11
public final setPreviousId(I)V
// annotable parameter count: 1 (visible)
// annotable parameter count: 1 (invisible)
L0
LINENUMBER 5 L0
ILOAD 1
INVOKESTATIC Main.access$setPreviousId$cp (I)V
RETURN
L1
LOCALVARIABLE this LMain$Companion; L0 L1 0
LOCALVARIABLE <set-?> I L0 L1 1
MAXSTACK = 1
MAXLOCALS = 2
複製程式碼
等等!怎麼還有INVOKESTATIC 指令!我定睛一看,怎麼又去呼叫Main的靜態方法了,回過頭去看Main的位元組碼,果然,有這樣的方法:
// access flags 0x1019
public final static synthetic access$getPreviousId$cp()I
L0
LINENUMBER 1 L0
GETSTATIC Main.previousId : I
IRETURN
L1
MAXSTACK = 1
MAXLOCALS = 0
// access flags 0x1019
public final static synthetic access$setPreviousId$cp(I)V
L0
LINENUMBER 1 L0
ILOAD 0
PUTSTATIC Main.previousId : I
RETURN
L1
LOCALVARIABLE <set-?> I L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
複製程式碼
難受了,一通OOP的操作下來,各種方法呼叫,最後居然還是給Main生成了靜態成員,而且還生成了方法來訪問id:
// access flags 0x1019
public final static synthetic access$getId$p(LMain;)Ljava/lang/Integer;
L0
LINENUMBER 1 L0
ALOAD 0
GETFIELD Main.id : Ljava/lang/Integer;
ARETURN
L1
LOCALVARIABLE $this LMain; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1019
public final static synthetic access$setId$p(LMain;Ljava/lang/Integer;)V
L0
LINENUMBER 1 L0
ALOAD 0
ALOAD 1
PUTFIELD Main.id : Ljava/lang/Integer;
RETURN
L1
LOCALVARIABLE $this LMain; L0 L1 0
LOCALVARIABLE <set-?> Ljava/lang/Integer; L0 L1 1
MAXSTACK = 2
MAXLOCALS = 2
複製程式碼
我就想實現一個基本的工廠方法,有必要給我生成這麼多方法嗎?我肯定閒不住的,我又開始搗鼓了:previousId是個靜態成員,那就想辦法讓它成為一個真正的靜態成員,newInstance方法本意也是一個靜態的建立物件的方法。
@file:JvmName("Main")
@JvmField
var previousId = -1
class Main private constructor() {
private var id: Int? = null
companion object {
@JvmStatic
fun newInstance(): Main {
val instance = Main()
instance.id = previousId++
}
}
fun main(args: Array<String>) {
val main = Main.newInstance()
print((previousId)
}
}
複製程式碼
我在之前已經跟大家討論過頂級成員配合@JvmField
的效果,@file:JvmName通知編譯器所有頂級成員都放到Main這個類下,我們就再也不用承受編譯器給我們生成那麼多額外方法的開銷了,而@JvmStatic
,會讓編譯器直接把newInstance方法編譯成一個靜態方法。
這時候再來看生成的位元組碼:
// access flags 0x1001
public synthetic <init>(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
L0
LINENUMBER 9 L0
ALOAD 0
INVOKESPECIAL Main$Companion.<init> ()V
RETURN
L1
LOCALVARIABLE this LMain$Companion; L0 L1 0
LOCALVARIABLE $constructor_marker Lkotlin/jvm/internal/DefaultConstructorMarker; L0 L1 1
MAXSTACK = 1
MAXLOCALS = 2
複製程式碼
除了Main$Companion類這個生成的建構函式,編譯器已經不會給我們生成那些彎彎繞繞的方法了,完美!
我們來做一下總結,其實就是避免在伴生物件中定義成員變數,而改在檔案中定義頂級變數,而且可以把伴生物件中的函式都用@JvmStatic
來修飾,使它變成一個真正的靜態函式。
下次,我們再來扒扒Kotlin一個獨特的類Range
。