本文主要介绍kotlin的特性如下:

  • nullsafety
  • primitive is Object too
  • first-class method
  • 扩展函数、属性

在手里拿着铁锤的人看来,世界就像一颗钉子。

多学一门语言, 可以开拓眼界,促使我们换一个角度去看问题。

kotlin汲取了许多优秀的语言的设计经验,上手时满满的熟悉感,让kotlin几乎没有什么学习曲线。许多编程语言都是从实现业务逻辑、解决算法问题的角度去设计的,而kotlin考虑了程序员使用体验,站在了使用者的角度——一门好的编程语言,不应该让使用者去帮它解决设计缺陷,而应该致力于提高使用者的效率。

举个例子,用Java实现读asset目录下的一个文件:

public class Utils {
public static String fileToString(AssetManager assetManager,String fileName) {
InputStream is = assetManager.open(fileName);
final String newline = System.lineSeparator();
try {
BufferedReader reader = new BufferedReader(is);
StringBuffer sb = new StringBuffer((int) file.length() * 2);
String line;
while ((line = reader.readLine()) != null) {
sb.append(line + newline);
}
reader.close();
return sb.toString();
} catch (IOException ioe) {
//log the exception
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
//log the exception
}
}
}
}
}

上面一坨代码中,真正有用的代码只有两行。

public class Utils {
public static String fileToString(AssetManager assetManager,String fileName) {
+ InputStream is = assetManager.open(fileName);
//....
try {
//...
+ return sb.toString();
} catch (IOException ioe) {
//...
} finally {
//...
}
}
}

使用kotlin实现一样的代码:

fun AssetManager.fileToString(filename: String): String {
return open(filename).use {
it.readBytes().toString(Charset.defaultCharset())
}
}

这就是代码的表现力。我想,除非公司根据代码行数给你支付工资,没人会想去写臃肿的Java代码版本。

本文是学习kotlin过程中,产生的一些笔记。

1. nullsafety

NPE 绝对是编程bug界的老大哥,毕竟上百万dollar的身价。

Java 也提供了@Nullable@NonNull的Lint检查,不过对程序员而言,这只是可选项,而且即使在所有的代码中加上了这两个注解,仅仅是会在IDE中产生一条warning。

public boolean isEmpty(@Nullable List list) {
return list.size() == 0;
}

而 kotlin 把 null 当做了一种新的类型,从而可以用类似类型检查的方式,把NPE扼杀在摇篮中。

nullsafety相关的基础语法:

  • Type?
+ var a: Int? = null

- var a: Int = null //not compile
  • 安全访问操作符?.

这个二元操作符的意思是,在操作符左边的值不为null的情况下,访问右边的表达式并返回其值,否则返回null。

val a: Int? = null
//....
a?.toString()
a?.let{ println(it) }
  • Elvis operator (?:)

猫王 Elvis Presley (这脑洞不得不服) ?:
区别于Java中的三元操作符,这是个二元操作符。
如果左值非空,就返回左值,否则返回右值。

val a:Int? = null
//...
val mStr = a?.toString() ?: ""

//等价于
val mStr = if(a!=null) a.toString() else ""

也可以在右边写 return、throw 语句,

val mStr = a?.toString() ?: return false

val mStr = a?.toString() ?: throw IllegalStateException()
  • 强制cast 为非null类型 !!
val a : Int ? = null
a!!.toString()

编译期不会检查null类型了。但是在运行期一样会抛出异常,所以这种写法是治标不治本。

在Android studio中使用自动转化功能生成的kotlin代码,到处都是这种!!

去掉!!可以用以下几个策略:

1.使用 val 代替 var

强迫自己尽可能的使用不可变量。如果是可变变量,它在运行时有可能被赋值为null。

2.使用 lateinit

Android中许多变量需要在onCreate中初始化,这时可以使用 lateinit 修饰属性。

访问未初始化的变量将抛出UninitializedPropertyAccessException

private lateinit var mAdapter: RecyclerAdapter<Transaction>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mAdapter = RecyclerAdapter(R.layout.item_transaction)
}

fun updateTransactions() {
mAdapter.notifyDataSetChanged()
}

对于基础数据类型Int、Double,lateinit 不能使用,可以用delegates

private var mNumber: Int by Delegates.notNull<Int>()

3.使用 let function

let是kotlin std中自带的几个顶层函数,经常和安全操作符?.配合使用。

private var mPhotoUrl: String? = null

fun uploadClicked() {
mPhotoUrl?.let { uploadPhoto(it) }
}

4.自定义 let function

if (mUserName != null && mPhotoUrl != null) {
uploadPhoto(mUserName!!, mPhotoUrl!!)
}

可以模仿let写一个函数ifNotNull,这个函数接收带有两个参数的lambda类型。

fun <T1, T2> ifNotNull(value1: T1?, value2: T2?, bothNotNull: (T1, T2) -> (Unit)) {
if (value1 != null && value2 != null) {
bothNotNull(value1, value2)
}
}

调用如下

ifNotNull(mUserName, mPhotoUrl) {
userName, photoUrl ->
uploadPhoto(userName, photoUrl)
}

5.使用 Elvis operator

fun getUserName(): String {
if (mUserName != null) {
return mUserName!!
} else {
return "Anonymous"
}
}

变成

fun getUserName(): String {
return mUserName ?: "Anonymous"
}

6.just crash

有时候 crash 也是必需的,但是除了直接抛KotlinNullPointerException,还可以用requireNotNull 或者 checkNotNull 封装异常信息。比如:

uploadPhoto(intent.getStringExtra("PHOTO_URL")!!)

去掉!!, 把Crash Exception封装下:

uploadPhoto(requireNotNull(intent.getStringExtra("PHOTO_URL"), { "Activity parameter 'PHOTO_URL' is missing" }))

查看kotlin编译出来的bytecode 就可以知道,这个 null check的特性是付出了代价的。在每次调用有非空参数的函数时、或者返回非空值的return语句前,都插入了下面一条类似这样的命令Intrinsics.checkXXXXIsNotNull,自动帮我们check了一遍非空。

INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V

2. 没有primitive 数据类型

Java 认为万物都是对象,并创建了 Object 这个顶层类,但是primitive数据类型单独处理。

“凡动物一律平等
但是有些动物比别的动物更加平等”
——From: 乔治·奥威尔(George Orwell). 《动物农场》.

  • primitive 存储在local stack空间,Object存储在 heap 空间
  • primitive 作为参数是传值,Object作为参数是传引用

为了打通 primitive 数据类型和 Object,Java使用了一个包装类型作为bridge,使得这两个类型之间可以自动转化。

kotlin 的类型系统直接舍弃了 primitive 数据类型,真正意义上的万物都是对象。

在kotlin中可以直接在数据类型上调用、定义方法。

println(1498621482L.toDateString())
//(`toDateString`是`Long`的自定义拓展方法)

除此之外 Int 到 Long 到 Double的转化必须显式调用toXXXX()
kotlin的数据类型底层仍然是primitive数据:

val a = 1

val b = 1L

反编译为Bytecode,再翻译为Java如下

private final int a = 1;
private final long b = 1L;

public final int getA() {
return a;
}

public final long getB() {
return b;
}

3. first-class method

在kotlin中,可以把函数作为参数、返回值,也就是说,我们可以在kotlin中写高阶函数了。为了实现这个特性,kotlin提出了函数类型 () -> Type ,类似OC中的block。举例说明

fun trace(sectionName: String, body: () -> Unit){
Trace.beginSection(sectionName)
body()
Trace.endSection()
}

这里的body就是一个函数类型参数。

另外kotlin可以像Java8中的一样将lambda表达式赋值给java版的SAM-interface,或者赋值给kotlin的函数类型变量。

还是上面的例子,调用trace方法

trace("render",{
initView()
})

当lambda是最后(或唯一)一个参数时,可以把lambda大括号移到括号外面来。

trace("render") {
initView()
}

这样简化有两个好处

  1. 减少括号的层级。
  2. 可以用kotlin直接写出类似DSL的声明式代码。例如,封装下okhttp,调用get时可以这样写:
get {
onSuccess = ...
onError = ...
header = ...
}

4. 扩展函数、属性

OCP

Software entities should be open for extension,but closed for modification
对扩展是开放的,对修改是关闭的

大多数Java项目,一定有一个或多个Utils类,里面一堆静态方法。当不确定某个helper方法写到哪儿时,new 一个Utils类太有诱惑力了。大家理直气壮地说,JDK也是这么干的,你看java.utils.**包下的都是这样写的啊。

Utils类为什么不好:

  1. 不符合OOP
  2. 不符合SOLID中的OCP、SRP原则
  3. 静态方法对单元测试不友好

不写Utils类,不用继承和组合,静态语言还能能像动态语言那样动态的扩展库函数吗?能。

在Objective-C中可以通过category给一个类增加方法,甚至覆盖方法。在kotlin中的扩展和OC的类似,但又有点区别。

  • 扩展属性
public var TextView.text: CharSequence
get() = getText()
set(v) = setText(v)
  • 扩展函数
fun Long.toDateString(dateFormat: Int = DateFormat.MEDIUM): String {
val df = DateFormat.getDateInstance(dateFormat, Locale.getDefault())
return df.format(this)
}

调用代码

println(1498621482L.toDateString())

不像OC修改了meta类的方法list,kotlin扩展特性的实现原理其实很简单。

上面的toDateString “反编译”为Java代码后

public final class ExtentionUtilsKt {
@NotNull
public static final String toDateString(long $receiver, int dateFormat) {
DateFormat df = DateFormat.getDateInstance(dateFormat, Locale.getDefault());
String var10000 = df.format(Long.valueOf($receiver));
Intrinsics.checkExpressionValueIsNotNull(var10000, "df.format(this)");
return var10000;
}
}

其实和我们自己写Utils静态方法一模一样。可以看出,kotlin的扩展属性就是语法糖。
当然,这种实现可以理解的,毕竟Java本身都没法舍弃Utils。kotlin没有重新实现一个Java,它只是在bytecode的基础上,提供了新的语法糖。

在了解实现细节后,显然,下面两个关于动态扩展的局限就很好理解了:

  1. 当扩展函数和成员函数有相同的函数签名时(override),成员函数优先于扩展函数。
  2. 扩展函数是静态分发的,总是使用函数的声明类型来决定调用哪个扩展函数

第一条,我们没有修改扩展类receiver的类结构,在调用同签名函数时,编译期会先查找receiver自己的方法表,只有没找到时,才会调用成Utils的静态方法。

第二条,看下面例子

open class C

class D: C()

fun C.foo() = "c"

fun D.foo() = "d"

fun printFoo(c: C) {
println(c.foo())
}

printFoo(D()) //c

输出c,为什么不是d,这里不是子类型多态吗,难道多态在kotlin中不生效了?
验证一下,复写toString方法

open class C {
override fun toString(): String {
return "C"
}
}

class D : C() {
override fun toString(): String {
return "D"
}
}

fun C.foo() = "c"

fun D.foo() = "d"

fun printFoo(c: C) {
println(c.foo())
println(c)
}

printFoo(D())
//c
//D

println(c)输出了正确的结果。

Java的多态是采用方法动态绑定的实现,在编译后,toString方法调用被编码成通过invokevirtual指令调用,invokevirtual会在运行时,去方法表里找对应的方法引用,而此时ctoString指向的是 D.toString 的实现,所以输出D不奇怪。

同样可以解释为什么foo输出的却是c。kotlin中的扩展函数,最终还是变成了某个Utils类中的静态方法,而静态方法的调用是静态绑定的,具体实现是使用invokestatic指令,在编译期,该指令就知道调用方法的引用情况了,具体看下面的Java代码。

@NotNull
public static final String foo(@NotNull C $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
return "c";
}

public static final void printFoo(@NotNull C c) {
Intrinsics.checkParameterIsNotNull(c, "c");
String var1 = foo(c);
System.out.println(var1);
}

5. to be continued…