1. Kotlin语法糖 —— 熟悉Kotlin书写习惯
1.1 函数声明
Java
int sum(int num1, int num2) {
return num1 + num2;
}
对应如下代码:
Kotlin
fun sum(num1: Int, num2: Int): Int {
return num1 + num2
}
使用fun关键字声明函数sum,传入num1和num2两个Int类型的常量参数,返回Int类型的数据。Kotlin中无基本类型数据,所有类型都类似于Java中的包裹类型,从使用Int而非int就可看出。
另外,Kotlin中的函数默认将函数体最后的一行作为返回值,结合Kotlin的类型推导可将上述代码简写成如下形式:
fun sum(num1: Int, num2: Int) {
num1 + num2
}
Kotlin特有的语法糖允许将只有一个语句块的函数写成如下形式(推荐):
fun sum(num1: Int, num2: Int) = num1 + num2
1.2 流程控制
Kotlin中的if...else语句带有返回值,在条件较多的情况下推荐使用when语句代替而非Java中的switch...case语句
fun largeNum(num1: Int, num2: Int) = if (num1 > num2) {
num1
} else {
num2
}
上面的代码可以写成如下形式(推荐):
fun largeNum(num1: Int, num2: Int) = when {
num1 > num2 -> num1
else -> num2
}
注意,需要为每个when语句添加else,它相当于Java的switch case语句中的default
when语句也可以写成带有参数的形式:
fun checkName(name: String) = when (name) {
"Tom" -> 86
"Jim" -> {
print("foo bar")
60
}
is String -> 100 // 关于is和when更多在一起的用法可查阅密封类sealed class
else -> 0
}
Java中的一些合法标识符在Kotlin中时关键字,例如is,采用如下方法调用Java中定义的is()方法:
a.`is()`
Kotlin中的for语句很强大,一种常见的使用方法如下:
for (i in 1..10) { // 1, 2, ..., 9, 10
// TODO
}
其中1..10相当于for (int i = 1; i <= 10; i += 1),而若要将条件变为i < 10,那么1..10也要变为1 until 10。
此外,for语句还有以下能够望文生义的写法:
for (i in 1 until 10 step 2) // 1, 3, 5, 7, 9
for (i in 10 downTo 1) // 10, 9, ..., 2, 1
1.3 JavaScript互操作性
Kotlin使用js()调用JavaScript,例如js("var a = 0")。Android的WebView中可以使用webView.evaluateJavascript("javascript:var a = 0", null)這樣的方法調用JavaScript。被JavaScript調用的Kotlin方法前要使用@JavascriptInterface修飾,同時WebView要像webView.addJavascriptInterface(jsObj,"name")这样才能调用Kotlin代码,其中jsObj是方法由@JavascriptInterface修飾的类对象,name是JavaScript那边要用的类名。
2. Kotlin的类和对象
2.1 数据类
Kotlin中没有new关键字,创建对象的方法类似于C++。对于仅用于保存数据的类,
Kotlin提供data class关键字声明数据类,类名后的括号内就是类的构造器,例如:
data class Student(
val name: String,
val id: String,
val score: Int
)
Kotlin会在编译时自动为这个类生成getter, setter, equals, toString和hashCode等方法
(编译时生成很多方法也是Kotlin编译速度慢的原因之一)对应的默认实现,重写可以使用override关键字。
可以如下使用数据类:
val stu1: Student = Student("Tom","123",99)
println("stu1:${stu1}") // 输出 stu1:Student(name=Tom, id=123, score=99)
注意,不同于函数传参,Kotlin类的构造器的括号内的变量需要有val关键字声明。
其中"${stu1}"能自动在Kotlin字符串中调用变量stu1的toString()方法(推荐),用于拼接字符串。多行字符串用三对双引号包裹,如"""$stu1""",{...}在不引起歧义时可以省略。另外,数据类不能被继承。
2.2 继承和构造器
Kotlin中使用了与var val相同的思想,不希望某个类在不知道的地方被继承后,
其子类的对象又被向上造型成父类对象而变得容易出错。
Kotlin中的类默认是都final的,不能被继承。使用open关键字让类可以被继承:
open class Person(val _name: String) {
lateinit var name: String
init {
name = _name
}
}
Kotlin中的变量默认不能为null,且必须在声明的同时初始化赋值,为了让不能在声明时就被初始化的成员name合理存在,需要用lateinit关键字修饰,并确保其在使用前被初始化。相应的,变量使用之前要用类似if(!::name.isInitialized) { name = String("") }这样的方法确保变量在使用之前被初始化。init语句块可以理解为构造方法的方法体,用于初始化成员变量,在对象创建时被调用。另外,在继承时可被重写的方法也需要用open关键字修饰。
Kotlin如同C++一样使用:实现继承:
class Student(val _id: String, _name: String) : Person(_name) {
lateinit var id: String
constructor(_name: String) : this("000", _name) // 次构造器,重载主构造器
constructor() : this("anonymous") // 次构造器,重载上面的次构造器
init {
id = _id
}
}
val person1 = Student("123", "Tom") as Person // 使用as关键字将Student类对象造型为Person类对象
注意,对于父类中已经定义的字段name: String,子类的构造器中不要再使用val关键字声明。由于编译器一般会自动完成隐式类型转换,as关键字有时对于编译不是必须的,但是可以增加代码的可阅读性。
Kotlin允许使用多个constructor语句块重载主构造器,但是由于写法复杂并不推荐(除非有充分的理由),使用默认参数可以将上面的多个构造器的实现方式简化为如下望文生义的代码(推荐):
class Student(val _id: String = "000", _name: String = "anonymous") : Person(_name) {
lateinit var id: String
init {
id = _id
}
}
val stu1 = Student("123") // _id为"123",_name为"anonymous"
val stu2 = Student(_name = "Tom") // _id为"000",_name为"Tom"
val stu3 = Student("123", "Tom") // _id为"123",_name为"Tom"
val stu4 = Student(_name = "Tom", _id = "123") // _id为"123",_name为"Tom"
2.3 匿名、单例和伴生对象
Kotlin使用object关键字声明单例类。虽然单例类对象的成员的使用方法类似于Java中的静态类成员,但是Kotlin确实在成员方法第一次被调用时自动为我们创建了一个单例对象并使用这个对象调用方法。
object Single {
fun test() {
// TODO
}
}
Single.test() // 注意,不是 Single().test()
Kotlin使用object关键字实现匿名类,下面一段可以望文生义的代码能够定义一个实现Runnable接口的匿名类。
Kotlin
Thread(object : Runnable {
override fun run() {
// TODO
}
}).start()
这段Kotlin代码等价于下面的这段Java代码:
Java
new Thread(new Runnable() {
@Override
public void run() {
// TODO
}
}).start();
这种使用单例类完成注入反转的方式在Kotlin中不推荐,请记住这段代码,在SAM转换中有多种优化的方式。
Kotlin中的伴生对象类似于Java中的静态成员,需要在类中使用companion object语句块声明,为了能让伴生对象成员在与Java互操作时被正确调用,成员变量前推荐添加@JvmField修饰符,成员函数前也要添加@JvmStatic修饰符:
open class Person() {
companion object {
@JvmField
var count = 10
@JvmStatic
fun test() {
// TODO
}
}
}
使用Person.count和Person.test()可以调用这两个成员。注意,不是Person().count和Person().test()。这里可能会有疑惑,为什么精心设计的Kotlin语言会使用object关键字同时完成接口、匿名类和单例类的实现,这当然不是因为关键字不够用了,后文会解释原因。
2.4 扩展函数、接口实现与委托
Kotlin允许在调用处临时定义类的扩展函数,有效的作用域限于当前包,例如:
fun String.funName(): Int {
return 5
}
println("123".funName()) // 输出 5
注意扩展函数不支持多态,如果一个子类对象被造型成父类对象,则调用的扩展函数就是父类的扩展函数。
Kotlin使用interface关键字定义接口,并允许在定义接口时的实现成员函数的函数体从而完成接口函数的默认实现。Kotlin如同继承一样使用:实现接口,使用,分割继承的单个类和实现的多个接口。下面的代码定义了接口Study并完成了其中一个成员函数的默认实现:
interface Study {
fun readBooks()
fun doHomework() {
println("default implementation")
}
}
下面的代码定义了继承Person并实现上面接口Study的类Student:
class Student(name: String, age: Int) : Person(name, age), Study {
override fun readBooks() {
// TODO
}
}
需要注意的是,因为类可以实现多个接口,所以接口方法的默认实现会引发类似C++中多重继承的复杂问题, 因此除非有充分的理由否则不推荐使用。
委托是指使用某一对象的方法和属性实现接口从而省去类内相应字段的实现,Kotlin使用by关键字完成委托:
val b = object : Base {
override fun test(): Unit { // Kotlin中的Unit相当于Java中的void,在此可省略
println("This is b!")
}
}
class Derived(b: Base) : Base by b {
}
Derived(b).test() // 输出 This is b
上述代码中b是实现了Base接口的对象,现在需要让Derived类实现Base接口,一种方式是使用: Base by b将接口内带实现的方法委托给b,这样在调用Derived类中没有重载的接口方法时就会自动调用对象b的这个方法。另外,Kotlin的属性委托允许将对象的一个字段的具体实现委托给另一个类完成,例如getValue()和setValue()。
3. 函数式编程
3.1 匿名函数与闭包
不同于面向对象编程(OOP),在函数式编程(FP)语言中允许在函数内部定义函数或者将一个函数作为另一个函数的输入参数或返回值(在Java中不允许这么做,虽然Java中也有折衷的替代方法绕过这种限制),试着理解下面一段代码:
fun makeFun(): () -> Unit = {
var a = 0
return fun () {
++ a
println(a)
}
}
val testFun1: () -> Unit = makeFun()
testFun1() // 输出 1
testFun1() // 输出 2
val testFun2 = makeFun()
testFun2() // 输出 1
testFun1() // 输出 3
上面的代码在makeFun()方法中定义了一个匿名函数并将其返回,注意这个匿名函数能够访问定义在它作用域之外的变量a,由于定义在函数makeFun()中的局部变量a在这个内部定义的匿名函数中被引用,因此它不会随着函数makeFun()的执行结束被释放内存,而会一直保存在内存之中。这个定义在函数中的持有函数局部变量的匿名函数叫闭包函数。另一方面,正如OOP中可以返回一个对象一样(返回的对象的在函数中申请内存,返回后内存不被销毁),FP可以将这个匿名函数返回,testFun1和testFun2获得了这个匿名函数的引用。
3.2 Lambda表达式与高阶函数
Lambda表达式与匿名函数及其类似,下面是定义函数和定义Lambda表达式的方式对比:
函数定义
fun lengthOf(str: String): (String) -> Int = str.length
Lambda定义
val lengthOf: (String) -> Int = { str: String -> str.length }
Lambda表达式也同函数一样,具有类型推导,上面代码可以简化成下面的形式:
函数定义
fun lengthOf(str: String) = str.length
Lambda定义
val lengthOf = { str: String -> str.length }
上面这四种写法都允许使用下面这样的方式调用:
println(lengthOf("banana")) // 输出 6
Lambda表达式允许使用匿名函数的形式实现与上段代码相同的功能:
println({ str: String -> str.length }("banana")) // 输出 6
也可以定义没有参数和返回值的Lambda表达式:
val printLengthOfBanana: () -> Unit = { println("banana".length) } // 输出 6, : () -> Unit 可以省略
对于接收但是不需要使用的参数使用_作为参数名,这种写法对下文的标准库函数使用很有用:
val lengthOf = { str: String, _: Int -> str.length }
高阶函数是输入参数包含Lambda表达式或其他函数的函数,下面是一个高阶函数的示例:
fun stringMap(str: String, map: (String) -> Int) {
return map(str)
}
stringMap的第二个参数是(String) -> Int类型的Lambda表达式,可以如下方式调用stringMap:
val lengthOf = { str: String -> str.length }
stringMap("apple", lengthOf) // 返回 5
上面的代码可以简化为:
stringMap("apple", { str: String -> str.length })
当Lambda表达式以匿名函数的形式被直接传入高阶函数时,Lambda的参数列表就支持类型推导:
stringMap("apple", { str -> str.length })
Kotlin规定,当Lambda表达式作为函数的最后一个参数输入时,可以将Lambda表达式移出括号:
stringMap("apple") { str -> str.length }
Kotlin规定,当Lambda表达式内的参数列表只有唯一的参数时,可以不声明参数名,直接用it代替这一参数:
stringMap("apple") { it.length } // 推荐写法
使用函数引用而非Lambda表达式也能达到相同效果:
fun lengthOf(str: String) = str.length
stringMap("apple", ::lengthOf) // 注意,函数的引用方式要使用::
另外,对于更高阶的函数,甚至能见到类似如下的定义:
fun kLevelFun(): () -> () -> Unit { // TODO } // 返回一个高阶函数
3.3 泛型与标准库
Kotlin允许以Java中方式的使用泛型,如:
val list = ArrayList<String>()
list.add("foo")
println(list[0]) // 输出 foo
val map = HashMap<String, Int>()
map.put("bar", 1)
println("${map["bar"]}"} // 输出 1
但是Kotlin更推荐使用listOf, mutableListOf, arrayOf, mutableArrayOf, setOf, mutableSetOf, mapOf和mutableMapOf等代替上面的用法,mutable开头的泛型类储存的元素是可变的,而没有mutable的泛型类的元素是不可变的,只能在定义时固定下来。下面是以mutableListOf和mapOf为例的一段能够望文生义的代码:
val listFriut: MutableList<String> = mutableListOf<String>("apple", "banana") // 可简化成 val listFriut = mutableListOf("apple", "banana")
listFriut.add("orange")
println(listFriut) // 输出 [apple, banana, orange]
val mapErrorCode = mapOf(1000 to "输入错误", 1001 to "网络错误") // Kotlin的新版中支持在最后的元素后添加逗号并推荐这么做
println(mapErrorCode[1001]) // 输出 网络错误
println(mapErrorCode.getOrDefault(9999, "未知错误")) // 输出 未知错误
println(mapErrorCode) // 输出 {1000=输入错误, 1001=网络错误}
这些泛型类的标准库同样允许接受Lambda表达式作为谓词进行一些操作:
val listFruit = listOf<String>("apple", "banana", "orange")
val lambda: (String) -> Int = { fruit: String -> fruit.length }
val maxLengthFruit = listFruit.maxBy(lambda)
println(maxLengthFruit) // 输出 banana
省掉lambda这一中间变量可将Lambda表达式作为匿名函数直接输入maxBy():
val maxLengthFruit = listFruit.maxBy({ fruit: String -> fruit.length })
上面的Lambda定义可以根据类型推导被简化成如下形式:
val maxLengthFruit = listFruit.maxBy({ fruit -> fruit.length })
当Lambda表达式作为函数的最后一个参数输入时,可以将Lambda表达式移出括号:
val maxLengthFruit = listFruit.maxBy() { fruit -> fruit.length }
当Lambda表达式作为函数唯一的参数输入时,可以将用于接收参数的圆括号删除:
val maxLengthFruit = listFruit.maxBy { fruit -> fruit.length }
当Lambda表达式内的参数列表只有唯一的参数时,可以不声明参数名,直接用it代替这一参数:
val maxLengthFruit = listFruit.maxBy { it.length } // 推荐写法
Kotlin的标准库还有许多功能强大的标准函数,举一个简单的例子:
listOf(2, 6, 1, 3, 4).filterNot { it % 2 == 1 }.sorted().reversed().forEachIndexed { _, i -> print(i) } // 输出 642
"1ab2c3d".filter { it in 'a'..'z' }.map { it.toUpperCase() }.forEach(::print) // 输出 ABCD
另外,自定义的泛型函数可以设置允许传入的参数类型,即泛型约束:
fun <T : Comparable<T>> sort(list: List<T>) {} // 传入参数只能是实现了Comparable接口的类型
3.4 SAM转换
单一抽象方法(SAM)转换,是指某个方法接收实现了一个接口的匿名类的对象,而这个接口定义在Java中且只有一个待实现的方法时,Kotlin允许直接传入匿名函数代替这个抽象类的对象。以匿名类中的代码为例:
Thread(object : Runnable {
override fun run() {
// TODO
}
}).start()
Thread类的构造方法接受一个实现Runnable接口的匿名类对象,而Runnable接口中唯一待实现的方法只有一个run(),因此这段代码符合SAM转换的要求,利用SAM转换精简代码:
Thread(Runnable {
// TODO
}).start()
因为Runnable中只有一个待实现方法run(),因此不用显式地写出override fun run()Kotlin也会明白Runnable后面的Lambda表达式就是run()函数体中的内容。
当Java中定义的的方法只接受一个实现接口的匿名类对象作为唯一的参数时,实现的接口名也可以省略:
Thread({
// TODO
}).start()
当Lambda表达式时函数的唯一参数时可以省去( ),继续简化代码(推荐):
Thread {
// TODO
}.start()
利用SAM转换,可以大量减少Android注入反转的代码,如下两段代码是等价的:
Java
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO
}
});
Kotlin
button.setOnClickListener { v ->
// TODO
}
现在可以解释关于object的那个疑问了。我们知道,严格来说,JavaScript不是一门面向对象的语言,因为Javascript中创建对象的方法不是OOP中那样先创建一个类,之后通过类来创建对象,而是可以不创建类直接创建出一个对象。Kotlin与JavaScript一样,使用object关键字创建对象,例如:
val list = listOf(1, 2, 3)
val person = object : List<Int> by list {
val cash = 180
val bill = 50
fun calcBalance(): Int = cash - bill
}
println(person.cash) // 输出 180
println(person.calcBalance()) // 输出 130
person.forEach { print(it) } // 输出 123
理解了上面这段代码后不难发现,Kotlin中的单例类不过是一个对象而已,我们在调用Kotlin单例类的成员时只是调用了这个对象的成员,Kotlin中的匿名类的对象和伴生对象也是同样的道理。SAM转换的过程也类似,可以从下面的代码中看清Lambda表达式与对象的关系:
val obj1 = object {
init {
println("obj1")
}
}
val obj2 = { println("obj2") }
obj1 // 输出 obj1
obj2() // 输出 obj2
3.5 处理非null性与作用域函数
Kotlin变量默认都是不能为null的。在下面这段代码中,不存在任何抛出空指针异常的风险:
fun doExercise(person: Person) {
person.swim()
person.skate()
}
上面这段代码在调用peson的swim()和skate()方法前无需使用if (person != null)对person进行非空判断,因为传入的参数person: Person不能null,如果在调用时使用doExercise(null)会编译不通过。
如果希望传入的参数可为null,那么传入参数的类型Person就要变为Person?。但是此时直接调用person.swim()和person.skate()会编译不通过,因为person可能为null,不能直接调用person的成员。需要在调用person的成员之前为其是否为null添加判断:
fun doExercise(person: Person?) {
if (person != null) {
person.swim()
person.skate()
}
}
但是Kotlin并不推荐使用这种方式处理null,一种可行的替代方式是使用?.操作符访问成员:
fun doExercise(person: Person?) {
person?.swim()
person?.skate()
}
对于person?.swim(),当person为null时,不执行swim()并返回null;当person不为null时执行swim()。
由于?.运算符有可能返回null,为了避免将null传递到下面的语句中,?.运算符常与?:运算符一起使用,例如下面这段能望文生义的代码:
val words = person?.speak() ?: "nothing"
上面这段代码中,当person不为null时,返回person.speak()的值;当person为null时,?:运算符左面的表达式值为null,此时?:运算符会自动返回其右面表达式的值"nothing"。
另一种对可为null性的处理方式是使用非空断言符!!,!!左边的表达式会被当做非null的来使用,当person为null时下面的代码会抛空指针异常:
fun doExercise(person: Person?) {
person!!.swim()
person!!.skate()
}
这样写的最大缺点就是难以消除空指针异常的风险,因此无论如何Kotlin代码中都不应该大量使用!!运算符,否则应该考虑重构。
Kotlin推荐使用处理可为null性的方法是使用作用域函数let判空,高阶函数let的返回值是Lambda表达式的结果:
fun doExercise(person: Person?) {
person?.let {
it.swim()
it.skate()
}
}
val words = person?.let{ it.speak() } ?: "nothing"
Kotlin中的作用域函数主要有7个,他们的使用方法大同小异,具体见下表:
| 函数 | 引用 | 返回值 | 是否是扩展函数 |
|---|---|---|---|
| let | it | Lambda表达式的结果 | 是 |
| run | this | Lambda表达式的结果 | 是 |
| run | - | Lambda表达式的结果 | 不是:无需上下文 |
| with | this | Lambda表达式的结果 | 不是:需要引入参数 |
| apply | this | 对象本身 | 是 |
| also | it | 对象本身 | 是 |
除此此外,还有takeIf和takeUnless两个作用域函数,他们的选择依据如下:
- 对一个非
null对象执行Lambda表达式:let - 将表达式作为变量引入到一个作用域中:
let - 对象配置:
apply - 对象配置并计算结果:
run - 在需要表达式的地方运行语句:
非扩展的run - 额外执行的内容:
also - 一个对象的一组函数调用:
with - 判断对象是否满足条件,返回
null或对象本身:takeIf和takeUnless
下面是一些常见的作用域函数使用场景:
// 下面这两行代码等价
val response = client.let { it.uri = "192.168.1.1" }
val response = client.run { uri = "192.168.1.1" }
// 下面这段代码中person1, person2, person3的值相同
val person1 = Person("Tom")
person1.age = 10
person1.id = "123"
val person2 = Person("Tom").apply { age = 10 }.apply { id = "123" } // 这行代码说明apply返回对象本身
val person3 = Person("Tom").apply {
age = 10
id = "123"
}.also { println(it) }
val firstOfList = with(numberList) {
"First number is ${this.first()}" // numberList.first()
}
val result1 = 100.takeIf { it > 100 } // result1是null
val result2 = 100.takeUnless { it > 100 } // result2是100
使用作用域函数没有任何特殊的优势,使用作用域函数的唯一理由是让代码简单易读。
4. 协程
4.1 并发和异步
下面這两段代码和产生相同的效果,都会输出Hello world!:
线程
Thread {
Thread.sleep(100L) // sleep是阻塞函数,会阻塞线程向下运行
println("world!")
}.start()
println("Hello ")
Thread.sleep(1000L)
协程
import kotlinx.coroutines.* // 还需添加依赖 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
GlobalScope.launch { // 协程必須运行在协程作用域中,GlobalScope就是一个协程作用域(GlobalScope是特殊的协程作用域,详见下节)
delay(100L) // delay是挂起函数,不是阻塞函數,不能阻塞线程向下云行,但是会挂起这个协程,阻止其向下运行
println("world!")
}
println("Hello ")
Thread.sleep(1000L) // 必須阻塞主线程,保證协程运行结束前主线程存活,否则携程会被取消
多个协程运行在不同的线程中,1000个协程可能只需要6到7个线程作为载体来运行,具体的线程数量是由Kotlin底层控制的,由Kotlin管理线程池,我们只需要管理协程。协程不仅轻量,而且容易书写和管理。下面使用Global.launch和runBlocking实现睡眠排序:
listOf(3, 6, 7, 9, 5, 2, 8, 4, 1).forEach {
GlobalScope.launch {
delay(it * 20L) // Global.launch中的delay起挂起作用
print("$it, ")
}
}
runBlocking { // runBlocking也是协程作用域,runBlocking中的delay能阻塞线程,這段代码相当于上面的Thread.sleep(1000L)
delay(1000L)
}
协程作用域是可以嵌套的,開始的那段代码更适合写成下面這種形式(推荐):
fun test() = runBlocking { // 作用域内的挂起操作能夠阻塞线程
val job = GlobalScope.launch { // 啓動協程時都會返回一個Job用於管理協程
delay(100L)
println("world!")
}
println("Hello ")
job.join() // 挂起并等待job运行结束,这比delay(1000L)这样挂起固定时间的写法更好,但是一定不要忘记写这句话,因为runBlocking(父携程作用域)结束時Global.launch(子协程作用域)内的操作会被取消
}
Job支持使用job.cancel()向协程发送结束运行的请求,或者使用job.cancelAndJoin()发送結束运行的请求并等待其结束。协程在执行挂起函数时会收到取消请求并抛出CancellationException,因此协程中的挂起操作要放在try {} finally {}语句块中,当检测到取消请求時就会执行finally中的语句执行携程取消前的操作(例如保存数据)。请注意,父作用域中的子协程抛出异常时,整个父协程作用域中的全部子协程都会取消。另一种情況是携程中沒有挂起函数,此時可以用协程作用域中的isActive属性判断是否收到取消請求:
while (isActive) {
println("Hello world!")
}
finally {}语句块中不允许调用用挂起函数,如delay(),因为Kotlin规定协程的取消操作不该耗费时间,但也有方法在finally {}语句块中使用挂起函数(不推荐)。另外,自定义的挂起函数要用suspend关键字。
有两点需要説明:
- 前文为了便于理解多次使用
Global.launch启动协程,其实随意使用Global.launch启动协程并且不获取协程返回的Job是很危险的,因为一旦没有对这種协程Job的引用,协程可能会长久运行在后台不停止,占用资源。更推荐的写法是在协程作用域中使用launch,这樣作用域结束时launch启动的协程就会被自动取消。(这在UI更新和IO处理时很有用,例如Activity调用finish()方法销毁时,context内的操作也应随之取消) - 除了
runBlocking这一推荐使用的协程作用域之外,另一个协程作用域coroutineScope常常放在runBlocking之内使用。它与runBlocking的区别是:runBlocking会阻塞当前线程,而coroutineScope只是挂起,并释放资源做其他事情。因此runBlocking是常规函数,而coroutineScope是挂起函数,这意味着coroutineScope只能在一个协程作用域内使用,而runBlocking可以用在最外层。
请试着理解下面这段代码:
fun main() = runBlocking {
launch { // 使用launch启动协程无需获取返回的Job,无需手动停止
delay(2000L)
println(2)
}
coroutineScope { // 本行的coroutineScope换成runBlocking输出的顺序不变,但是coroutineScope能释放底层线程用于其他用途,省资源
launch {
delay(3000L)
println(3)
}
delay(1000L)
println(1)
}
println(4) // 最后输出 1 2 3 4
}
4.2 挂起、阻塞与返回值
注意,网络请求通常是阻塞的,而非挂起,下面利用Thread.sleep(1000L)模拟网络请求,delay(1000L)模拟挂起函数。试着理解下面几段代码:
fun test() = runBlocking {
val time = measureTimeMills {
val job1 = launchJob1()
val job2 = launchJob2()
job1.join()
job2.join()
}
println(time) // 输出 1017
}
suspend fun launchJob1() = launch {
delay(1000L)
}
suspend fun launchJob2() = launch {
delay(1000L)
}
在一个作用域中使用同一个线程启动两个协程,由于每个协程内都不会阻塞,只会挂起,因此大约需要1秒执行完。
fun test() = runBlocking {
val time = measureTimeMills {
val job1 = launch {
Thread.sleep(1000L)
}
val job2 = launch {
Thread.sleep(1000L)
}
job1.join()
job2.join()
}
println(time) // 输出 2083
}
在一个作用域中使用同一个线程启动两个协程,由于每个协程内都会阻塞这个线程,因此大约需要2秒执行完。
fun test() = runBlocking {
val time = measureTimeMills {
val job1 = GlobalScope.launch {
Thread.sleep(1000L)
}
val job2 = GlobalScope.launch {
Thread.sleep(1000L)
}
job1.join()
job2.join()
}
println(time) // 输出 1022
}
在一个作用域中启动两个顶级协程GlobalScope.launch,GlobalScope.launch不受协程作用域影响,并且不使用父作用域所在的线程,而是由Kotlin分配一个线程池中空闲的线程,因此这段代码中的job1和job2其实运行在两个线程上,最终要1秒执行完。
线程利用回调取值,而协程有返回值,无需回调。前文中的launch启动的是“一劳永逸”的协程,只能返回一个Job,不能获取协程的计算结果,而async返回的是Job的子类Deffered,使用async启动协程不仅可以像前文那样控制协程,还能获取协程计算的返回值。获取协程的返回值需要使用await()方法:
fun test() = runBlocking {
val time = measureTimeMills {
val job1 = async {
delay(1000L) // 此行为阻塞时也可以用GlobalScope.async启动这个协程
1
}
val job2 = async {
delay(1000L)
2
}
// 对集合这样用: listOf(job1, job2).awaitAll()
println(job1.await() + job2.await()) // 输出 3
}
println(time) // 输出 1045
}
async可以惰性启动协程:
val job1 = async { launchJobAsync() } // 非懒惰启动
val job2 = async(start = CoroutineStart.LAZY) { launchJobAsync() } // 懒惰启动
job2.start()
suspend fun launchJobAsync(): Int {
delay(1000L)
return 1
}
指定协程所在的线程要使用协程调度器,其实GlobalScope.launch就是指定使用Kotlin线程池中的未被阻塞的线程运行协程,它也可以被写成launch(Dispatchers.Default)。Kotlin提供如下调度器:
// 使用父协程作用域的线程
launch {}
// 不限制使用哪个线程(除非有充分的理由否则不推荐)
launch(Dispatchers.Unconfined) {}
// 只用于UI操作和执行很快的工作
launch(Dispatchers.Main) {}
// 专门优化用于主线程之外的IO密集型工作,例如磁盘读写和网络IO
launch(Dispatchers.IO) {}
// 专门优化用于主线程之外的CPU密集型工作,例如排序和解析JSON
launch(Dispatchers.Default) {}
// 使用自定义线程(除非有充分的理由否则不推荐,耗费资源)
launch(newSingleThreadContext("myThread")) {}
Scala部署
Try Scala in IDEA
Installation
- Make sure you have the Java 8 JDK (also known as 1.8)
- Run
javac -versionon the command line and make sure you seejavac 1.8.___ - If you don’t have version 1.8 or higher, install the JDK
- Run
- Next, download and install IntelliJ Community Edition
- Then, after starting up IntelliJ, you can download and install the Scala plugin by following the instructions on how to install IntelliJ plugins (search for “Scala” in the plugins menu.)
When we create the project, we’ll install the latest version of Scala. Note: If you want to open an existing Scala project, you can click Open when you start IntelliJ.
Creating the Project
- Open up IntelliJ and click File => New => Project
- On the left panel, select Scala. On the right panel, select IDEA.
- Name the project HelloWorld
- Assuming this is your first time creating a Scala project with IntelliJ, you’ll need to install a Scala SDK. To the right of the Scala SDK field, click the Create button.
- Select the highest version number (e.g. 2.13.8) and click Download. This might take a few minutes but subsequent projects can use the same SDK.
- Once the SDK is created and you’re back to the “New Project” window click Finish.
Writing code
- On the Project pane on the left, right-click
srcand select New => Scala class. If you don’t see Scala class, right-click on HelloWorld and click on Add Framework Support…, select Scala and proceed. If you see Error: library is not specified, you can either click download button, or select the library path manually. If you only see Scala Worksheet try expanding thesrcfolder and itsmainsubfolder, and right-click on thescalafolder. - Name the class
Helloand change the Kind toobject. - Change the code in the class to the following:
object Hello extends App {
println("Hello, World!")
}
Running it
- Right click on
Helloin your code and select Run ‘Hello’. - You’re done!
Experimenting with Scala
A good way to try out code samples is with Scala Worksheets
- In the project pane on the left, right click
srcand select New => Scala Worksheet. - Name your new Scala worksheet “Mathematician”.
- Enter the following code into the worksheet:
def square(x: Int) = x * x
square(2)
As you change your code, you’ll notice that it gets evaluated in the right pane. If you do not see a right pane, right click on your Scala worksheet in the Project pane, and click on Evaluate Worksheet.
Build Scala PR in CMD
Installation
- Make sure you have the Java 8 JDK (also known as 1.8)
- Run
javac -versionin the command line and make sure you seejavac 1.8.___ - If you don’t have version 1.8 or higher, install the JDK
- Run
- Install sbt
Create the project
cdto an empty folder.- Run the following command
sbt new scala/hello-world.g8. This pulls the ‘hello-world’ template from GitHub. It will also create atargetfolder, which you can ignore. - When prompted, name the application
hello-world. This will create a project called “hello-world”. - Let’s take a look at what just got generated:
- hello-world
- project (sbt uses this to install and manage plugins and dependencies)
- build.properties
- src
- main
- scala (All of your scala code goes here)
- Main.scala (Entry point of program) <-- this is all we need for now
- build.sbt (sbt's build definition file)
After you build your project, sbt will create more target directories for generated files. You can ignore these.
Running the project
cdintohello-world.- Run
sbt. This will open up the sbt console. - Type
~run. The~is optional and causes sbt to re-run on every file save, allowing for a fast edit/run/debug cycle. sbt will also generate atargetdirectory which you can ignore.
Modifying the code
- Open the file
src/main/scala/Main.scalain your favorite text editor. - Change “Hello, World!” to “Hello, New York!”
- If you haven’t stopped the sbt command, you should see “Hello, New York!” printed to the console.
- You can continue to make changes and see the results in the console.
Adding a dependency
Changing gears a bit, let’s look at how to use published libraries to add extra functionality to our apps.
- Open up
build.sbtand add the following line:
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2"
Here, libraryDependencies is a set of dependencies, and by using +=, we’re adding the scala-parser-combinators dependency to the set of dependencies that sbt will go and fetch when it starts up. Now, in any Scala file, you can import classes, objects, etc, from scala-parser-combinators with a regular import.
You can find more published libraries on Scaladex, the Scala library index, where you can also copy the above dependency information for pasting into your build.sbt file.
Build Scala PR in IDEA
Creating the project
In this section, we’ll show you how to create the project in IntelliJ. However, if you’re comfortable with the command line, we recommend you try Getting Started with Scala and sbt on the Command Line and then come back here to the section “Writing Scala code”.
- If you didn’t create the project from the command line, open up IntelliJ and select “Create New Project”
- On the left panel, select Scala and on the right panel, select sbt
- Click Next
- Name the project “SbtExampleProject”
- If you already created the project on the command line, open up IntelliJ, select Import Project and open the
build.sbtfile for your project - Make sure the JDK version is 1.8 and the sbt version is at least 0.13.13
- Select Use auto-import so dependencies are automatically downloaded when available
- Select Finish
Understanding the directory structure
sbt creates many directories which can be useful once you start building more complex projects. You can ignore most of them for now but here’s a glance at what everything is for:
- .idea (IntelliJ files)
- project (plugins and additional settings for sbt)
- src (source files)
- main (application code)
- java (Java source files)
- scala (Scala source files) <-- This is all we need for now
- scala-2.12 (Scala 2.12 specific files)
- test (unit tests)
- target (generated files)
- build.sbt (build definition file for sbt)
Writing Scala code
- On the Project panel on the left, expand
SbtExampleProject=>src=>main - Right-click
scalaand select New => Package - Name the package
exampleand click OK (or just press the Enter or Return key). - Right-click the package
exampleand select New => Scala class (if you don’t see this option, right-click theSbtExampleProject, click Add Frameworks Support, select Scala and proceed) - Name the class
Mainand change the Kind toObject. - Change the code in the class to the following:
object Main extends App {
val ages = Seq(42, 75, 29, 64)
println(s"The oldest person is ${ages.max}")
}
Note: IntelliJ has its own implementation of the Scala compiler, and sometimes your code is correct even though IntelliJ indicates otherwise. You can always check to see if sbt can run your project on the command line.
Running the project
- From the Run menu, select Edit configurations
- Click the + button and select sbt Task.
- Name it
Run the program. - In the Tasks field, type
~run. The~causes sbt to rebuild and rerun the project when you save changes to a file in the project. - Click OK.
- On the Run menu. Click Run ‘Run the program’.
- In the code, change
75to61and look at the updated output in the console.
Adding a dependency
Changing gears a bit, let’s look at how to use published libraries to add extra functionality to our apps.
- Open up
build.sbtand add the following line:
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2"
Here, libraryDependencies is a set of dependencies, and by using +=, we’re adding the scala-parser-combinators dependency to the set of dependencies that sbt will go and fetch when it starts up. Now, in any Scala file, you can import classes, objects, etc, from scala-parser-combinators with a regular import.
You can find more published libraries on Scaladex, the Scala library index, where you can also copy the above dependency information for pasting into your build.sbt file.
Scala FP
这个例子中原本的函数字面量由下面的代码组成:
(i: Int) => i % 2 == 0
检视这段代码,有助于将=>符号理解为一个转换器,因为表达式转变了符号左边(名为i的Int变量)的参数列表,应用符号(在这个例子中,表达式返回Boolean值)右边的算法生成新的结果。
如上所述,本例用较长的格式定义了一个匿名函数,通过几种方式可对其进行简化。第一个例子的形式最为清晰:
val evens = x.filter((i: Int) => i % 2 == 0)
因为Scala编译器可以从表达式推断出i是一个Int型变量,所以Int声明可以去掉:
val evens = x.filter(i => i % 2 == 0)
同时,当参数在函数中只出现一次时,Scala允许使用“_”通配符替换变量名,从而可_以将代码简化为:
val evens = x.filter(_ % 2 == 0)
在其他的例子中,可以进一步简化匿名函数。例如,从最清晰的形式开始,用匿名函数和foreach方法打印列表中的每一个元素:
x.foreach((i:Int) => println(i))
和以前一样,Int声明可以忽略:
x.foreach((i) => println(i))
因为只有一个参数,参数i的小括号就显得多余了:
x.foreach(i => println(i))
鉴于i在函数体内只使用了一次,表达式可以用“_”通配符进一步简化:
x.foreach(println(_))
最后,如果一个函数字面量只有一条语句,并且该语句只接受一个参数,那么参数不需要特别指定,也不需要显式声明,最终精简版的代码如下:
x.foreach(println)
声明函数字面量至少有两种方式。我个人喜欢下面这种方式,隐式推断下列函数的返回类型为Boolean:
val f = (i: Int) => { i % 2 == 0 }
Scala编译器在这种情况下可以通过函数体智能地推断出其返回Boolean值。程序员也应当很容易从代码右边的表达式判断出其返回为Boolean值,所以我通常不会在函数声明时加上显式的Boolean返回类型。
但是如果函数更加复杂,或者要显式声明函数字面量的返回类型,下面的例子展示了显式声明函数返回值为Boolean类型的不同形式:
val f: (Int) => Boolean = i => { i % 2 == 0 }
val f: Int => Boolean = i => { i % 2 == 0 }
val f: Int => Boolean = i => i % 2 == 0
val f: Int => Boolean = _ % 2 == 0
第二个例子可以帮助展示这些方式的不同之处。函数都接受两个Int的参数,返回输入参数和的Int值:
// implicit approach
val add = (x: Int, y: Int) => { x + y }
val add = (x: Int, y: Int) => x + y
// explicit approach
val add: (Int, Int) => Int = (x,y) => { x + y }
val add: (Int, Int) => Int = (x,y) => x + y
如上所示,这些例子中函数体的大括号不是必须的,但是函数体包含一个以上的表达式时,一定要使用大括号:
val addThenDouble: (Int, Int) => Int = (x,y) => {
val a = x + y
2*a
}
像匿名函数一样使用方法
Scala非常灵活,就像是可以将匿名函数赋给一个变量,也可以将定义的方法像实例变量一样传递。再次利用模数的例子,用下面任一种方式定义一个方法:
def modMethod(i: Int) = i % 2 == 0
def modMethod(i: Int) = { i % 2 == 0 }
def modMethod(i: Int): Boolean = i % 2 == 0
def modMethod(i: Int): Boolean = { i % 2 == 0 }
任一个方法都可以传入集合方法,这些方法期望接受一个函数作为参数,该函数接受Int参数返回Boolean值,如List[Int]的filter方法:
val list = List.range(1, 10)
list.filter(modMethod)
如下是REPL中展示的结果:
scala> def modMethod(i: Int) = i % 2 == 0
modMethod: (i: Int)Boolean
scala> val list = List.range(1, 10)
list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)
scala> list.filter(modMethod)
res0: List[Int] = List(2, 4, 6, 8)
这和定义函数字面量然后将其指派给一个变量的过程很相似。下面代码中的函数和前面的方法作用一样:
val modFunction = (i: Int) => i % 2 == 0
list.filter(modFunction)
modMethod是类的方法,modFunction是赋给变量的函数,这是它们在编码层面明显的不同。Function1特质定义了接受一个参数的函数,modFunction实际上是它的一个实例。(Scala中定义了其他类似的特质,包括Function0,Function2,以此类推,一直到Function22。)
创建一个接受简单函数作为参数的方法。
解决办法
这个解决办法分三步:
1.定义方法,包括期望接受的函数参数的签名。
2.定义满足这个签名的一个或者多个函数。
3.将函数作为参数传递给方法。
为了证实这点,定义一个名为executeFu nction的方法,该方法接受一个函数作为参数。参数名为callback,它是一个函数,没有输入参数,也没有返回值:
def executeFunction(callback:() => Unit) {
callback()
}
两个快速注释:
·callback:()语法定义了一个无参函数。如果函数有参数,其类型应当在括号中列出。
·=>Unit部分代码表明该方法没有返回值。
很快会讨论这个语法。
接下来,定义一个函数匹配这个签名。下面代码中的sayHello函数没有参数也没有返回值:
val sayHello = () => { println("Hello") }
最后一步,将sayHello函数传入executeFunction方法:
executeFunction(sayHello)
REPL输出结果如下:
scala> def executeFunction(callback:() => Unit) { callback() }
executeFunction: (callback: () => Unit)Unit
scala> val sayHello = () => { println("Hello") }
sayHello: () => Unit = <function0>
scala> executeFunction(sayHello)
Hello
讨论
例子中函数的参数callback没有任何特殊的意义。当我第一次明白如何向方法传递函数参数时,我选择用callback这个名字,这样表意更加清晰,但这仅仅是方法参数的名字而已。现在,我经常将Int型参数命名为i,函数参数起名为f:
def executeFunction(f:() => Unit) {
f()
}
这部分特别之处在于传入的函数必须匹配函数签名。这个例子中已经声明了传入的必须是无参函数并且没有返回值:
f:() => Unit
定义函数作为方法参数的常用语法如下:
parameterName: (parameterType(s)) => returnType
本例中parameterName就是f,parameterType为空因为函数不接受任何参数,返回类型为Unit是因为不希望该函数有返回:
executeFunction(f:() => Unit)
下面的代码定义接受String参数返回Int值的函数,两种签名都是可行的:
executeFunction(f:String => Int)
executeFunction(f:(String) => Int)
与Kotlin对比
FP
// Kotlin
val lengthOf: (String) -> Int = { str: String -> str.length }
{ str: String -> str.length }("foobar")
// Scala
val lengthOf: (String) => Int = (str: String) => { str.length }
((str: String) => { str.length })("foobar")
// Kotlin
list.filter { it % 2 == 1 }.reverse().foreach { println(it) }
// Scala
list.filter { _ % 2 == 1 }.reverse.foreach { println(_) }
SAM
// Java
Thread(object : Runnable {
override fun run() {
// TODO
}
}).start()
// Kotlin
Thread(Runnable {
// TODO
}).start()
Thread({
// TODO
}).start()
Thread {
// TODO
}.start()
// Scala :set -Xexperimental
new Thread(() =>
// TODO
).start