你說你會用Companion object?恐怕不是!

小小小小小粽子-發表於2019-03-27

初次接觸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

相關文章