你说你会用Companion object?恐怕不是!

作者: 小小小小小粽子 | 来源:发表于2019-03-27 22:33 被阅读101次

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

    快来关注我吧!

    相关文章

      网友评论

        本文标题:你说你会用Companion object?恐怕不是!

        本文链接:https://www.haomeiwen.com/subject/kxyxbqtx.html