Kotlin的语法糖
接触kotlin两个月时间里,由于kotlin和java的无缝衔接,已经用kotlin开发了部分非核心代码并应用在生产环境里,除了精练简洁,koltin还有些特性和语法糖让开发者写代码更轻松。
1.空安全
Null Pointer Exception(NPE)应该是java程序员见的最多的异常,没有之一,因为GC或者人为置null, 一个对象在你没有察觉的情况下就可能被回收,再使用它的时候就直接崩溃。为了不让程序崩溃,程序员会在代码里添加大量的防御代码。有时对着一行代码,我会纠结一阵子要不要给它加空判断,正常情况下它是不会为空的,但是谁又能把异常情况全部都考虑到呢,加的话代码臃肿不够优雅,不加又担心哪天生产环境多个这么几个没有技术含量的崩溃。后期android使用注解框架@NonNull 和@Nullable虽然可以提醒开发者在可能为null的地方添加空判断,但是有多少开发者是会真正注意这些警告是个未知数。
kotlin或许能缓解我的NPE焦虑症,创建变量的同时就需要声明它的可空性,如果该变量可能为null,使用之前必须判断是否为null,否则代码编译无法通过,而且这些操作用操作符就可以实现。
简单的声明变量的代码:
1 | var a: String? = "adc" |
2 | var b: String = "def" |
字符串a是可空的, 字符串b是非空的,也就是可以给字符串a赋值null, 但是不能给字符串b赋值null:
1 | a = null //正确,可以这样赋值 |
2 | b = null //错误,代码检查工具报错 |
如果要调用一个可能为null的变量的属性,该怎么写呢,两种方法可以做到:
1.用条件语句做判断
1 | var a: String? = "adc" |
2 | val length = if (a != null) a.length else -1 |
3 | print(length) |
2.使用安全调用操作符 ?.
1 | var a: String? = "adc" |
2 | print(a?.length) |
3 | //运行结果 null |
安全调用操作符在这写情况下也非常有用:
a.链式调用
举个栗子,想要获取小明的女朋友的弟弟的身高,但是小明,小明的女朋友,小明的女朋友的弟弟,这几个属性都可能为null,这种情况 java中只能做多级非空判断:
1 | if(xiaoming != null && xiaoming.girlfriend != null && xiaoming.girlfriend.brother != null) { |
2 | int height = xiaoming.girlfriend.brother.height |
3 | } |
kotlina里可以很简洁的获取到:
1 | val height = xiaoming?.girlfriend?.brother?.height |
如果任意一个属性(环节)为null,链式调用就会返回null,是不是简便了些
链式调用也可以出现在表达式的左侧用来给一个属性赋值:
1 | xiaoming?.girlfriend?.brother?.height = getHeight() |
左边链式调用的任意一个属性如果为null,都会跳过赋值,右侧的表达式也不会被调用
b.如果只对非空值执行某个操作,安全调用操作符可以和let一起使用:
1 | val nullableList: List<String?> = listOf("Kotlin", null) |
2 | for (item in listWithNulls) { |
3 | item?.let { println(it) } // 运行结果 kotlin |
4 | } |
或者,kotlin中可以使用用集合的非null过滤器, 只保留集合中的非null元素
1 | val nullableList: List<String?> = listOf("Kotlin", null) |
2 | val nonnullList = nullableList.filterNotNull() |
除了调用变量的属性,程序里会有很多根据是否为null,执行代码块的操作,这时Elvis操作符 ?: 就派上了用途:
和java中的条件运算符?:长的类似,用法还是有些区别
1 | //java中的 条件运算符 |
2 | int length = b.length !=null? b.length : 0 |
3 | |
4 | //kotlin中的Elvis操作符, |
5 | //Elvis操作符 ?: 左侧表达式非null,就返回其左侧表达式,否则返回右侧表达式, |
6 | //当且仅当左侧为null时,才会对右侧表达式求值 |
7 | val length = b.length ?: -1 |
如果某个值为null需要停止程序运行,并且抛出 Null Pointer Exception, 那就使用非空断言运算符 !!
1 | //b如果为null, throw NPE |
2 | val length = b!!.length |
2.单例一个关键字搞定
java中创建单例有N种写法,饿汉式、懒汉式,双重校验式,静态内部类式…,创建一个单例前,我会思考几分钟用哪种写法比较适合当前的场景,然乎再写个三五行代码搞定。kotlin里创建线程安全的单例,只需要一个object关键字搞定。
和java中object是所有类的父类不同,kotlin中用object声明一个对象,这个对象会被创建为单例的
1 | object Singleton { |
2 | var s:String ?= null |
3 | init { |
4 | //初始化代码块 |
5 | initMethod() |
6 | } |
7 | |
8 | fun initMethod() { |
9 | // init operation |
10 | } |
11 | |
12 | fun foo() { |
13 | //do something |
14 | } |
15 | } |
java中的单例可以有构造函数,但是kotlin中object生命的对象没有构造函数,如果要在对象创建前执行一些操作,可以使用init{}代码块。作为一个有好奇心的程序员,我不禁会问,kotlin怎么做到用一个object就创建单例的呢,如果把kotlin的字节码文件反编译成java代码,可以看到kotlin的代码等价于java中的如下代码:
1 | public final class Singleton { |
2 | |
3 | private static String s; |
4 | public static final Singlton INSTANCE; |
5 | |
6 | |
7 | public final String getS() { |
8 | return s; |
9 | } |
10 | |
11 | public final void setS(@Nullable String var1) { |
12 | s = var1; |
13 | } |
14 | |
15 | public final void initMethod() { |
16 | } |
17 | |
18 | public final void foo() { |
19 | } |
20 | |
21 | static { |
22 | Singlton var0 = new Singlton(); |
23 | INSTANCE = var0; |
24 | var0.initMethod(); |
25 | } |
26 | } |
其实kotlin使用了java中静态内部类的方式创建单例。
3.习惯了java中静态方法的用法, kotlin中怎么办
kotlin和java不同的一点是 kotlin里没有static关键字,对于刚从java转过来的开发者,想要使用类似java中statica关键字的特性该怎么办呢?
包级函数(package-level functions)
如果在java中有一个class,里面所有的方法都是静态的,就像各种Util class,并且这个class会在整个应用程序中各处使用,建议在kotlin中用包级函数(package-level functions)实现:
1 | //SampleClass.kt |
2 | |
3 | package packageA |
4 | fun bar(){ |
5 | println("Method bar") |
6 | } |
只需要创建一个kotlin文件,命名为SampleClass.kt,不需要顶级的class声明,直接在文件里添加函数就行。如果在其它文件比如HelloWord.kt里调用这个函数 bar,需要倒入要调用函数的包名后直接调用就行:
1 | package packageB |
2 | //注意,需要导入包名 |
3 | import packageA.bar |
4 | // HelloWorld.kt |
5 | class HelloWord { |
6 | fun main(args: Array < String > ) { |
7 | //调用函数 |
8 | bar() |
9 | } |
10 | } |
如果把kotin字节码文件反编译成java文件,代码长这样的:
1 | public final class SampleClassKt { |
2 | public static final void bar() { |
3 | String var0 = "Method bar"; |
4 | System.out.println(var0); |
5 | } |
6 | } |
可以看到kotlin使用类似java中的静态方法创建包级函数,但是因为调用包级函数的时候不需要写类名,如果创建太多的包级函数,命名会是一个问题,所以推荐只把整个应用程序范围内都会用到的函数声明为包级函数。
伴生对象(company object)
如果在java中有一个class,它的所有方法是静态的,但这个class不是全局范围使用的,只在部分模块中会用到;
或者一个class它的部分方法是静态的,部分方法是非静态的,例如一个model class有一些静态的数据验证方法;
推荐这两种情况下在kotlin中用伴生对象实现。为了方便伴生对象里的方法在java文件中被调用,方法可以关联@JvmStatic注解。company object 关键字经常在工厂模式中使用:
1 | class Car { |
2 | companion object Factory { |
3 | fun create(): Car = Car() |
4 | } |
5 | } |
伴生对象对象里的方法可以这样被调用:
1 | //在kotlin文件中调用 |
2 | Car.create() |
3 | //在java文件中调用 |
4 | Car.Factory.create(); |
伴生对象的名字可以省略,
1 | class Car { |
2 | //省略名字后的写法 |
3 | companion object { |
4 | fun create(): Car = Car() |
5 | } |
6 | } |
虽然伴生对象的成员看起来其它语言里的静态成员,但是在运行时环境伴生对象还是会被创建实例,把kotlin字节码文件反编译成java代码:
1 | public final class Car { |
2 | public static final Car.Companion Companion = new Car.Companion((DefaultConstructorMarker)null); |
3 | |
4 | public static final class Companion { |
5 | |
6 | public final Car create() { |
7 | return new Car(); |
8 | } |
9 | |
10 | private Companion() { |
11 | } |
12 | public Companion(DefaultConstructorMarker $constructor_marker) { |
13 | this(); |
14 | } |
15 | } |
16 | } |
可以看到被company object修饰的class类似于java中的静态内部类。
如果想让虚拟机也生成静态成员,可以用注解@JvmStatic关联伴生对象内的方法:
1 | class Car { |
2 | companion object Factory { |
3 | |
4 | fun create(): Car = Car() |
5 | } |
6 | } |
添加注解后半生对象内部的成员可以在java文件中更方便的被调用:
1 | //在kotlin中调用 |
2 | Car.create() |
3 | //在java中调用 |
4 | Car.create(); |
添加@JvmStatic注解后编译器既会在虚拟机中创建静态方法,也会创建伴生对象的实例,注解的作用是让编译器额外生成静态的方法。
4.函数命名参数
先看一个简单的函数,用来构建全名称:
1 | //构建称呼的全名 |
2 | private fun buildFullName(isFemale: Boolean, givenName:String, middleName:String,surName:String): String { |
3 | val prefix = if (isFemale) "Mr" else "Ms" |
4 | return "$prefix$givenName·$middleName·$surName" |
5 | } |
java中只能这样调用函数:
1 | buildFllName(true, "William", "Jefferson","Clinton"); |
如果不去看函数的声明,很难区分各个String对应的参数。
Kotlin为了增加函数的可读性,添加了命名参数,传参的时候还可以打乱函数定义时声明的参数顺序:
1 | buildFullName(isFemale = true, surName = "Clinton",middleName = "Jefferson", givenName = "William") |
5.函数默认参数值
继续使用上面的例子,如果一个人没有middle name, java中需要重载函数,声明一个新的没有middleName参数的函数。
在创建不同的构造函数时,这种情况更常见,重载方法会导致这些参数名和类型被重复一遍又一遍,如果函数有注释,还有再多加一遍注释,有时调用一个省略部分参数的重载函数时,还可能会搞不清楚到底用的是哪个。
在kotlin中,可以在声明函数的时候,指定参数的默认值,避免创建重载的函数,添加默认参数值后的函数如下:
1 | //build full name with default value |
2 | private fun buildFullName(isFemale: Boolean, givenName: String, middleName: String = "", surName: String): String { |
3 | val prefix = if (isFemale) "Mr" else "Ms" |
4 | return "$prefix$givenName·$middleName·$surName" |
调用函数的时候可以省略传参给有默认值的参数:
1 | buildFullName(isFemale = true, surName = "Clinton", givenName = "William") |
6.java和kotlin的互相转换
kotlin是jetBrain开发的一种语言,jetBrain 同时也是广泛使用的IDE Intellij Idea的开发厂商,所以Android Studio也对kotlin提供了强大的支持。既可以让java文件转换成kotlin文件,也可以反编译kotlin的字节码文件成java文件,这些功能可以方便的让这两种语言做对比,帮助新接触kotlin的java开发者快速上手kotin使用。
java文件转换成kotlin文件:
选中要转换的java文件,点击cmd + shift + option + K,会自动转换成kotlin文件,转换后的kotlin文件会有一些错误需要处理。只推荐把这种方式作为熟悉kotlin的工具,不建议使用这种方法开发kotlin,因为自动转换很多情况下不是代码的最优写法,自己动手写代码(write the code)永远是学习和理解一门语言最好的方法。
反编译kotlin字节码文件成java文件
选中kotlin文件,依次点击 Tools –> Kotlin –> Show Kotlin Bytecode –> Decompile.
这中方法会让人更好理解kotlin中的一些特性和关键字是怎么工作的。