kotlin入门潜修之类和对象篇—object及其原理
本文收录于 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)
}
}
上面代码需要注意以下几点:
- 如果父类构造方法有入参,则在生成匿名对象时必须要按父类构造方法的定义传参。
- 由于生成的匿名对象有多个父实现,所以在声明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作为属性存在时有一些限制:
- 匿名object作为private属性时,其表达式返回的类型就是object类型
- 匿名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作为声明时需要注意以下几点:
- object作为声明时是没有右值的,所以无法像上一章节中作为表达式那样赋值给其他变量。
- object作为声明时无法声明本地变量,但可以作为类成员存在。
- 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
}
通过上面字节码我们可以总结以下几点:
- 使用object修饰后之所以是单例的,是因为其构造方法是private的,我们无法在外部进行对象生成,其对应的字节码摘录如下:
private <init>()V//私有的构造方法
- 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
}
字节码文件比较长,照例,这里找几个关键点分析总结如下:
- kotlin编译器同样会为每一个object表达式生成一个新的类,命名规则是所在的类名(Test)+$+字段名+数字(默认1)。其对应的字节码摘录如下所示:
public final class Test$obj$1 //obj对应的类名
public final class Test$obj1$1 //obj1对应的类名
- 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
- 从上面第二点得知,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
}
结合字节码以及上面使用伴随对象的一些限制,我们来看下其背后的原理,梳理如下:
- kotlin编译器同样会为伴随对象生成一个新类,该类的命名规则是伴随对象所属的类名+$+Companion,而这个类有个私有的构造方法,因此我们无法自己生成伴随对象,对应的字节码摘录如下所示:
public final class Test$Companion //kotlin为伴随对象生成的新类
private <init>()V//构造方法是私有的
- 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
- 我们在调用伴随对象的方法的时候,实际上是使用第二步中生成的实例对象进行调用的,这也就是上面所说的伴随对象中的方法和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相关内容已经阐述完毕。