从入门到Kotlin

时间:2019-09-23 20:05来源: 操作系统
上篇已提Java中的各种坑。习惯了C#的各种特性和语法糖后,再转到Java感觉比较别扭。最后本着反正Java也不是很熟悉,干脆再折腾折腾其他语言的破罐子破摔的心态,逛了一圈JVM语言,

上篇已提Java中的各种坑。习惯了C#的各种特性和语法糖后,再转到Java感觉比较别扭。最后本着反正Java也不是很熟悉,干脆再折腾折腾其他语言的破罐子破摔的心态,逛了一圈JVM语言,最终决定转Kotlin。

转自:

为何选择Kotlin

  • 项目遭遇人员变动,包括我在内就剩两个人开发,转型成本低,代码质量容易控制。
  • JVM语言。号称与Java 100%兼容。实际使用的确能够与Java几乎无缝地相互调用,基本上可以无缝迁移,完美兼容Java生态。
  • OOP。目前OOP仍是主流,方便后续交接或者其它新加入的开发成员上手。
  • 静态类型。在选择语言的时候也考虑过像Groovy,JRuby等的动态类型语言。然而俗话说得好,动态一时爽,重构火葬场。当项目变大的时候,静态类型支持的较为完善的语义分析能够帮助项目快速整理、重构代码。并且引入很多函数式特性后,静态类型语言的开发效率与爽感,不比动态类型语言低多少。
  • 吸收了一些函数式特性。除了常见的lambda,map,filter,reduce之外,还吸收了ruby的一些如对象上下文切换、代码块语法糖等便捷的特性(但是也可能导致代码可读性下降)。
  • 对JetBrain的信任。JetBrain在静态分析的成果上有目共睹。相信JetBrain设计的语言应该会比较有品位(然而严格得不近人情的null safety是有点让人纠结)。
  • 最后,就是刚好看到Kotlin,确认了眼神……

Kotlin是一门与Swift类似的静态类型JVM语言,由JetBrains设计开发并开源。与Java相比,Kotlin的语法更简洁、更具表达性,而且提供了更多的特性,比如,高阶函数、操作符重载、字符串模板。它与Java高度可互操作,可以同时用在一个项目中。

Kotlin好用的特性

按照JetBrains的说法,根据他们多年的Java平台开发经验,他们认为Java编程语言有一定的局限性和问题,而且由于需要向后兼容,它们不可能或很难得到解决。因此,他们创建了Kotlin项目,主要目标是:

Lambda

牺牲了CE使得Lambda不像Java中那么多的约束。引入类似Ruby代码块的写法,让代码看起来比较好看,虽然我个人不是很喜欢这种默认约定,但是用起来真香。

  • 创建一种兼容Java的语言
  • 编译速度至少同Java一样快
  • 比Java更安全
  • 比Java更简洁
  • 比最成熟的竞争者Scala还简单

面向表达式

不同于其他语言,Kotlin里的if else,try catch等都是表达式,我们可以直接这样子写代码:

val y = if (x % 2 == 0) "even" else "odd"val z = try { readFromFile() } catch (ex: IOException) { "" }

Ashraff Hathibelagal是一名喜欢研究新框架和SDK的独立开发者。近日,他撰文介绍了Kotlin的一些语法。按照他的说法,一个合格的Java程序员可以在很短的时间内学会使用Kotlin。

DSL

  • Lambda是最后一个参数时,可以写在括号外面。主要是用来让回调比较好看,和实现DSL。
val ls = listOfls.map { 2 * it }  // returns [2, 4, 6]
  • Receiver。Kotlin不仅有纯函数类型,还可以通过Receiver声明类的方法类型。这个特性可以用来实现类的方法扩展、this切换的功能。
    下面代码给Int扩展了个double方法:
val double = fun Int.() = 2 * thisval x = 3.double()  // x = 6

下面例子通过切换this实现了一个类似C#初始化对象的方法:

class Obj(init: Obj.() -> Unit) {    var prop1: Int = 0    var prop2: String = ""    init {        init    }}        val obj = Obj {    prop1 = 1    prop2 = "abc"}

类与构造函数

其他

  • 很多好用的方法,像listOfmapOfto操作符等
  • ……

Kotlin创建类的方式与Java类似,比如下面的代码创建了一个有三个属性的Person类:

Kotlin的坑

class Person{
    var name: String = ""
    var age: Int = 0
    var college: String? = null
}

Kotlin没有final,但是有open

Kotlin中Class默认都是不能继承的。需要继承的Class要在声明的地方加上open修饰。另外提一下有个插件叫all-open,专门用来让所有Kotlin的类变为可继承的……

可以看到,Kotlin的变量声明方式略有些不同。在Kotline中,声明变量必须使用关键字var,而如果要创建一个只读/只赋值一次的变量,则需要使用val代替它。另外,为了实现“空安全(null safety)”,Kotlin对可以为空的变量和不可以为空的变量作了区分。在上述代码中,变量nameage不可为空,而表明变量college可以为空。定义完类之后,创建实例就非常简单了:

注解的继承

Kotlin不支持可继承的注解。

var jake = Person()

纯的容器类型

ListMap不能修改其内部存储的元素。需要修改应该用MutableListMutableMap

注意,Kotlin没有关键字new。实例创建完成后,就可以像在Java中一样为变量赋值了:

Lombok

号称和Java 100%兼容,但是不能访问Lombok生成的方法!

因为Lombok的方法是编译期通过注解处理器(annotation processing)生成的,Kotlin编译时只调用了Javac,所以无法处理Lombok定义的方法。强制先编译Java代码,后编译Kotlin代码,可以解决这个问题,但是又会有新的问题:你不能在Java代码中调用Kotlin代码。所以如果你要混合使用Java和Kotlin的话,推荐所有数据类型都用Kotlin写。

jake.name = "Jake Hill"
jake.age = 24
jake.college = "Stephen's College"

valvar

var就是普通变量。val相当于const。平时尽量使用val,有益身心健康。

变量可以采用上述方式赋值,也可以通过构造函数赋值,但后者是一种更好的编码实践。在Kotlin中,创建这样的一个构造函数非常简单:

重头戏,null safety

Null safety是Kotlin宣传得最多的特性,但是我并没有放在“好用的特性”节中介绍,因为它的坑非常多,以至于我十分怀疑null safety的好处是否能抵消它带来的副作用。

  • 所有类型默认都不包括null值,除非加个问号定义为Nullable类型。Nullable类型取值时,强制check null。如果调用Java代码,默认Java代码都是Nullable。不过从Java来的变量不做check null倒是不会报error,只报warning。如果运行时值为null的话,仍然会抛NullPointerException。Kotlin的null safety的特性其实只是一个编译器的特性,通过将null与其他类型区分开来,在类型检查的时候顺便检查了可能出现的NullPointerException,但是在运行时非Nullable的变量实际上也是可以放进去null值的。
  • 由于非Nullable类型不被赋值为null值,导致这些类型的变量可能会没有默认值!这是个严重的问题。如果是像IntString这种比较像值的类型还好,可以有0,空字符串等默认值。而像自定义的类,这种类型的变量其实是个引用,如果不能默认为null的话,那么它的默认值的取值只能有这么几种方案:
    1. 类似C语言,未初始化的随机值:会产生更大更不确定硬隐蔽的问题。
    2. 定义一个“未初始化”的值:那么这个值和null有什么区别?又绕回来了。
    3. 类似C++,默认创建一个空对象:但是并非所有类都有默认构造函数,而且在拥有GC的语言中,创建空对象需要分配内存,还会调用构造函数中的逻辑。声明变量时引入这么多过程是非常不合适的。
    4. 所以,Kotlin最终选了一种简单粗暴的方案:禁止变量未初始化。
      禁止变量未初始化的问题在于,当你需要定义大量的数据类的时候,你就知道有多蛋疼了——所有属性都必须有个初始值。这不仅需要多敲不少键盘,影响手指健康,当碰到属性是非Nullable的聚合时,也常常无法确定其初始值。我已经隐隐看到某些开发人员将所有变量都标记为Nullable的画面了……Kotlin自身也发现了这个问题,因此引入了lateinit特性,然而用起来仍然有点令人胆战心惊。
  • 反序列化。即使是业务逻辑上明确了不会为null值的属性,你也无法保证网络上/数据库里传输过来的数据中,对应的属性会不会是null值,或者干脆漏了,所以就算model设计正确的,实际运行时可能还是会出现NullPointerException。我又隐约看到某些开发人员将所有变量都标记为Nullable的画面了……另外反序列化时,需要先生成一个空对象,也就是属性都没初始化的对象。当然Kotlin不会允许这么做的,所以还需要引入NoArg插件来自动生成无参数的构造函数……
class Person(var name: String, var age: Int, var college: String?) {
}

类型擦除式泛型

为了和Java 100%兼容,Kotlin不得不跟着Java用类型擦除式泛型,也拥有了前面说过的类型擦除式泛型的所有坑。不过Kotlin可以使用内联函数来稍微缓解类型擦除的负面影响。比如可以这样定义json反序列化的方法:
inline fun <reified T> parse(json: String): T = objectMapper.readValue(json, T::class.java)

而实际上,由于构造函数中没有其它操作,所以花括号也可以省略,代码变得相当简洁:

return

Kotlin有两种方法定义一个匿名函数:lambda和anonymous function。当在这两种方法的函数体中使用return时,执行的语义是不同的。根据官方文档return会跳出最近的显示声明的函数或anonymous function。例如下面的return会直接跳出foo函数。

fun foo() {    listOf(1, 2, 3, 4, 5).forEach {        if  return // non-local return directly to the caller of foo()        print    }    println("this point is unreachable")}// outputs: 12

而下面这个只是当value == 3时跳过一次循环,相当于其他语言的continue

fun foo() {    listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) {        if (value == 3) return  // local return to the caller of the anonymous fun, i.e. the forEach loop        print    })    print(" done with anonymous function")}// outputs: 1245 done with implicit label

或者也可以使用Label来指定执行return后跳到的位置(感觉像goto似的)。

fun foo() {    listOf(1, 2, 3, 4, 5).forEach lit@{        if  return@lit // local return to the caller of the lambda, i.e. the forEach loop        print    }    print(" done with explicit label")}

另外,break和continue也是有类似的问题。

class Person(var name: String, var age: Int, var college: String?)

var jake = Person("Jake Hill", 24, "Stephen's College")

写在最后

最近家庭工作都比较忙,这短短的一篇转型踩坑记竟然写了个跨年。有些踩坑的记忆随着时间流逝以及用习惯了给慢慢淡化掉了,于是也没写进来。目前Java系这边的开发我尽量使用Kotlin,并没有碰到什么根本上的大问题,与Java的兼容性也挺好的,有精力的同学可以放心品尝。

上述代码中的构造函数是类头的一部分,称为主构造函数。在Kotlin中,还可以使用constructor关键字创建辅助构造函数,例如,下面的代码增加了一个辅助构造函数初始化变量email

class Person(var name: String, var age: Int, var college: String?) {

    var email: String = ""

    constructor(name:String, age:Int, college: String?, email: String) : this(name, age, college) {
        this.email = email
    }
}

Kotlin允许创建派生类,但要遵循如下规则:

  • 必须使用代替Java中的extends关键字
  • 基类头必须有open注解
  • 基类必须有一个带参数的构造函数,派生类要在它自己的头中初始化那些参数

比如下面的代码创建了一个名为Empoyee的派生类:

open class Person(var name: String, var age: Int, var college: String?) {
    ...
}

class Employee(name: String, age: Int, college: String?, var company: String) : Person(name, age, college) {
}

函数与扩展

有派生就有重载。与类的派生一样,允许重载的方法要有open注解,而在派生类中重载时要使用override注解。例如,下面是在Employee类中重载Person类的isEligibleToVote方法的代码:

override fun isEligibleToVote(): Boolean {
    return true
}

除了改变类的已有行为,Kotlin还允许开发者在不修改类的原始定义的情况下实现对类的扩展,如下面的代码为Person类增加了一个名为isTeenager的扩展:

fun Person.isTeenager(): Boolean {
    return age in 13..19
}

在需要扩展来自其它项目的类时,这个特性特别有用。

上面提到的函数都与Java中的函数类似,但Kotlin还支持其它类型的函数。如果一个函数返回单个表达式的值,那么可以使用=来定义函数。下面是一个创建单表达式函数的例子:

fun isOctogenarian(): Boolean = age in 80 .. 89

Kotlin还支持高阶函数和Lambda表达式。例如,lambda表达式{x,y->x+y}可以像下面这样给一个变量赋值:

val sumLambda: (Int, Int) -> Int = {x,y -> x+y}

而下面的高阶函数将上述表达式作为一个参数,并将表达式的计算结果翻倍:

fun doubleTheResult(x:Int, y:Int, f:(Int, Int)->Int): Int {
    return f(x,y) * 2
}

该函数可以使用下面的其中一种方式调用:

val result1 = doubleTheResult(3, 4, sumLambda)

val result2 = doubleTheResult(3, 4, {x,y -> x+y})

范围表达式

在Kotlin中,范围表达式用的比较多。范围创建只需要..操作符,例如:

val r1 = 1..5
//该范围包含数值1,2,3,4,5

如果创建一个降序范围,则需要使用downTo函数,例如:

val r2 = 5 downTo 1
//该范围包含数值5,4,3,2,1

如果步长不是1,则需要使用step函数,例如:

val r3 = 5 downTo 1 step 2
//该范围包含数值5,3,1

条件结构

在Kotlin中,if是一个表达式,根据条件是否满足返回不同的值,例如,下面的代码将isEligibleToVote设置为“Yes”

var age = 20
val isEligibleToVote = if(age > 18) "Yes" else "No"

when表达式相当于Java的switch,但功能更强大,例如,下面的代码将typeOfPerson设置为“Teenager”:

val age = 17

val typeOfPerson = when(age){
    0 -> "New born"
    in 1..12 -> "Child"
    in 13..19 -> "Teenager"
    else -> "Adult"
}

循环结构

Kotlin使用for..in遍历数组、集合及其它提供了迭代器的数据结构,语法同Java几乎完全相同,只是用in操作符取代了:操作符,例如,下面的代码将遍历一个String对象数组:

val names = arrayOf("Jake", "Jill", "Ashley", "Bill")

for (name in names) {
    println(name)
}

whiledo..while循环的语法与Java完全相同。

字符串模板

Kotlin允许在字符串中嵌入变量和表达式,例如:

val name = "Bob"
println("My name is ${name}") //打印"My name is Bob"

val a = 10
val b = 20
println("The sum is ${a+b}") //打印"The sum is 30"

此外,Kotlin与Java高度可互操作。Kotlin可以用一种自然的方式调用现有的Java代码,而Java也很容易调用Kotlin代码。同时,Kotlin也可以与JavaScript互操作。

上面介绍的只是Kotlin的一些基本语法和特性,更多细节请查阅官方文档。事实上,到目前为止,Kotlin还仍然只是一个预览版本,接下来的几个月中还会有多项重大改进及新增特性。尽管如此,GitHub上已有400多个与Kotlin项目相关的库。

在另外一篇文章中,Hathibelagal写道,“如果你正在为Android开发寻找一种替代编程语言,那么应该试下Kotlin。它很容易在Android项目中替代Java或者同Java一起使用。”想要了解如何在Android Studio中使用Kotlin开发Android项目的读者,可以读下这篇文章。

编辑: 操作系统 本文来源:从入门到Kotlin

关键词: