Kotlin代码日记Android开发禅与计算机程序设计艺术

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

2019-03-27  本文已影响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

快来关注我吧!
上一篇下一篇

猜你喜欢

热点阅读