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
   @Nullable
3
   private static String s;
4
   public static final Singlton INSTANCE;
5
6
   @Nullable
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
   		@NotNull
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
        @JvmStatic
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中的一些特性和关键字是怎么工作的。