稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

学习Kotlin,看这一篇就够了

人生苦短,要用Kotlin

这是一种对程序猿更为友好的语言,可以减少开发者的工作量,原本由开发者干的事情,其实很多都可以由编译器实现了,这是一种更为高级的语言。Java虽然严谨,但却过于繁琐,太啰嗦了,一个小事情却要写大量的代码,而且有些代码又是非常机械式的,在实际编码过程中都是用IDE来自动生成。Java,C,C++,Object C这些都是上世纪的编程语言。

现在到了新时代了,编程也发展了很多,像lambda表达式,函数式编程,等等一些新的概念和范式在涌现。所以就有了新时代的编程语言,像水果的Swift,Groovy,Scala,以及Java阵营的Kotlin。Kotlin是新一代的编程语言,与Java完美融合,简洁,方便,可以大大提高程序可读性,特别是对于Android开发者来说。水果推出了Swift以解放水果平台的开发者,而Kotlin就是来解放Android开发者的。

虽然说Kotlin可以用在任何可以用Java的地方,但目前主要就是两大领域服务端,以及Android应用开发,特别是有了Google官方的支持,所以Kotlin对于Android开发者的意义更为重大,身为一个Android猿,是一定要学习一下这门现代的编程语言的,因为当你学过了之后 ,你会发现,之前写的代码都是在浪费生命。

Development environment setup

有三种方式

命令行

其实,这是最好的方式,因为配置起来非常的方便。到官网去下载编译器,解压,然后把kotlinc/bin/放到PATH环境变量里面,就可以了。如果要配置Vim,还需要安装一下插件,大神们早就把插件准备好了,只需要下载,然后按照官方方法安装即可,其实就是把解压后的东西拷贝到相应的目录里面就好了。

Idea IntellJ

这个看官方文档就可以了,孤未亲测,如遇困难请自行Google。

Android Studio

因为Kotlin官已支持了Android Studio,而Google也支持了,总而言之就是在Android Studio中可以直接使用Kotlin。所以, Android Stuido 3.0以后的版本无需特殊配置,就可以用例Kotlin了。

对于刚开始学习Kotlin而言呢,孤推荐使用命令行的方式,而不要使用Android Studio,特别是直接创建一个基于Kotlin的Android项目,因为此时对语言还不够熟悉,直接上项目,会迷失在项目配置,frameworks以及语言基础之中。刚学习一门语言的时候要先学习基本的语法以及语言本身的特性,这最好先绕开框架和项目,会更容易上手一些。

Hello world

这是所有编程语言的入门必学课程,目的是让学习者快速的体验一下一门语言,我们也不用多想,照着一个字母,一个字母的把示例敲进去就好了:

  1. 选择喜欢的文本编辑器,如Vim hello.kt,Kotlin的文件扩展名是*.kt,我们遵循就好。
  2. 一字不差的敲进去:
1
2
3
4
5
package hello

fun main(args: Array<String>) {
    println("Hello, world")
}

然后,保存文件
3. 回到命令行,编译源码,如果一切顺利会得到一个叫hello.jar的文件,这就是kotlin的最终输出,也就是它的目标文件.

1
kotlinc hello.kt -include-runtime -d hello.jar


4. 运行,这里跟Kotlin其实已经没啥关系了,因为经过编译得到的是一个标准的Jar文件,像运行其他jar一样运行就好了:

1
java -jar hello.jar

就会得到输出Hello, world到此,第一个Kotlin程序已经完成,是不是很酷,已经迫不及待的想深入学习了!往下看吧。

The basics

语句结构

一行一个语句(先不纠结语句与表达式的区别),不用加分号,不用打分号,光这个就可以节省多少时间呢?是不是感觉人生都浪费在了分号上面。如果想在一行写多个语句,前面的要加上分号。

缩进规则与Java一致,用四个空格,也可以用tab,或者不加缩进,只要没人打你。

语句块需要加上花括号{}。总之,语句结构与Java很类似。

变量

用var来声明变量,用val来声明常量,因为Kotlin是静态强类型语言(也就是说每个变量在编译的时候必须知道类型)声明时需要带上类型,方法是在变量名的后面加冒号,空格跟上类型名字,与Pascal差不多。如果声明时直接定义,则可以不用指定类型,编译器会根据定义表达式来推测它的类型。示例:

1
2
3
var str: String
val i: Int
var str = "Hello, world"

语句和表达式

主要想说一下语句和表达式的区别,简单来说就是表达式是有值的,可以放在变量赋值的右边,而语句是没有值的,不能放在赋值的右边

基本运算

不多说了,跟Java一样

注释

这个跟Java也一样: // 单行注释 / / 多行注释 /* / documentation

函数

以fun关键字来定义一个函数格式为:fun 函数名(参数): 返回类型 {函数体},如:

1
2
3
fun foo(name: String): Int {
   return name.length()
}

命名参数和默认值,调用函数时可以把参数的名字带上,以增加可读性。声明函数时可以用默认值 ,以更好的支持函数的重载。如:

1
fun foo(name: String, number: Int = 42, toUpper: Boolean = false): String {}

使用时,可以指定参数的名字:

1
2
3
4
foo("a)
foo("b", number = 1)
foo("c", toUpper = true)
foo(name = "d", number = 2, toUpper = false)

表达式体如果一个函数体内只有一个表达式,且有返回值时,那么,可以直接把返回值放在函数 的后面,如:

1
fun foo(name: String): String = name.toUpperCase()

甚至还可以把返回类型的声明给省略掉,如:

1
fun foo(name: String) = name.toUpperCase()

跟Java不一样的是,Kotlin的函数可以声明为toplevel也就是跟class一个级别,也就是说不必非放在类里面,也就是说跟C和C++是类似的。此外,还可以函数赋值给一个变量,这个变量就像其他变量一样。

类与对象

类的声明与对象创建

用class来声明一个类型,用:来继承父类或者实现接口,不需要使用new来创建对象:

1
2
3
4
class Person {
   var name: String
   var age: Int
}

假如,一个类,是空的,没有内容,那么花括号{}是可以省略的:

1
class Person

创建对象:

1
var someone = Person()

Primary constructor

构造方法,有所谓的primary constructor,可以直接写在类名的后面:

1
class Person constructor(name: String)

一般情况下,constructor 可以省略掉:

1
class Person(name: String)

初始化块因为primary constructor不能包含代码,所以,想要做些初始化工作就可以放在初始化块里面(initializer block),也可以在定义属性时直接使用:

1
2
3
4
5
6
class Person(name: String) {
    var firstName: String = name
    init {
        println("First initializer block that prints ${name}")
    }
}

一般情况下,如果声明的属性变量在primary constructor中都有赋值(通过initializer block)的话,可以有更简洁的表达方式:

1
class Person(var name: String, var age: Int)

这相当于:

1
2
3
class Person(theName: String, theAge: Int) {
   var name: String = theName   var age: Int = theAge
}

如果primary construct前面要声明属性,或者有annotation的话,关键字constructor不能省略:

1
class Person public @Inect constructor(var name: String)

Secondary constructor

如果primary constructor不能满足需求怎么办呢?还可以声明其他constructor,所谓的secondary constructor:

1
2
3
4
5
class Person {
   var name: String constructor(name: String{
       this.name = name
   }
}

是不是看起来舒服一些,因为跟Java一样了,可以把primary constfuctor和second constructor联合起来一起用:

1
2
3
4
5
class Person(var name: String) {
    constructor(name: String, parrent: Person) : this(name) {
        parrent.addChild(this)
    }
}

这里要把secondary construct尽可能delegate到primary constructor,这里的delegate的意思就是primary constructor会在second constructor之前 执行,还有就是initiailzer block都是在primary construct中执行的,这就能保证initiliazer block在second constructor之前执行。即使没有显示的声明primary constructor,编译器还是会生成一个默认的primary constructor以及把secondary constructor默认的delegate到primary constrcutor上面。也就是说,会保证primary constructor以及initializer block执行在second constructor前面:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Constructors {
    init {
        println("Initializer block")
    }

    constructor(i: Int) {
        println("second constructor")
    }
}

fun main(args: Array<String>) {
    val c = Constructors(3)
}

输出:

1
2
Initializer block
second constructor

属性和访问方法

Kotlin会为声明的属性生成默认的setter和getter:

1
2
3
4
5
class Person(var name: Strring, var age: Int)

val p = Person("Kevin", 24)
p.getName() // 返回"Kevin"
p.setAge(32) // age变成了32

如果想自定义setter和getter,也是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
    var name: String
        set(n: String) {
            if (n == null || n == "") {
                name = "Unkown"
            } else {
                name = n
            }
        }
        get() {
            if (name == "Unkwon") {
                return "Nobody"
            }
            return name
        }
}

定义类的方法

跟声明普通函数一样,只不过是放在了类里面:

1
2
3
class Person(val name: String, val age: Int) {
    fun report() = "My name is $name, and I'm $age"
}

如果,要覆写父类的方法,需要使用在方法声明时加上override关键字。

1
2
3
class Doggy(val name: String) : Animal {
    override fun yell() = "Barking from $name"
}

访问权限

访问权限也跟Java类似分为public,protected,private以及internal,前三个意义也都一样,只不过默认值不一样,在Java里,如果对成员没有指明,则是package scope,也就是同一个package可以访问,但是Kotlin默认是public的。

internal是module内部可见,有点类似于Java中的package,但是module定义跟package不一样,module是一组编译在一起的Kotlin文件,它跟编译打包有关系,简单的理解它的范围要比package要大。

还有就是类,默认是不可被继承的,相当于final class。如果想要允许继承就要在声明类的时候加上open。

字串

概念就不说了,大部分与Java一模一样的,像支持的方法等。唯一需要说的就是字串模板,就是说把其他类型转化为字串时,有较Java更为方便的方式:直接用$来把变量嵌入到字串之中,如:

1
2
3
4
val msg = "Error 1"
val count = 32
print("We got message $msg") //等同于"We got message " + msg
print("Total is $count") // Total is 32

lambda表达式

首先要介绍一个概念,高阶函数,其实就是把另外函数当作参数的函数,或者说产生一个函数,也即把函数作为返回值 的函数。前面说过,函数是一级对象,可以像常规变量一样来使用,所以,就能把函数作为参数或者返回值来使用高阶函数。lambda表达式就是为高阶函数更方便使用而生的。

lambda 表达式

作为新时代的编程语言,都会支持函数式编程,而lambda表达 式又是函数式编程里面必不可少的一份子。其实啥是lambda表达式呢?说的简单点就是没有名字的函数,非常简短的,通常都是一两句话的没有名字的函数。就是长这个样子{A, B -> C},这里面A,B是参数,C是表达式,如:

1
val sum = { x: Int, y Int -> x + y }

其中,参数的类型是可以省略的,因为编译器能从上下文中推测出来: max(strings, { a, b -> a.length < b.length } 表达式部分,可以不止一个,最后一个表达式作为返回值。

当把一个lambda表达作为最后一参数,传给某个函数时,可以直接把lambda表达式写在参数的外面,比如:

1
val product = items.fold(1) { acc, e -> acc * e }

而当lambda是唯一的参数时,也可以把参数的括号省略掉:

1
run { println("Hello, world") }

还有就是,如果lambda表达中只有一个参数,那么参数也可以省略,直接写表达式:

1
eval{ x * x }

函数类型

前面提到了函数是可以像普通变量一样使用的一级类,也就是说它是一个类型。它的具体形式是: (A, B)->C,其中括号内的是参数,C是返回类型,如:

1
2
val sum: (Int, Int)->Int = { x, y -> x + y }
val square: (Int)->Int = { x -> x * x }

为啥要提一下函数类型呢,因为有时需要声明高阶函数:

1
2
fun walk(f: (Int)->Int)
fun run(f: ()->Unit)

Unit是一个特殊的返回值,相当于void,意思就是此函数没有返回值。

集合

其实大部分跟Java是一样的。只不过有一些函数式的操作,要多注意使用,从而让代码更简洁,如:

  • 遍历
  • 过滤
  • 映射
  • 排序
  • 折叠
  • 分组
  • 归类

这些操作,对于大家应该都不难理解,就不一一解释了,来段code就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun collectionTests() {
    val list = listOf("Apple", "Google", "Microsoft", "Facebook", "Twitter", "Intel", "QualComm", "Tesla")
    // 遍历,以进行某种操作
    list.forEach{ println(it) }
    //按条件进行过滤,返回条件为true的
    val short = list.filter { it.length < 6 }
    println(short) // [Apple, Intel, Tesla]
    // 把列表元素映射成为另外一种元素
    val lenList = list.map{ it.length }
    println("Length of each item $lenList") //Length of each item [5, 6, 9, 8, 7, 5, 8, 5]
    // 按某种条件进行排序
    val ordered = list.sortedBy { it.length }
    println("Sorted by length $ordered") // Sorted by length [Apple, Intel, Tesla, Google, Twitter, Facebook, QualComm, Microsoft]
    // 折叠,用累积的结果继续遍历
    val joint = list.fold("", {partial, item -> if (partial != "")  "$partial, $item" else item })
    println("Joint list with comma $joint") // Joint list with comma Apple, Google, Microsoft, Facebook, Twitter, Intel, QualComm, Tesla
    //分组,用某种条件 把列表分成两组
    val (first, second) = list.partition { it.length < 6 }
    println("Length shorter than 6 $first") // Length shorter than 6 [Apple, Intel, Tesla]
    println("Longer than 6 $second") // Longer than 6 [Google, Microsoft, Facebook, Twitter, QualComm]
    // 归类,按某种方法把元素归类,之后变成了一个Map
    val bucket = list.groupBy { it.length }
    println("$bucket is a map now") //{5=[Apple, Intel, Tesla], 6=[Google], 9=[Microsoft], 8=[Facebook, QualComm], 7=[Twitter]} is a map now
}

null处理

为了有效的减少空指针异常,Kotlin加入了Nullable类型,核心的原理是这样的:声明类型的时候要明确的告诉编译器,这个变量是否可能为null,如果可能为null,那么可以赋null给这个变量,并且在使用此变量时必须检查是否为null;假如这个变量不可能为null,那么是不可以赋null给此变量的。也就是说,编译器会帮忙做一些检查,以减少NullPointerException的发生。

Nullable变量

默认的变量声明都是不可为null的,如:

1
2
var safe: String
safe = null // 会有compile error

要想允许变量为null,要在类型后面加一个问号,以告诉编译器这是一个nullable类型:

1
2
var danger: String?
danger = null // OKay

使用时,nullable不能直接使用,必须检查是否为null:

1
2
safe.length // okay
danger.length // compile error, danger could be null

检查Nullable的真伪

可以用传统方式:

1
val len = if (danger != null) danger.length else -1
Safe call

既然有Nullable类型,自然就有配套的方式来更方便的使用它:

1
val len = danger?.length

如果danger是null就返回null,否则返回长度,注意它的返回值是一个Int?(又是一个Nullable类型)。这个还能链起来:

1
bob?.department?.head?.name

如果任何一环为null,则直接返回null。是不是感觉省了好多if (a == null)判断。

Elvis operator

假如不能接受safe call返回的null,咋办呢?想提供默认值的呢?也有方式:

1
2
val len = danger?.length
println(len ?: -1)

稍有点绕哈,首先,danger?.length返回一个Int?吧,那么?:的作用就是如果len是null,那么就返回-1,否则返回它的值。

强制取值符!!

它的作用是如果Nullable变量为null就抛出NullPointerException,如果正常的话就取其值,返回的类型是一个non-null类型:

1
val len = danger!!.length // get length or NullPointerException

尽管,编译器可以帮助我们做一些事情,但是现实的项目中的大量的NPE并不是直接来源于,可以方便追踪的赋值为null,而多是发生在多线程环境中,以及非常复杂的逻辑之中,编译器能否追踪到并警示,还有待考察。另外,就是虽有利器,但是要运用恰当,何时用允许null,何时不允许,还是要靠工程师的设计能力,比如尽可能返回空列表,空Map,或者空字串,而不是直接简单的返回null,这就能减少一定的NPE。

Exercises

光是看书或者看教程是比较乏味的,学习编程最重要的是要上手去练习,这样能加深印象,更好的理解书中或者教程中所讲的概念和知识点。官方也准备了一个非常好的练习项目叫Kotlin-koans,非常适配初学习者来练手。 下面说一下如何使用这个练习项目:

  1. 官网去下载后,解压
  2. 用Android Studio打开此项目,一切提示都回答yes
  3. 要想运行测试前需要先编译一下项目,否则会提示找不到基础的测试类,找到Gradle窗口,一般在右侧,点开找到kotlin-koans->Tasks->build->build,运行它
  4. 现在就可以用先进的TDD方式来学习Kotlin了,在Project视图下面,可以看到kotlin-koans项目,里面有两个,一个是java,一个是tests,这两个目录里面的子目录都是一一对应的,先运行tests下面的,会失败,然后编辑java/下面的对应的代码,直到测试通过。

Essence of Kotlin

致此,我们可以看出Kotlin这门语言的设计的核心理念:简洁,这是Kotlin的核心理念,所以我们看到,一些机械的,重复的,可以从上下文中推测 出来的都 可以省略,以增加可读性。我们在使用Kotlin的时候要践行此理念,把语言的特性发挥到最大。 当然,简洁,不是牺牲可读性的方式来缩短代码,而是要使用语言中的标准的简洁的表达方式,比如lambda表达式,省略参数等。

要注意参考Kotlin conventions以及Android Kotlin conventions以写出更加简洁和容易理解的代码。

Android dev setup

我们来新建一个项目,用纯Kotlin实现一个Hello, world Android应用,来展示一下如何在Android中使用Kotlin:

注意: 这里使用的是Android Studio 3.1.2版本,默认就支持Kotlin,如果使用小于3.0的版本需要安装Kotlin插件,可自行Google,孤还是建议先升级AS吧。

  1. 新建一个项目,其实流程跟新建一个普通Android Studio项目是一样一样的,从Android Studio3.0起,新建项目时就会有一个Checkbox,问你要不要添加Kotlin。这里把它选上。 Step 1
  2. 就直接下一步就好 Step 2
  3. Next,创建一个empty activity Step 3
  4. Finish
    Step 4
  5. 布局跟其他新建的Android项目无差别 Step 5
  6. 代码已经是Kotlin的了 Step 6
  7. 直接显示”Hello, world”略显无聊,所以加一下点击事件:
1
2
3
4
5
6
7
8
9
10
11
12
13
class HelloActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_hello)

        val colorTable = listOf("#ff0000", "#00ff00", "#0000ff", "#ffff00", "#00ffff", "#ff00ff")
        val label = findViewById<TextView>(R.id.label)
        label.setOnClickListener { view ->
            val randomIndex = (Math.random() * colorTable.size).toInt()
            view.setBackgroundColor(Color.parseColor(colorTable[randomIndex]))
        }
    }
}

其实,整体来看,布局和项目的结构还是按照Android的方式来,唯一的不同是代码可以用Kotlin来写了。

Good to go

至此,Kotlin就算入门了,可以使用Kotlin来构建应用程序了,或者在你的项目中应用Kotlin了。

参考资料和有用的资料分享

Comments