kotlin机制

函数定义

在kotlin中,函数终于成为一等公民,支持面向过程终于在 “JAVA”阵营中成为了现实。

顶级函数

在kotlin中,可以将函数直接定义在源文件中,这种函数就被称为 “顶级函数”。顶级函数不像java函数那样,只能被封装在类中。然而,它仅仅只是一个语法糖,在本质上,顶级函数其实还是被封装了,因为kotlin整个源文件都被看作成一个类,从字节码可以验证这一点。
实例:
定义一个Test.kt文件,文件内容如下:

fun main() {
    println(add(33,54))
}

fun add(a: Int, b: Int): Int {
    return a + b
}

编译源码在编译后的out目录中找到 Test.class 文件 ,在该目录打开命名工具,使用命令:
javap -verbose TestKt.class 查看编译后的字节码,主体如下:

{
  public static final void main();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=0
         0: bipush        33
         2: bipush        54
         4: invokestatic  #13                 // Method add:(II)I
         7: istore_0
         8: iconst_0
         9: istore_1
        10: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_0
        14: invokevirtual #25                 // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 2: 0
        line 3: 17

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #9                  // Method main:()V
         3: return

  public static final int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: ireturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0     a   I
            0       4     1     b   I
}

字节码文件中的 main 和 add 方法都是 “public static final” 修饰符修饰的,说明这两个方法都是公共的,静态的且不可修改的,但是在java中即使使用了“static”修饰了,也必须将其声明在一个具体的类中,而不能像kotlin这样,被声明成顶级函数。其实,kotlin的顶级函数就是直接与kotlin文件被编译后所生产的类绑定在一起的,作为其静态变量。在这个例子中,main()函数调用add()函数的字节码指令如下:

4: invokestatic #13 // Method add:(II)I

该指令其实与java中的静态方法调用的方法完全一致,只不过在本例中还看不出kotlin顶级函数与类相绑定的效果,所以需要对这个例子进行修改,修改后的程序清单,变成了两个类,详情如下:

Test.kt

fun main() {
    println(add(33,54))
}

TopLevelFunction.kt

fun add(a: Int, b: Int): Int {
    return a + b
}

从新编译后查看Test.class的字节码,输出如下:

关键在序号4

kotlin的顶级函数本质上仍然被封装于类中,因此kotlin虽然在语法层面支持在类的外部定义函数,但是本质并未打破面向对象的特性。

内联函数

内联函数与lambda表达式一起使用时,为了提升lambda表达式执行的效率,将高阶函数调用的函数声明成内联函数,从而避免JVM虚拟机为函数类型的变量分配内存。实例如下:

fun main() {
   advance(5,::square)
}

inline fun advance(i: Int, square:(Int)->Int) {
    val product = square(3) + i
    println(product)
}
fun square(x:Int):Int{
    return x*x
}

编译后得到的字节码:

image.png

可以看出来,原本main()函数中仅仅包含一个函数调用,而编译后却有如此多的字节码,很显然内联关键字 inline 发生了作用。其实,这些字节码的逻辑与高阶函数 advance() 函数的逻辑完全一致。
取消advance()函数的inline关键字后的字节码如下:
image.png

可以看出字节码的指令数量少了许多,当advance()函数被声明成内联函数后,main()函数对该函数的调用在编译期被内联,内联后,main函数不在包含对advance函数的调用指令,advance整个函数体被内嵌到main函数中,变成了main函数的字节码指令。其中,原本在advance函数中对其入参变量square的调用,也变成了在main函数中直接调用square函数,这种变化,才是提升效率的关键所在。
在函数square前加上关键字inline后,编译后square也会被内联到main中。
函数内联是一把双刃剑,即有利也有弊,好处自然是更高的执行效率,而坏处则是更大的堆栈内存占用。如果被内联的函数体特别大,则有可能造成调用者函数发生堆栈溢出。

因此,如果不希望被高级函数引用的普通函数也被调用者直接内联,可以通过 noinline 关键字进行解除,实例如下:

fun main() {
   advance(5,::square)
}

inline fun advance(i: Int, noinline square:(Int)->Int) {
    val product = square(3) + i
    println(product)
}

inline fun square(x:Int):Int{
    return x*x
}

变量与属性

kotlin中的变量会被自动包装为属性,编译器会自动为其提供 get/st读写接口。编译器所生成的 get/set 接口到底长啥样?如果自定义 get/set 接口,那接口里的field字段究竟是什么呢?顶级变量和类变量有何不同?对于这些问题,通过观察字节码指令可以找到相应的答案。

属性包装

在Test.kt源文件中,就声明一个变量:

val money: Int = 5

编译Test.kt源码文件,得到字节码文件,查看字节码文件如下:


get/set

在字节码文件中,生成了两个方法 setMoney(int) 和 getMoney(),(如果是val 修饰的就只有get方法)。
由此可知,是编译器自动为kotlin变量生成了 get/set包装器,结合这两个函数内的字节码指令来看,kotlin编译器对变量的处理结果是,如果使用java程序来表达,便类似于下面这种形式:

class ATM {
    private static Integer money;
    public static void setMoney(Integer money){
        ATM.money = money;
    }

    public static Integer getMoney(){
        return ATM.money;
    }
}

在java中建模时绝对不会这么干,不会把模型字段声明成static类型,否则就不是封装了,而且,就算有人这么干,也不一定会为其开发一个set/get 属性包装器。所以kotlin中的顶级字段其实严格意义上来讲,并不属于 “属性”概念。它就是一个全局变量,它已经与类脱离了关系。不过,在语法层面,开发者感受不到全局变量的封装性,因为开发者并不需要通过调用变量对应的set/get方法来访问变量。

既然kotlin会自动为属性提供get/set访问接口,那么在程序中对变量进行读写时,会不会自动调用属性的get/set接口呢?

class Animal{
    var name:String? =null
}

fun main() {
    val animal = Animal()
    animal.name = "长颈鹿"
    println("animal name = ${animal.name}")
}

定义了一个Animal类,声明了一个属性name,在main函数中先为name属性写入一个值,在通过println来读取name的值,编译该类,并用javap命令查看字节码:

{
  public static final void main();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=3, args_size=0
          //实例化animal类
         0: new           #11                 // class Animal
         3: dup
         4: invokespecial #14                 // Method Animal."":()V
         7: astore_0
         8: aload_0
         9: ldc           #16                 // String 长颈鹿
           //执行animal.name = "长颈鹿"
        11: invokevirtual #20                 // Method Animal.setName:(Ljava/lang/String;)V
        14: new           #22                 // class java/lang/StringBuilder
        17: dup
        18: invokespecial #23                 // Method java/lang/StringBuilder."":()V
        21: ldc           #25                 // String animal name =
        23: invokevirtual #29                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        26: aload_0
        //调用 animal.getname 完成属性name的读取
        27: invokevirtual #33                 // Method Animal.getName:()Ljava/lang/String;
        30: invokevirtual #29                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        33: invokevirtual #36                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        36: astore_1
        37: iconst_0
        38: istore_2
        39: getstatic     #42                 // Field java/lang/System.out:Ljava/io/PrintStream;
        42: aload_1
        43: invokevirtual #48                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        46: return
      LineNumberTable:
        line 6: 0
        line 7: 8
        line 8: 14
        line 9: 46
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            8      39     0 animal   LAnimal;

通过字节码可以看出,kotlin编译器将 animal.name = "长颈鹿"最终解析成通过调用animal.setName接口来完成对name属性的写入。同时解析成通过animal.getName()来完成对name属性的读取。kotlin通过接口对属性进行读写的严格约束,为面向对象封装做了最好的诠释。

延迟初始化

在声明非空类型的属性时必须要对其进行初始化,为了不赋初值,可以有好几种写法。其实还有一种方式可以声明属性而无须赋初值,那就是延迟初始化。
要延迟初始化,只需要在类属性前面使用lateinit关键字进行修饰即可,例如:

lateinit var name : String

需要注意的是lateinit关键字不能用于修饰kotlin的原生类型,例如下面这样写就不行:

lateinit var weight : Int

既然lateinit关键字可以使得在声明属性时无须赋初值,那么在使用时如果尚未赋初值,会出现什么样的后果呢?,要知道kotlin在空指针异常校验这方面可是下了很大功夫的,举例如下:

class Animal{
    lateinit var name:String
}

fun main() {
    val animal = Animal()
    println("animal name = ${animal.name}")
}

运行的结果就是程序报错:

xception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property name has not been initialized at Animal.getName(Test.kt:2)

从报错的信息来看,引起程序异常的原因是延迟初始化的属性未被初始化。由此可见,lateinit关键字并非仅仅是静态编译期的一个标识符那么简单,这个关键字会使系统在运行期对属性字段加以校验,如果在运行期,一个延迟初始化的属性在被使用前还未被初始化,则该关键字会趋势系统抛出异常。
这个关键字是如何做到将其影响力从编译期一直带到运行期的呢?
查看编译后文件的字节码文件,在Animal.class中的getName方法中发现了抛出异常的地方:

 public final java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #11                 // Field name:Ljava/lang/String;
         4: dup
         5: ifnonnull     13
         8: ldc           #12                 // String name
        10: invokestatic  #18                 // Method kotlin/jvm/internal/Intrinsics.throwUninitializedPropertyAccessException:(Ljava/lang/String;)V
        13: areturn
      StackMapTable: number_of_entries = 1
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/String ]
      LineNumberTable:
        line 2: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   LAnimal;
    RuntimeInvisibleAnnotations:
      0: #7()

分析字节码中的指令,编号5 判断如果属性的值不为空,就直接跳到13行执行,如果为空就继续执行到第10行指令,抛出异常。这便是lateinit 关键字将其生命周期延伸到运行期的秘密所在。
为了对比,将上面的Animal类中的name属性的lateinit关键字去掉,从新编译得到getName的字节码如下:

 public final java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #11                 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 2: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LAnimal;
    RuntimeInvisibleAnnotations:
      0: #7()

let语法糖

kotlin拥有非常强大的完全校验机制,如果一个变量被声明为可空类型(例如字符串对应的可空类型为String?),那么是不能直接在程序中使用该变量的,需要使用let{}块包住它。
let{}块其实就是一个语法糖,由编译器负责解释,生产特定的字节码指令。任然以前面Animal文件为例子:

class Animal {
    var name: String? = null
    var height: Int? = null

    fun test(){
        name?.let {
            println("name = $name")
            println("height = $height")
        }
    }
}

这个例子中的test函数为了能够安全地使用name属性,使用let{}块,这样编译器就不会报错,编译该程序,得到Animal.class字节码文件,使用javap查看test函数的字节码:

  public final void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=8, args_size=1
         0: aload_0
         1: getfield      #11                 // Field name:Ljava/lang/String;
         4: dup
         5: ifnull        93
         8: astore_1
         9: iconst_0
        10: istore_2
        11: iconst_0
        12: istore_3
        13: aload_1
        14: astore        4
        16: iconst_0
        17: istore        5
        19: new           #28                 // class java/lang/StringBuilder
        22: dup
        23: invokespecial #31                 // Method java/lang/StringBuilder."":()V
        26: ldc           #33                 // String name =
        28: invokevirtual #37                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        31: aload_0
        32: getfield      #11                 // Field name:Ljava/lang/String;
        35: invokevirtual #37                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        38: invokevirtual #40                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        41: astore        6
        43: iconst_0
        44: istore        7
        46: getstatic     #46                 // Field java/lang/System.out:Ljava/io/PrintStream;
        49: aload         6
        51: invokevirtual #52                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        54: new           #28                 // class java/lang/StringBuilder
        57: dup
        58: invokespecial #31                 // Method java/lang/StringBuilder."":()V
        61: ldc           #54                 // String height =
        63: invokevirtual #37                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        66: aload_0
        67: getfield      #22                 // Field height:Ljava/lang/Integer;
        70: invokevirtual #57                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
        73: invokevirtual #40                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        76: astore        6
        78: iconst_0
        79: istore        7
        81: getstatic     #46                 // Field java/lang/System.out:Ljava/io/PrintStream;
        84: aload         6
        86: invokevirtual #52                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        89: nop
        90: goto          94
        93: pop
        94: return
      StackMapTable: number_of_entries = 2
        frame_type = 247 /* same_locals_1_stack_item_frame_extended */
          offset_delta = 93
          stack = [ class java/lang/String ]
        frame_type = 0 /* same */
      LineNumberTable:
        line 6: 0
        line 7: 19
        line 8: 54
        line 9: 89
        line 6: 90
        line 10: 94
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           16      73     4    it   Ljava/lang/String;
           19      70     5 $i$a$-let-Animal$test$1   I
            0      95     0  this   LAnimal;

在字节码文件中 偏移量为5的字节码指令,该指令是 “ifnull 93” 。ifnull 这条指令的含义是:如果站定数据为空值,就执行跳转,跳转到ifnull这条指令后面所跟的一个数字所表示的指令,这里就是如果站顶的值是空的就跳转到偏移量是93的指令 并执行。 93的指令是pop,即是弹出站顶数据,接着就是return,退出函数了。
所以let{}块的作用就是判断使用let的属性是否为空,如果为空,就不执行let{}块中的代码。
这便是在kotlin中可以安全地使用可空变量的机制所在。

类定义

虽然在kotlin中可以直接声明顶级函数,这让kotlin看起来像是面向过程的编程语言,但是kotlin其实仍然是面向对象的语言,比较底层直接基于JVM虚拟机。正式因为这一点,kotlin在语法层面支持使用“类型”来进行封装。但是kotlin的源文件在被编译后,整体被当做一个类型,那么问题来了:如果在kotlin源码中再显示的定义一个类型,这个被显示定义的类型究竟算什么?会不会与java中的内部类是一个性质呢?

java 内部类

为了搞清楚上面这个问题,我们先对java程序中的额内部类进行深入的研究。程序清单如下:

import java.util.Map;
public class Cache {
    private Map container;
    public Object get(String key){
        return container.get(key);
    }
    private final class Slot{
        long q0,q1,q2,q3,q4,q5,q6,q7,q8,q9,qa,qb,qc,qd,qe;
        public Slot(){
            
        }
        public Object get(String key){
            return container.get(key);
        }
    }
}

编译后其实生成了两个字节码文件,一个是Cache.class,一个是Cache Slot.class,这两个类彼此独立,但是在数据上存在内部联系,例如,在内部类Slot的成员函数中可以直接访问其外部类Cache中的成员变量。内部类之所以能够访问外部类的成员变量,其实是编译器偷偷做了手脚。使用javap命令查看Cache$Slot.class字节码文件:

{
  final Cache this$0;
    descriptor: LCache;
    flags: ACC_FINAL, ACC_SYNTHETIC

  public Cache$Slot(Cache);
    descriptor: (LCache;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:LCache;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."":()V
         9: return
      LineNumberTable:
        line 12: 0
        line 14: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LCache$Slot;

  public java.lang.Object get(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: getfield      #1                  // Field this$0:LCache;
         4: invokestatic  #3                  // Method Cache.access$000:(LCache;)Ljava/util/Map;
         7: aload_1
         8: invokeinterface #4,  2            // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
        13: areturn
      LineNumberTable:
        line 16: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   LCache$Slot;
            0      14     1   key   Ljava/lang/String;
}

在第一行的“final Cache this 0” 表示在Cache Slot类中声明了一个被final修饰的类成员变量 this 0。但是,在上面显示的源码中,并没有人工定义这个变量,很显然,这个变量是编译器自动加上的。这个成员变量的类型是Cache,有了Cache类型的成员变量的实例引用,在Cache的内部类Slot的成员方法中就能访问Cache中的成员变量了。
但是内部类Slot的成员方法若要访问this$0成员变量中的成员变量,得有一个前提,那就是首先要实例化this0变量。那么实例化的动作是在哪里完成的呢?
其实也简单通过 Slot的字节码文件可以看出,编译器自动为Slot插入了一个带Cache入参的构造函数。这个构造函数完成了对this 0 的初始化。

kotlin中的类

首先定义一个kotlin的源文件 Test.kt ,内容如下:

val a : Int = 3
class Test {
    fun put(m:Int){
        println("===========set key = $m")
    }
}

编译后再输出目录总,产生了两个class文件,一个TestKt.class,一个Test.class。kotlin源码经过编译后,所生成的类名是kotlin源码文件的名称加上“Kt”后缀,因此可以确定,Test.class ,必定是在Test.kt源程序中显示定义的Test类,那么,这个显示定义的Cache类究竟算不算是java里的“内部类”呢?
仍然使用javap命令分析TestKt.class,在输出的结果中搜索有没有命为“InnerClasses”的tag属性。经过测试,TestKt.class中并没有这个属性,因此可以确定,这个显示定义的Test类并不等同于Java中的内部类。
为了进一步证明,可以另外写一个测试程序。

fun main() {
    val test = Test()
    test.put(4)
}

在另一个源程序中,实例化Test类,并查看这个main函数的字节码:

 public static final void main();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=1, args_size=0
         0: new           #11                 // class Test
         3: dup
         4: invokespecial #14                 // Method Test."":()V
         7: astore_0
         8: aload_0
         9: iconst_4
        10: invokevirtual #18                 // Method Test.put:(I)V
        13: return
      LineNumberTable:

从字节码中可以看出,是直接对Test类进行的初始化,并没有像java内部类那样,对外部类进行了初始化,并传给内部类。因此可以进一步证明,在kotlin源程序中显示定义的类并非被处理成内部类,而是会被当做普通的类来处理。这种类与普通的java类最大的不同之处在于,java类被编译后,直接生产对应的字节码文件,而kotlin则会另外生成一个字节码文件。

kotlin类对顶级属性和方法的访问

在kotlin源程序中声明的顶级方法和属性,默认具备public和static全局性质,因此无论在kotlin源程序内部还是外部,都可以直接访问。那么对于kotlin中显示定义的类型,该如何访问呢,编译器有没有进行特殊处理?
编写一个如下的测试类:

var a: Int = 3
fun add(x: Int, y: Int): Int {
    return x + y
}

class Test {
    fun add(c: Int) {
        val sum = a + c
        a = 5
        println("sum = ${add(a, sum)}")
    }
}

在这段kotlin程序中,声明了一个顶级方法add(int,int),一个顶级属性a,在kotlin源程序中显示声明的类型Test的add(int)方法内部,对顶级属性和顶级方法都进行了访问,其中还对顶级属性a进行了写操作。
编译这段程序,使用javap命令查看Test类中add(int)方法的字节码内容:

{
  public final void add(int);
    descriptor: (I)V
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=3, locals=5, args_size=2
         0: invokestatic  #12                 // Method TestKt.getA:()I
         3: iload_1
         4: iadd
         5: istore_2
         6: iconst_5
         7: invokestatic  #15                 // Method TestKt.setA:(I)V
        10: new           #17                 // class java/lang/StringBuilder
        13: dup
        14: invokespecial #21                 // Method java/lang/StringBuilder."":()V
        17: ldc           #23                 // String sum =
        19: invokevirtual #27                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: invokestatic  #12                 // Method TestKt.getA:()I
        25: iload_2
        26: invokestatic  #30                 // Method TestKt.add:(II)I
        29: invokevirtual #33                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        32: invokevirtual #37                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        35: astore_3
        36: iconst_0
        37: istore        4
        39: getstatic     #43                 // Field java/lang/System.out:Ljava/io/PrintStream;
        42: aload_3
        43: invokevirtual #49                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        46: return
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 10
        line 11: 46
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            6      41     2   sum   I
            0      47     0  this   LTest;
            0      47     1     c   I

观察这段字节码内容,可知在Test类内部访问顶级属性啊时,调用了TestKt.getA()方法(偏移量为0的字节码指令),而在写变量的时候,则调用了TestKt.setA:(I)方法(偏移量为7的字节码指令)。
同样的在Test类内部访问顶级方法add(iint,int)时,调用了TestKt.add:(II)方法(偏移量为26的字节码指令)
这段示例程序在对顶级方法和顶级变量读写的同时还进行了验证,由此可以证明,在kotlin类型内部对顶级方法和属性进行访问时,并没有进行任何特殊处理。

kotlin类中的成员变量

kotlin中的顶级变量本质是全局静态变量,因此这样的变量不能称为“属性”。如果想为一个客观事物封装属性,就只能在kotlin中通过类型来解决。为了加强对比,写下如下的程序:

var money : Int = 0

class ATM {
    var money : Int = 0
}

在这个源程序中,定义了一个顶级变量money,为了演示属性封装,定义了一个ATM类,同时在ATM类中声明了一个属性money。编译这个程序,编译后会得到两个class字节码文件,一个ATMKt.class,另一个是ATM.class。使用javap命令查看ATM.class字节码文件:

 public final int getMoney();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #10                 // Field money:I
         4: ireturn
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LATM;

  public final void setMoney(int);
    descriptor: (I)V
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #10                 // Field money:I
         5: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LATM;
            0       6     1    I

通过字节码内容可以看出,kotlin编译器也为类中的属性生成了get/set访问器,但是这些访问器都不是static的,换言之,这些访问器就是java中的类成员方法。从javap的分析结果中看不到有money字段描述,这说明编译后,money字段的访问标识被设置成private了,所以,ATM类中的money字段被编译后,实际上是这样一种效果,这种效果使用java程序来表达:

class ATM{
  private Interger money;//金额
  //存款
  public void setMoney(Interger money){
    ATM.money = money;
  }
  //取款
  public Interger getMoney(){
    return ATM.money;
  }
}

由此可见在kotlin中,类型属性与全局变量是区分的很开的,全局变量没有必要非要在类中声明,而可以直接将其声明成顶级变量。这相对于java语言,无疑是一种巨大的进步。而在java中,不管一个变量是不是全局的,都必须在类中定义,缺失了灵活性,同时也有过于面向对象之嫌。

单例对象

前文在讲解kotlin.unit类型时,声明该类型时所使用得关键字并不是class,而是object。使用object关键字声明一个类型时,声明的是一个单例模式的类型。根据kotlin的官方文档,object是lazy-init的,即延迟初始化,只有在第一次使用时才会加载并实例化它。
单例模式有一个非常著名的讨论——double check,这个稍后再说,先看看kotlin中单例模式的实现机制,下面是一个单例模式的实例:

public object Singleton{
  override fun toString(): String {
        return "singleton"
    }
}

编译后查看字节码内容如下:

{
  public static final Singleton INSTANCE;
    descriptor: LSingleton;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  public java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #9                  // String singleton
         2: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   LSingleton;
    RuntimeInvisibleAnnotations:
      0: #7()

  private Singleton();
    descriptor: ()V
    flags: ACC_PRIVATE
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #15                 // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LSingleton;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: new           #2                  // class Singleton
         3: dup
         4: invokespecial #31                 // Method "":()V
         7: astore_0
         8: aload_0
         9: putstatic     #33                 // Field INSTANCE:LSingleton;
        12: return
      LineNumberTable:
        line 1: 0
}

使用命令:javap -verbose -private Singleton.class查看,加了-private 可以查看private修饰的属性方法。
根据字节码反向出相应的该单例模式的java代码:

public class Singleton{
    /**该字段由编译器自动生成*/
    public  static Singleton INSTANCE;
    /**该构造函数由编译器自动生成,注意其房屋标示是private*/
    private  Singleton(){
        INSTANCE = this;
    }
    
    /**这里的static{}块逻辑对应字节码中的static方法*/
    static {
        new Singleton();
    }
}

值得注意的是在字节码文件中,INSTANCE这个字段被标记成 具有 public ,final,static 三个性质,如果在java源码中真的这么写,则编译器会提示INSTANCE应当在声明时就被初始化。
在java类中,static{}块中的逻辑会在类被加载的过程中被执行,在这个例子总,在static{}块逻辑中直接实例化一个Singleton对象,因此会调用该对象的构造函数。而在构造函数中,通过 INSTANCE = this;,将 INSTANCE 这个静态字段指向所构建的Singleton实例对象,从而完成单例构建。其他地方要使用该类实例时,通过Singleton.INSTANCE获取。
由于一个类型只会被加载一次(除非使用),因此无论客户端调用多少次Singleton.INSTANCE 来获取实例,都不会从新实例化Singleton对象,从而实现单例设计模式。
kotlin中单例模式的使用:

fun main() {
    println(Singleton.toString())
}

查看该main()方法的字节码内容如下:

{
  public static final void main();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=0
         0: getstatic     #15                 // Field Singleton.INSTANCE:LSingleton;
         3: invokevirtual #19                 // Method Singleton.toString:()Ljava/lang/String;
         6: astore_0
         7: iconst_0
         8: istore_1
         9: getstatic     #25                 // Field java/lang/System.out:Ljava/io/PrintStream;
        12: aload_0
        13: invokevirtual #31                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        16: return
      LineNumberTable:
        line 2: 0
        line 3: 16

可以看出实际上是使用了Singleton.INSTANCE ,由此可知kotlin对于单例对象其实又一次使用了障眼法。
在源代码中引用的单例对象类型,之所以不能被实例化,其实是因为单例对象已经是一个“实例对象”,自然不能再次实例化。
想kotlin所实现的这种单例机制,其实并不是最完美的,因为与java中最初实现的单例模式一样,kotlin的这种实现机制会占用内存,java中最初实现的单例模式,基本模型如下:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return INSTANCE;
    }
}

这种实现机制与上面反向推导的kotlin的单例模式实现机制基本类似,其实都是在singleton类型加载期间完成单例实例化,通过类型仅加载一次(仅限同一个类加载器)的机制实现单例模式。但是这两种机制都有一个缺陷——如果类中定义了其他静态资源,程序在引用其他资源时并不想获取其单例,但是系统也会实例化一个类型对象,从而占用内存,虽然一个类型貌似也占用不了多少内存,但是万一类型实例化过程中会大量创建其他对象,例如数据库连接池之类的,那么这种内存占用和计算资源的耗费就是不可估量的,所以大家不能忍受这种单例模式的实现机制。
后来经对单例模式的多次改造,大部分都会有多线程环境的问题,最终沉淀出一个完美的实现机制——通过内部类实现单例机制,其模型如下:

public class Singleton {
    //定义一个内部类
    private static class LazyHolder{
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton(){}
    public static Singleton getInstance(){
        return LazyHolder.INSTANCE;
    }
}

使用该模式的单例模式完美的避免了多线程问题(其实是利用JVM内部的机制实现了多线程并发解决方案,JVM对一个类的初始化会做同步控制,同一时间只会允许一个线程去初始化一个类,这样就从虚拟机层面避免了大部分单例实现机制所碰到的问题,尤其是“臭名昭著”的double-check机制),同时也不会占用内存。在这种机制下,如果你只想访问单例的其他静态资源,系统不会实例化单例对象。
编写一个测试程序来测试内部类单例模式:

public class Singleton {

    public static final String a = "this is a test value";
    //定义一个内部类
    private static class LazyHolder{
        private static final Singleton INSTANCE = new Singleton();
        static {
            System.out.println("lazy holder");
        }
    }
    private Singleton(){
        System.out.println("constructor");
    }
    public static Singleton getInstance(){
        return LazyHolder.INSTANCE;
    }
    //类加载阶段执行的代码块
    static {
        System.out.println("init");
    }

    public static void main(String[] args) {
        System.out.println(Singleton.a);
    }
}

执行的输出结果:

init
this is a test value

从输出结果可以看出,虽然main函数访问了单例类型中的静态资源,但是并没有触发单例类型的构造函数,并且其内部类也没有被加载。从这个角度看,kotlin的单例设计模式的实现机制,并不是特别优秀。
虽然kotlin对单例模式的实现机制的优化不是那么积极,却对单例对象的属性做了一定的优化——单例对象中的属性都具有static性质,在JVM层面,打上该标记的属性都是全局性的,这就确保了单例对象中的属性不会随着单例对象的不同而不同,换言之,虽然单例类型仍然可以通过反射等技术打破单例模式,但是单例对象中的属性却依然保持其全局唯一性。

你可能感兴趣的:(kotlin机制)