kotlin入门潜修

kotlin入门潜修之类和对象篇—object及其原理

2018-12-17  本文已影响0人  寒潇2018

本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

创作不易,如有转载,还请备注。

写在前面

人一能之,己百之;人十能之,己千之。果能此道,虽愚必明,虽柔必强。——与君共勉。

object表达式及声明

kotlin入门潜修之类和对象篇—嵌套类及其原理这篇文章中我们已经使用过了object关键字,从文中可知,使用object能够在kotlin中实现类似于java中的匿名内部类的功能。但由于那篇文章主要在是阐述内部类,因此没有过多篇幅对object做较深阐述,而本篇文章就是要对object进行一次深入探讨。

再次强调,此object不是java中的Object类,而是kotlin中的关键字,该object首字母是小写的。

kotlin提供object关键字的用意,就是在生成一个对当前类进行轻微修改的类对象、且不需要声明一个新的子类的时候使用。本篇文章将从object作为表达式以及object作为声明两部分来阐述下kotlin中的object的用法。

object表达式

先来回顾下匿名内部类的实现,示例如下:

//MyListener接口,用于监听点击事件
interface MyListener {
    fun onClick()//这里是当点击发生时的回调方法
}
//我们暴露了一个设置点击监听器的入口
fun setOnCliCkListener(listener: MyListener) {
//假设当点击事件发生时,在这里通过listener通知外界
//也即通过调用listener.onClick方法实现
}
//测试类
class Main {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
          //注意这里,通过匿名类对象实现了onClick方法
            setOnCliCkListener(object : MyListener {
                override fun onClick() {
                    println("clicked...")
                }
            })
        }
    }
}

上面的代码是ui交互中典型的监听事件的写法,也可被称为观察者模式,很好的体现了匿名类的用处。但重点想表明的是,这段代码中object实际上是作为表达式存在的。为什么这么说?这是因为上述代码实际上是产生了object类型的匿名对象,这是表达式和声明的本质区别:作为表达式时可以有右值,反之不行。

再来看几个object作为表达式的几个例子。

interface MyListener {
    fun onClick()
}
open class Test(val name: String) {}
//object作为表达式,返回了匿名对象
val obj: MyListener = object : Test("test"), MyListener {
    override fun onClick() {
          print(name)
    }
}

上面代码需要注意以下几点:

  1. 如果父类构造方法有入参,则在生成匿名对象时必须要按父类构造方法的定义传参。
  2. 由于生成的匿名对象有多个父实现,所以在声明obj时必须指定类型,即val obj: MyListener后面的MyListener必须要显示指定。

当object作为表达式时,还可以单纯的作为一个对象存在,这个时候没有任何超类型。如下所示:

fun test() {
    val add = object {//没有任何超类,只是作为一个对象存在
        var x: Int = 1
        var y: Int = 2
    }
    println(add.x + add.y)//打印 '3'
}

有朋友会发现,上面我们演示的匿名object的用法基本都是在本地(区别于类成员)定义的,那么匿名object能否作为属性存在呢?答案是可以的,但是匿名object作为属性存在时有一些限制:

  1. 匿名object作为private属性时,其表达式返回的类型就是object类型
  2. 匿名object作为public属性时,其表达式返回的类型将是其超类的类型,如果没有超类,则返回kotlin中的顶级类Any类型。

看个例子就会明白:

class Test() {
    private val test1 = object {//注意这里test1被声明了private
        val value = "test1"
    }
    public val test2 = object {//注意这里test2被声明了public(其实可以省略,因为默认就是public)
        val value = "test2"
    }
    fun test() {
        val val1 = test1.value//正确,private修饰的匿名object返回类型就是object类型
        val val2 = test2.value//错误,public修饰的匿名object返回的类型是其超类
//这里的超类就是Any,而Any类并没有value字段
    }
}

同java一样,kotlin允许在匿名类对象内部访问外部的成员,但是有一点与java不一样,那就是此时外部成员不必再声明为final(java中关键字,表示不可变的,对应于val),示例如下:


class Main {
    var className = "Main.class"
    fun test() {
        setOnCliCkListener(object : MyListener {
            override fun onClick() {
                println(className)//这里使用外部的className属性,此时外部也不必声明为不可变的
            }
        })
    }
}

object声明

上一章节讲述了object作为表达式的一些用法,本章节讲述object作为声明时的一些用法。

当object作为声明存在时,其实就是我们前面文章中已经阐述过的kotlin中单例的写法。在kotlin中,可以不用再像java那样自己去实现单例,而是通过提供关键字来保证单例,这个关键字就是object。当object作为声明修饰一个“class”时,这个“class”就只有一个对象。示例如下:

object SingleInstance {//使用object来声明一个单例对象
}
class Main {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val s1 = SingleInstance//注意这里不再是SingleInstance()
            val s2 = SingleInstance
            println(s1 === s2)//打印'true'
        }
    }
}

上面代码中s1===s2打印结果为true标明了SingleInstance就是个单例对象。

那么如何使用单例对象呢?很简单,像普通对象一样使用即可:

object SingleInstance {//单例
    fun test(){}//有个test方法
}
//测试方法test
fun test(){
    SingleInstance.test()//直接通过单例名来调用其test方法
}

object作为声明时需要注意以下几点:

  1. object作为声明时是没有右值的,所以无法像上一章节中作为表达式那样赋值给其他变量。
  2. object作为声明时无法声明本地变量,但可以作为类成员存在。
  3. object作为声明存在时,可以为其定义超类,也就是说单例可以有超类,如下所示:
object SingleInstance:MyListener {//该单例实现了MyListener接口
    override fun onClick() {
    }
}

最后需要说明的是,kotlin中的单例天生是线程安全的,所以不必像java那样考虑多线程情况。

伴随对象(Companion Objects)

在阐述伴随对象之前先来看个伴随对象的使用例子:

class MyClass {
    companion object {//这就是伴随对象的定义
        fun test() {}
    }
}
//测试类
class Main {
    companion object {//实际上我们已经用多很多次了
        @JvmStatic
        fun main(args: Array<String>) {
            MyClass.test()//伴随对象的调用
            MyClass.Companion.test()//你也可以通过这种方式调用
        }
    }
}

上面代码展示了伴随对象的定义及其使用,事实上,我们已经多次见识过伴随对象了:那就是每次测试时候使用的main方法。

通过上面代码可以知道,伴随对象可以直接通过类名来进行访问,也可以通过kotlin为我们提供的Companion成员来调用。其实还可以通过下面方式来调用:

class MyClass {
    companion object {
        fun test() {}
    }
}
//测试方法
fun m1(){
    val obj = MyClass//竟然可以直接通过类名进行赋值!!!
    obj.test()//调用伴随对象的test方法
}

很神奇,竟然可以通过类名直接调用伴随对象中的test方法,这些原理我们稍后再来讨论。先看看kotlin对此用法的说明:

无论伴随对象有没有命名,只要使用包含有伴随对象的类名进行赋值的时候,此值实际上就是该类所持有的其内部伴随对象的引用地址。

那么如果一个类中含有多个伴随对象会怎样呢?很抱歉,kotlin规定一个类中只允许存在一个伴随对象!

kotlin中的伴随对象使用起来虽然很像java中static修饰的类成员的使用方法,但实际上它依然是以对象的形式调用的,和static修饰的成员是不一样的。

kotlin中的伴随对象是可以继承类或实现接口的,如下所示:

interface MyListener {//定义了一个MyListener接口
    fun onClick()
}
open class Test{}//定义了一个类Test
class MyClass {
    companion object : Test(),MyListener {//伴随对象继承了Test类,并且实现了MyListener接口
        override fun onClick() {
        }
    }
}

但是如果我们使用@JvmStatic注解修饰就表示和java中的static成员是一样。比如我们常用的main方法中你会发现都是用了@JvmStatic注解修饰,这是因为,java的入口方法main必须是static的,而伴随对象中的成员并不是static的,所以需要额外加上static修饰符告知编译器,这个是真正意义上的static方法。

那么伴随对象的初始化时机是什么?是和普通成员一致还是和静态成员一致?

事实上,伴随对象是在其所属类加载的时候完成初始化的,是和java中的静态成员初始化时机一致的。

object的原理

又到了刨根问底的时候了,本篇章节会阐述object实现的底层原理。
照例,先上一段要分析的object的源代码,如下所示:

object SingleInstance {//使用object来声明一个单例对象
}

没错,就是这么一个非常简单的object声明源码,我来看下其生成的字节码到底是什么,如下所示:

public final class SingleInstance {//字节码对应的SingleInstance类
  // access flags 0x2
  private <init>()V//注意,构造方法是私有的
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    ALOAD 0
    CHECKCAST SingleInstance
    PUTSTATIC SingleInstance.INSTANCE : LSingleInstance;
    RETURN
   L1
    LOCALVARIABLE this LSingleInstance; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x19
  public final static LSingleInstance; INSTANCE//这里生成了一个静态的常量实例

  // access flags 0x8
  static <clinit>()V//类构造方法初始化
   L0
    LINENUMBER 1 L0
    NEW SingleInstance
    INVOKESPECIAL SingleInstance.<init> ()V//在类构造方法初始化的时候调用了其实例构造方法
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 0
  // compiled from: Main.kt
}

通过上面字节码我们可以总结以下几点:

  1. 使用object修饰后之所以是单例的,是因为其构造方法是private的,我们无法在外部进行对象生成,其对应的字节码摘录如下:
  private <init>()V//私有的构造方法
  1. object修饰的单例是线程安全的,这是因为在其类构造方法初始化的时候就完成了实例的生成,这个是由类加载器来保证的。字节码摘录如下:
static <clinit>()V//类构造初始化方法
   L0
    LINENUMBER 1 L0
    NEW SingleInstance///此处及下面代码生成了唯一的一个SingleInstance实例
    INVOKESPECIAL SingleInstance.<init> ()V
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 0

那么我们是怎么用调用该唯一的一个实例呢?很简单,我们增加一个测试方法,看下其生成的字节码即可,如下所示:

fun test(){
    SingleInstance//这种写法是允许的,我们只是想看看其对应该的字节码而已
}

其对应的字节码如下所示:

  public final static test()V
   L0
    LINENUMBER 5 L0
    GETSTATIC SingleInstance.INSTANCE :LSingleInstance;//注意这里,我们通过static的方式调用了该实例
    POP
   L1
    LINENUMBER 6 L1
    RETURN
   L2
    MAXSTACK = 1
    MAXLOCALS = 0
  // compiled from: Main.kt
}

上面字节码文件清晰表明,kotlin是通过SingleInstance.INSTANCE的方式使用上述实例的,而SingleInstance.INSTANCE正是SingleInstance类生成的,其字节码摘录如下:

  public final static LSingleInstance; INSTANCE

这正是SingleInstance类的唯一一个实例,也就是我们通常所说的单例。

看完kotlin字节码对应的单例,我们不难想象其对应于java中实现单例的方法,这里顺便给出,方便有的朋友对二者进行比较:

//kotlin单例对应的java单例的写法
public class SingleInstance {
    private SingleInstance() {
    }
    public final static SingleInstance INSTANCE = new SingleInstance();
}

关于单例我们已经讲完了,下面再来看一段关于object的其他用法的源代码,如下所示:

class Test {
    public val obj = object {
        val i = 1
    }
    private val obj1 = object {
        val i = 1
    }
    fun test() {
        Test().obj1.i//正确
        Test().obj.i//!!!错误,无法找到变量i
    }
}

上面代码是极其简单的代码,重点关注,为什么在test方法中的obj1可以访问到其内部属性i,而obj却无法访问到其内部属性i?这个就是前面提到的public和private修饰object返回值不同的问题。下面我们看下生成的相关字节码:

public final class Test {//Test类对应的字节码
  // access flags 0x12
  private final Ljava/lang/Object; obj
  @Lorg/jetbrains/annotations/NotNull;() // invisible
  // access flags 0x11
  public final getObj()Ljava/lang/Object;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 2 L0
    ALOAD 0
    GETFIELD Test.obj : Ljava/lang/Object;
    ARETURN
   L1
    LOCALVARIABLE this LTest; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x12
  private final LTest$obj1$1; obj1

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 2 L1
    ALOAD 0
    NEW Test$obj$1
    DUP
    INVOKESPECIAL Test$obj$1.<init> ()V
    PUTFIELD Test.obj : Ljava/lang/Object;
   L2
    LINENUMBER 5 L2
    ALOAD 0
    NEW Test$obj1$1
    DUP
    INVOKESPECIAL Test$obj1$1.<init> ()V
    PUTFIELD Test.obj1 : LTest$obj1$1;
    RETURN
   L3
    LOCALVARIABLE this LTest; L0 L3 0
    MAXSTACK = 3
    MAXLOCALS = 1
  // access flags 0x19
  public final static INNERCLASS Test$obj$1 null null
  // access flags 0x19
  public final static INNERCLASS Test$obj1$1 null null
  // compiled from: Main.kt
}
//注意这里,kotlin编译器为我们生成了一个新类,对应于obj
// ================Test$obj$1.class =================
// class version 50.0 (50)
// access flags 0x31
public final class Test$obj$1 {

  OUTERCLASS Test <init> ()V

  // access flags 0x12
  private final I i = 1

  // access flags 0x11
  public final getI()I
   L0
    LINENUMBER 3 L0
    ALOAD 0
    GETFIELD Test$obj$1.i : I
    IRETURN
   L1
    LOCALVARIABLE this LTest$obj$1; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x0
  <init>()V
   L0
    LINENUMBER 2 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 3 L1
    ALOAD 0
    ICONST_1
    PUTFIELD Test$obj$1.i : I
    RETURN
   L2
    LOCALVARIABLE this LTest$obj$1; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
  // access flags 0x19
  public final static INNERCLASS Test$obj$1 null null
  // compiled from: Main.kt
}
//注意这里,kotlin编译器为我们生成了一个新类,对应于obj1
// ================Test$obj1$1.class =================
// class version 50.0 (50)
// access flags 0x31
public final class Test$obj1$1 {

  OUTERCLASS Test <init> ()V

  // access flags 0x12
  private final I i = 1

  // access flags 0x11
  public final getI()I
   L0
    LINENUMBER 6 L0
    ALOAD 0
    GETFIELD Test$obj1$1.i : I
    IRETURN
   L1
    LOCALVARIABLE this LTest$obj1$1; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x0
  <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 6 L1
    ALOAD 0
    ICONST_1
    PUTFIELD Test$obj1$1.i : I
    RETURN
   L2
    LOCALVARIABLE this LTest$obj1$1; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
  // access flags 0x19
  public final static INNERCLASS Test$obj1$1 null null
  // compiled from: Main.kt
}

字节码文件比较长,照例,这里找几个关键点分析总结如下:

  1. kotlin编译器同样会为每一个object表达式生成一个新的类,命名规则是所在的类名(Test)+$+字段名+数字(默认1)。其对应的字节码摘录如下所示:
public final class Test$obj$1 //obj对应的类名
public final class Test$obj1$1 //obj1对应的类名
  1. kotlin编译器会在所有的新类中为object中的非private修饰(private的修饰的则不会!)的属性添加一个公有的get方法,字节码摘录如下所示:
//obj对应的字节码
  public final getI()I//公有的getI方法
   L0
    LINENUMBER 3 L0
    ALOAD 0
    GETFIELD Test$obj$1.i : I
    IRETURN
   L1
    LOCALVARIABLE this LTest$obj$1; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
//obj1对应的字节码
  public final getI()I//公有的getI方法
   L0
    LINENUMBER 7 L0
    ALOAD 0
    GETFIELD Test$obj1$1.i : I
    IRETURN
   L1
    LOCALVARIABLE this LTest$obj1$1; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
  1. 从上面第二点得知,kotlin同时都为obj、obj1生成了暴露属性访问的入口(即公有的get方法),那么为什么我们在代码中能通过obj1访问到i,而却不能通过obj访问到i呢?难道kotlin并不是通过暴露的公有方法来访问其内部属性的?

不急,我们直接来看下调用处的字节码即可,但是由于在源码中写obj.i这样的语句是不合法的,也就是无法编译,这样我们就无法看到字节码了,所以我们需要对上述源码做些微小的变更,先贴出来变更的源码:

    fun test() {
        obj1.i
        obj//注意这里,仅仅改成了obj语句,并没有访问其属性i
    }

上面源码是合法的,obj本身可以作为一个语句。那么现在就可以看到二者生成的字节码有什么不同了,对应的字节码摘录如下:

  public final test()V//test方法对应的字节码
   L0
    LINENUMBER 11 L0
    ALOAD 0
    GETFIELD Test.obj1 : LTest$obj1$1;//注意这里Test.obj1的类型
    INVOKEVIRTUAL Test$obj1$1.getI ()I
    POP
   L1
    LINENUMBER 12 L1
    ALOAD 0
    GETFIELD Test.obj : Ljava/lang/Object;//注意这里Test.obj的类型
    POP
   L2
    LINENUMBER 13 L2
    RETURN
   L3
    LOCALVARIABLE this LTest; L0 L3 0
    MAXSTACK = 1
    MAXLOCALS = 1

上面字节码中有几处典型的注释,再次摘录如下:

    GETFIELD Test.obj1 : LTest$obj1$1;//注意这里Test.obj1的类型
    GETFIELD Test.obj : Ljava/lang/Object;//注意这里Test.obj的类型

这里再看就很明了了,对于public修饰的object,在调用的时候实际上被kotlin编译成了其超类类型对象(如果没有超类则就是顶层类,这里就是Any类型的对象),而对于private修饰的object,在调用的时候实际上被kotlin编译成了kotlin为其生成的真正的新类型对象。

这就是private修饰的object和public修饰的object的本质区别!有朋友会说如果是其他两种修饰符呢?即如果是使用protected和internal修饰呢?答案是这两种修饰符和public的效果一致,都是生成其超类类型对象。

另外需要说明的是,所谓超类类型对象即是其继承的父类类型,如果没有继承特定类,在kotlin中则默认继承自Any类型,但是刚刚在字节码中明明看到的是java.lang.Object类型?这是为什么?如下所示:

    GETFIELD Test.obj : Ljava/lang/Object;//注意这里Test.obj的类型,是java.lang.Object,为什么不是Any?

这个答案放在这回答是显而易见的,否则只能说你还未了解kotlin。kotlin实际上是对java的弊端做了诸如语法糖之类的包装,能够让用户简单使用的同时保证了与java百分之百的兼容,而其底层实际上正是被编译成了java的字节码,源码中的Any对应于字节码层面上就是java.lang.Object。

接下来,再来看一下伴随对象的实现原理,照例先上我们要分析的源代码:

class Test {//Test类
    companion object {//在Test类中我们生声明了一个伴随对象
        fun m1() {}
    }
}
//测试方法
fun test() {
    Test.m1()//使用伴随对象
}

然后我们将上面代码生成的字节码先粘贴出来:

// ================Test.class =================
// class version 50.0 (50)
// access flags 0x31
public final class Test {//Test类对应的字节码
  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LTest; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x8
  static <clinit>()V//注意这里类构造初始化方法
    NEW Test$Companion
    DUP
    ACONST_NULL
    INVOKESPECIAL Test$Companion.<init> (Lkotlin/jvm/internal/DefaultConstructorMarker;)V
    PUTSTATIC Test.Companion : LTest$Companion;
    RETURN
    MAXSTACK = 3
    MAXLOCALS = 0
  // access flags 0x19
  public final static LTest$Companion; Companion//这里实际上生成了一个public final staitc 的Companion对象
  // access flags 0x19
  public final static INNERCLASS Test$Companion Test Companion
  // compiled from: Main.kt
}
//kotlin编译器为我们编译的新类
// ================Test$Companion.class =================
// class version 50.0 (50)
// access flags 0x31
public final class Test$Companion {
  // access flags 0x11
  public final m1()V//注意m1方法并不是static的
   L0
    LINENUMBER 3 L0
    RETURN
   L1
    LOCALVARIABLE this LTest$Companion; L0 L1 0
    MAXSTACK = 0
    MAXLOCALS = 1

  // access flags 0x2
  private <init>()V//私有构造方法,注定无法自己生成伴随对象
   L0
    LINENUMBER 2 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LTest$Companion; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1001
  public synthetic <init>(Lkotlin/jvm/internal/DefaultConstructorMarker;)V//这里调用了上面的私有构造方法
   L0
    LINENUMBER 2 L0
    ALOAD 0
    INVOKESPECIAL Test$Companion.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LTest$Companion; L0 L1 0
    LOCALVARIABLE $constructor_marker Lkotlin/jvm/internal/DefaultConstructorMarker; L0 L1 1
    MAXSTACK = 1
    MAXLOCALS = 2
  // access flags 0x19
  public final static INNERCLASS Test$Companion Test Companion
  // compiled from: Main.kt
}


// ================MainKt.class =================
// class version 50.0 (50)
// access flags 0x31
public final class MainKt {
  // access flags 0x19
  public final static test()V
   L0
    LINENUMBER 8 L0
    GETSTATIC Test.Companion : LTest$Companion;
    INVOKEVIRTUAL Test$Companion.m1 ()V
   L1
    LINENUMBER 9 L1
    RETURN
   L2
    MAXSTACK = 1
    MAXLOCALS = 0
  // compiled from: Main.kt
}

结合字节码以及上面使用伴随对象的一些限制,我们来看下其背后的原理,梳理如下:

  1. kotlin编译器同样会为伴随对象生成一个新类,该类的命名规则是伴随对象所属的类名+$+Companion,而这个类有个私有的构造方法,因此我们无法自己生成伴随对象,对应的字节码摘录如下所示:
public final class Test$Companion //kotlin为伴随对象生成的新类
private <init>()V//构造方法是私有的
  1. kotlin在外部类(Test)进行类初始化的时候,就完成了对伴随对象的初始化,这就是说伴随对象的初始化时机是和其外部类静态成员一致,其对应的字节码摘录如下:
//Test类对应的类初始化构造方法
  static <clinit>()V
    NEW Test$Companion//这里及以下语句生成了一个Test$Companion类型对象,即是我们所用的伴随对象
    DUP
    ACONST_NULL
    INVOKESPECIAL Test$Companion.<init> (Lkotlin/jvm/internal/DefaultConstructorMarker;)V
    PUTSTATIC Test.Companion : LTest$Companion;
    RETURN
    MAXSTACK = 3
    MAXLOCALS = 0
  1. 我们在调用伴随对象的方法的时候,实际上是使用第二步中生成的实例对象进行调用的,这也就是上面所说的伴随对象中的方法和static方法的本质区别。要证明这一点需要看两处字节码,分别摘录如下:
//这是test测试方法对应的字节码文件
   public final static test()V
   L0
    LINENUMBER 8 L0
    GETSTATIC Test.Companion : LTest$Companion;//这里获取了一个LTest$Companion类型的静态变量
    INVOKEVIRTUAL Test$Companion.m1 ()V//这里是通过Test$Companion类型实例来完成调用的
   L1
    LINENUMBER 9 L1
    RETURN
   L2
    MAXSTACK = 1
    MAXLOCALS = 0
//这是Test类对应的一部分字节码
  public final static LTest$Companion; Companion//这句字节码就是上段代码中获取到的LTest$Companion类型的静态变量

上面字节码已经很清楚了,在伴随对象所属的类中,kotlin编译器为其生成了一个public final static的伴随对象,在调用伴随对象中的方法m1时,就是通过该实例进行调用的。

那么如果为m1方法加上@JvmStatic注解修饰呢?如下所示:

//这里使用了@JvmStatic来修饰m1方法
class Test {
    //Test类
    companion object {
        //在Test类中我们生声明了一个伴随对象
        @JvmStatic fun m1() {
        }
    }
}

我们只需要看下m1方法对应生成的字节码即可,如下所示;

  public final static m1()V//注意这里,变成了public final static
  @Lkotlin/jvm/JvmStatic;()
   L0
    GETSTATIC Test.Companion : LTest$Companion;
    INVOKEVIRTUAL Test$Companion.m1 ()V
    RETURN
   L1
    MAXSTACK = 1
    MAXLOCALS = 0

上面字节码表明,使用@JvmStatic注解修饰的方法,会被kotlin编译器真正的编译成static方法!这也正式是使用@JvmStatic修饰和不使用@JvmStatic修饰的伴随对象方法之间的本质区别。

至此,kotlin的object相关内容已经阐述完毕。

上一篇下一篇

猜你喜欢

热点阅读