函数定义
在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的字节码,输出如下:
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
}
编译后得到的字节码:
可以看出来,原本main()函数中仅仅包含一个函数调用,而编译后却有如此多的字节码,很显然内联关键字 inline 发生了作用。其实,这些字节码的逻辑与高阶函数 advance() 函数的逻辑完全一致。
取消advance()函数的inline关键字后的字节码如下:
可以看出字节码的指令数量少了许多,当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源码文件,得到字节码文件,查看字节码文件如下:
在字节码文件中,生成了两个方法 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层面,打上该标记的属性都是全局性的,这就确保了单例对象中的属性不会随着单例对象的不同而不同,换言之,虽然单例类型仍然可以通过反射等技术打破单例模式,但是单例对象中的属性却依然保持其全局唯一性。