你说你会用Companion object?恐怕不是!
初次接触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
。