第十二章 Kotlin的多線程:協程(Coroutines)
Kotlin 1.1 introduced coroutines, a new way of writing asynchronous, non-blocking code (and much more). In this tutorial we will go through some basics of using Kotlin coroutines with the help of the kotlinx.coroutines
library, which is a collection of helpers and wrappers for existing Java libraries.
Make sure it's configured for Kotlin 1.1 or higher.
Since coroutines have the experimental status in Kotlin 1.1, by default the compiler reports a warning every time they are used. We can opt-in for the experimental feature and use it without a warning by adding this code to build.gradle
:
apply plugin: 'kotlin'
kotlin {
experimental {
coroutines 'enable'
}
}
Since we'll be using the kotlinx.coroutines
, let's add its recent version to our dependencies:
dependencies {
...
compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.15"
}
This library is published to Bintray JCenter repository, so let us add it:
repositories {
jcenter()
}
This code to pom.xml
:
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
...
<configuration>
<args>
<arg>-Xcoroutines=enable</arg>
</args>
</configuration>
</plugin>
Since we'll be using the kotlinx.coroutines
, let's add its recent version to our dependencies:
<dependencies>
...
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>0.15</version>
</dependency>
</dependencies>
This library is published to Bintray JCenter repository, so let us add it:
<repositories>
...
<repository>
<id>central</id>
<url>http://jcenter.bintray.com</url>
</repository>
</repositories>
That's it, we are good to go and write code under src/main/kotlin
.
My first coroutine
One can think of a coroutine as a light-weight thread. Like threads, coroutines can run in parallel, wait for each other and communicate.
The biggest difference is that coroutines are very cheap, almost free: we can create thousands of them, and pay very little in terms of performance.
True threads, on the other hand, are expensive to start and keep around. A thousand threads can be a serious challenge for a modern machine.
So, how do we start a coroutine? Let's use the launch {}
function:
launch(CommonPool) {
...
}
This starts a new coroutine on a given thread pool. In this case we are using CommonPool
that uses ForkJoinPool.commonPool()
. Threads still exist in a program based on coroutines, but one thread can run many coroutines, so there's no need for too many threads.
Let's look at a full program that uses launch
:
import kotlinx.coroutines.experimental.*
fun main(args: Array<String>) {
println("Start")
// Start a coroutine
launch(CommonPool) {
delay(1000)
println("Hello")
}
Thread.sleep(2000) // wait for 2 seconds
println("Stop")
}
Here we start a coroutine that waits for 1 second and prints Hello
.
We are using the delay()
function that's like Thread.sleep()
, but better: it doesn't block a thread, but only suspends the coroutine itself.
The thread is returned to the pool while the coroutine is waiting, and when the waiting is done, the coroutine resumes on a free thread in the pool.
The main thread (that runs the main()
function) must wait until our coroutine completes, otherwise the program ends before Hello
is printed.
Exercise: try removing the sleep()
from the program above and see the result.
If we try to use the same non-blocking delay()
function directly inside main()
, we'll get a compiler error:
Suspend functions are only allowed to be called from a coroutine or another suspend function
This is because we are not inside any coroutine. We can use delay if we wrap it into runBlocking {}
that starts a coroutine and waits until it's done:
runBlocking {
delay(2000)
}
So, first the resulting program prints Start
, then it runs a coroutine through launch {}
, then it runs another one through runBlocking {}
and blocks until it's done, then prints Stop
. Meanwhile the first coroutine completes and prints Hello
. Just like threads, we told you :)
Let's run a lot of them
Now, let's make sure that coroutines are really cheaper than threads. How about starting a million of them? Let's try starting a million threads first:
val c = AtomicInteger()
for (i in 1..1_000_000)
thread(start = true) {
c.addAndGet(i)
}
println(c.get())
This runs a 1'000'000 threads each of which adds to a common counter. My patience runs out before this program completes on my machine (definitely over a minute).
Let's try the same with coroutines:
val c = AtomicInteger()
for (i in 1..1_000_000)
launch(CommonPool) {
c.addAndGet(i)
}
println(c.get())
This example completes in less than a second for me, but it prints some arbitrary number, because some coroutines don't finish before main()
prints the result. Let's fix that.
We could use the same means of synchronization that are applicable to threads (a CountDownLatch
is what crosses my mind in this case), but let's take a safer and cleaner path.
Async: returning a value from a coroutine
Another way of starting a coroutine is async {}
. It is like launch {}
, but returns an instance of Deferred<T>
, which has an await()
function that returns the result of the coroutine. Deferred<T>
is a very basic future (fully-fledged JDK futures are also supported, but here we'll confine ourselves to Deferred
for now).
Let's create a million coroutines again, keeping their Deferred
objects. Now there's no need in the atomic counter, as we can just return the numbers to be added from our coroutines:
val deferred = (1..1_000_000).map { n ->
async (CommonPool) {
n
}
}
All these have already started, all we need is collect the results:
val sum = deferred.sumBy { it.await() }
We simply take every coroutine and await its result here, then all results are added together by the standard library function sumBy()
. But the compiler rightfully complains:
Suspend functions are only allowed to be called from a coroutine or another suspend function
await()
can not be called outside a coroutine, because it needs to suspend until the computation finishes, and only coroutines can suspend in a non-blocking way. So, let's put this inside a coroutine:
runBlocking {
val sum = deferred.sumBy { it.await() }
println("Sum: $sum")
}
Now it prints something sensible: 1784293664
, because all coroutines complete.
Let's also make sure that our coroutines actually run in parallel. If we add a 1-second delay()
to each of the async
's, the resulting program won't run for 1'000'000 seconds (over 11,5 days):
val deferred = (1..1_000_000).map { n ->
async (CommonPool) {
delay(1000)
n
}
}
This takes about 10 seconds on my machine, so yes, coroutines do run in parallel.
Suspending functions
Now, let's say we want to extract our workload (which is "wait 1 second and return a number") into a separate function:
fun workload(n: Int): Int {
delay(1000)
return n
}
A familiar error pops up:
Suspend functions are only allowed to be called from a coroutine or another suspend function
Let's dig a little into what it means. The biggest merit of coroutines is that they can suspend without blocking a thread. The compiler has to emit some special code to make this possible, so we have to mark functions that may suspend explicitly in the code. We use the suspend
modifier for it:
suspend fun workload(n: Int): Int {
delay(1000)
return n
}
Now when we call workload()
from a coroutine, the compiler knows that it may suspend and will prepare accordingly:
async (CommonPool) {
workload(n)
}
Our workload()
function can be called from a coroutine (or another suspending function), but can not be called from outside a coroutine. Naturally, delay()
and await()
that we used above are themselves declared as suspend
, and this is why we had to put them inside runBlocking {}
, launch {}
or async {}
.
Kotlin 1.1 的新特性
目錄
JavaScript
從 Kotlin 1.1 開始,JavaScript 目標平臺不再當是實驗性的。所有語言功能都支持,
并且有許多新的工具用于與前端開發環境集成。更詳細改動列表,請參見下文
。
協程(實驗性的)
Kotlin 1.1 的關鍵新特性是協程,它帶來了 future
/await
、 yield
以及類似的編程模式的
支持。Kotlin 的設計中的關鍵特性是協程執行的實現是語言庫的一部分,
而不是語言的一部分,所以你不必綁定任何特定的編程范式或并發庫。
協程實際上是一個輕量級的線程,可以掛起并稍后恢復。協程通過掛起函數支持:對這樣的函數的調用可能會掛起協程,并啟動一個新的協程,我們通常使用匿名掛起函數(即掛起 lambda 表達式)。
我們來看看在外部庫 kotlinx.coroutines 中實現的 async
/await
:
// 在后臺線程池中運行該代碼
fun asyncOverlay() = async(CommonPool) {
// 啟動兩個異步操作
val original = asyncLoadImage("original")
val overlay = asyncLoadImage("overlay")
// 然后應用疊加到兩個結果
applyOverlay(original.await(), overlay.await())
}
// 在 UI 上下文中啟動新的協程
launch(UI) {
// 等待異步疊加完成
val image = asyncOverlay().await()
// 然后在 UI 中顯示
showImage(image)
}
這里,async { …… }
啟動一個協程,當我們使用 await()
時,掛起協程的執行,而執行正在等待的操作,并且在等待的操作完成時恢復(可能在不同的線程上) 。
標準庫通過 yield
和 yieldAll
函數使用協程來支持惰性生成序列。
在這樣的序列中,在取回每個元素之后掛起返回序列元素的代碼塊,
并在請求下一個元素時恢復。這里有一個例子:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
//sampleStart
val seq = buildSequence {
for (i in 1..5) {
// 產生一個 i 的平方
yield(i * i)
}
// 產生一個區間
yieldAll(26..28)
}
// 輸出該序列
println(seq.toList())
//sampleEnd
}
</div>
運行上面的代碼以查看結果。隨意編輯它并再次運行!
請注意,協程目前還是一個實驗性的功能,這意味著 Kotlin 團隊不承諾
在最終的 1.1 版本時保持該功能的向后兼容性。
其他語言功能
類型別名
類型別名允許你為現有類型定義備用名稱。
這對于泛型類型(如集合)以及函數類型最有用。
這里有幾個例子:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
//sampleStart
typealias OscarWinners = Map<String, String>
fun countLaLaLand(oscarWinners: OscarWinners) =
oscarWinners.count { it.value.contains("La La Land") }
// 請注意,類型名稱(初始名和類型別名)是可互換的:
fun checkLaLaLandIsTheBestMovie(oscarWinners: Map<String, String>) =
oscarWinners["Best picture"] == "La La Land"
//sampleEnd
fun oscarWinners(): OscarWinners {
return mapOf(
"Best song" to "City of Stars (La La Land)",
"Best actress" to "Emma Stone (La La Land)",
"Best picture" to "Moonlight" /* …… */)
}
fun main(args: Array<String>) {
val oscarWinners = oscarWinners()
val laLaLandAwards = countLaLaLand(oscarWinners)
println("LaLaLandAwards = $laLaLandAwards (in our small example), but actually it's 6.")
val laLaLandIsTheBestMovie = checkLaLaLandIsTheBestMovie(oscarWinners)
println("LaLaLandIsTheBestMovie = $laLaLandIsTheBestMovie")
}
</div>
更詳細信息請參閱其 KEEP。
已綁定的可調用引用
現在可以使用 ::
操作符來獲取指向特定對象實例的方法或屬性的成員引用。
以前這只能用 lambda 表達式表示。
這里有一個例子:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
//sampleStart
val numberRegex = "\\d+".toRegex()
val numbers = listOf("abc", "123", "456").filter(numberRegex::matches)
//sampleEnd
fun main(args: Array<String>) {
println("Result is $numbers")
}
</div>
更詳細信息請參閱其 KEEP。
密封類和數據類
Kotlin 1.1 刪除了一些對 Kotlin 1.0 中已存在的密封類和數據類的限制。
現在你可以在同一個文件中的任何地方定義一個密封類的子類,而不只是以作為密封類嵌套類的方式。
數據類現在可以擴展其他類。
這可以用來友好且清晰地定義一個表達式類的層次結構:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
//sampleStart
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
fun eval(expr: Expr): Double = when (expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
}
val e = eval(Sum(Const(1.0), Const(2.0)))
//sampleEnd
fun main(args: Array<String>) {
println("e is $e") // 3.0
}
</div>
更詳細信息請參閱其文檔或者
密封類 及
數據類的 KEEP。
lambda 表達式中的解構
現在可以使用解構聲明語法來解開傳遞給 lambda 表達式的參數。
這里有一個例子:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
fun main(args: Array<String>) {
//sampleStart
val map = mapOf(1 to "one", 2 to "two")
// 之前
println(map.mapValues { entry ->
val (key, value) = entry
"$key -> $value!"
})
// 現在
println(map.mapValues { (key, value) -> "$key -> $value!" })
//sampleEnd
}
</div>
下劃線用于未使用的參數
對于具有多個參數的 lambda 表達式,可以使用 _
字符替換不使用的參數的名稱:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
fun main(args: Array<String>) {
val map = mapOf(1 to "one", 2 to "two")
//sampleStart
map.forEach { _, value -> println("$value!") }
//sampleEnd
}
</div>
這也適用于解構聲明:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
data class Result(val value: Any, val status: String)
fun getResult() = Result(42, "ok").also { println("getResult() returns $it") }
fun main(args: Array<String>) {
//sampleStart
val (_, status) = getResult()
//sampleEnd
println("status is '$status'")
}
</div>
更詳細信息請參閱其 KEEP。
數字字面值中的下劃線
正如在 Java 8 中一樣,Kotlin 現在允許在數字字面值中使用下劃線來分隔數字分組:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
//sampleStart
val oneMillion = 1_000_000
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010
//sampleEnd
fun main(args: Array<String>) {
println(oneMillion)
println(hexBytes.toString(16))
println(bytes.toString(2))
}
</div>
更詳細信息請參閱其 KEEP。
對于屬性的更短語法
對于沒有自定義訪問器、或者將 getter 定義為表達式主體的屬性,現在可以省略屬性的類型:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
//sampleStart
data class Person(val name: String, val age: Int) {
val isAdult get() = age >= 20 // 屬性類型推斷為 “Boolean”
}
//sampleEnd
fun main(args: Array<String>) {
val akari = Person("Akari", 26)
println("$akari.isAdult = ${akari.isAdult}")
}
</div>
內聯屬性訪問器
如果屬性沒有幕后字段,現在可以使用 inline
修飾符來標記該屬性訪問器。
這些訪問器的編譯方式與內聯函數相同。
<div class="sample" markdown="1" data-min-compiler-version="1.1">
//sampleStart
public val <T> List<T>.lastIndex: Int
inline get() = this.size - 1
//sampleEnd
fun main(args: Array<String>) {
val list = listOf('a', 'b')
// 其 getter 會內聯
println("Last index of $list is ${list.lastIndex}")
}
</div>
你也可以將整個屬性標記為 inline
——這樣修飾符應用于兩個訪問器。
局部委托屬性
現在可以對局部變量使用委托屬性語法。
一個可能的用途是定義一個延遲求值的局部變量:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
import java.util.Random
fun needAnswer() = Random().nextBoolean()
fun main(args: Array<String>) {
//sampleStart
val answer by lazy {
println("Calculating the answer...")
42
}
if (needAnswer()) { // 返回隨機值
println("The answer is $answer.") // 此時計算出答案
}
else {
println("Sometimes no answer is the answer...")
}
//sampleEnd
}
</div>
更詳細信息請參閱其 KEEP。
委托屬性綁定的攔截
對于委托屬性,現在可以使用 provideDelegate
操作符攔截委托到屬性之間的綁定
。
例如,如果我們想要在綁定之前檢查屬性名稱,我們可以這樣寫:
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(thisRef: MyUI, property: KProperty<*>): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, property.name)
…… // 屬性創建
}
private fun checkProperty(thisRef: MyUI, name: String) { …… }
}
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { …… }
class MyUI {
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}
provideDelegate
方法在創建 MyUI
實例期間將會為每個屬性調用,并且可以立即執行
必要的驗證。
更詳細信息請參閱其文檔。
泛型枚舉值訪問
現在可以用泛型的方式來對枚舉類的值進行枚舉:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
//sampleStart
enum class RGB { RED, GREEN, BLUE }
inline fun <reified T : Enum<T>> printAllValues() {
print(enumValues<T>().joinToString { it.name })
}
//sampleEnd
fun main(args: Array<String>) {
printAllValues<RGB>() // 輸出 RED, GREEN, BLUE
}
</div>
對于 DSL 中隱式接收者的作用域控制
@DslMarker
注解允許限制來自 DSL 上下文中的外部作用域的接收者的使用。
考慮那個典型的 HTML 構建器示例:
table {
tr {
td { +"Text" }
}
}
在 Kotlin 1.0 中,傳遞給 td
的 lambda 表達式中的代碼可以訪問三個隱式接收者:傳遞給 table
、tr
和 td
的。 這允許你調用在上下文中沒有意義的方法——例如在 td
里面調用 tr
,從而
在 <td>
中放置一個 <tr>
標簽。
在 Kotlin 1.1 中,你可以限制這種情況,以使只有在 td
的隱式接收者上定義的方法
會在傳給 td
的 lambda 表達式中可用。你可以通過定義標記有 @DslMarker
元注解的注解
并將其應用于標記類的基類。
rem
操作符
mod
操作符現已棄用,而使用 rem
取代。動機參見這個問題。
標準庫
字符串到數字的轉換
在 String 類中有一些新的擴展,用來將它轉換為數字,而不會在無效數字上拋出異常:
String.toIntOrNull(): Int?
、 String.toDoubleOrNull(): Double?
等。
val port = System.getenv("PORT")?.toIntOrNull() ?: 80
還有整數轉換函數,如 Int.toString()
、 String.toInt()
、 String.toIntOrNull()
,
每個都有一個帶有 radix
參數的重載,它允許指定轉換的基數(2 到 36)。
onEach()
onEach
是一個小、但對于集合和序列很有用的擴展函數,它允許對操作鏈中
的集合/序列的每個元素執行一些操作,可能帶有副作用。
對于迭代其行為像 forEach
但是也進一步返回可迭代實例。 對于序列它返回一個
包裝序列,它在元素迭代時延遲應用給定的動作。
inputDir.walk()
.filter { it.isFile && it.name.endsWith(".txt") }
.onEach { println("Moving $it to $outputDir") }
.forEach { moveFile(it, File(outputDir, it.toRelativeString(inputDir))) }
also()、takeIf() 和 takeUnless()
這些是適用于任何接收者的三個通用擴展函數。
also
就像 apply
:它接受接收者、做一些動作、并返回該接收者。
二者區別是在 apply
內部的代碼塊中接收者是 this
,
而在 also
內部的代碼塊中是 it
(并且如果你想的話,你可以給它另一個名字)。
當你不想掩蓋來自外部作用域的 this
時這很方便:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
class Block {
lateinit var content: String
}
//sampleStart
fun Block.copy() = Block().also {
it.content = this.content
}
//sampleEnd
// 使用“apply”代替
fun Block.copy1() = Block().apply {
this.content = this@copy1.content
}
fun main(args: Array<String>) {
val block = Block().apply { content = "content" }
val copy = block.copy()
println("Testing the content was copied:")
println(block.content == copy.content)
}
</div>
takeIf
就像單個值的 filter
。它檢查接收者是否滿足該謂詞,并
在滿足時返回該接收者否則不滿足時返回 null
。
結合 elvis-操作符和及早返回,它允許編寫如下結構:
val outDirFile = File(outputDir.path).takeIf { it.exists() } ?: return false
// 對現有的 outDirFile 做些事情
<div class="sample" markdown="1" data-min-compiler-version="1.1">
fun main(args: Array<String>) {
val input = "Kotlin"
val keyword = "in"
//sampleStart
val index = input.indexOf(keyword).takeIf { it >= 0 } ?: error("keyword not found")
// 對輸入字符串中的關鍵字索引做些事情,鑒于它已找到
//sampleEnd
println("'$keyword' was found in '$input'")
println(input)
println(" ".repeat(index) + "^")
}
</div>
takeUnless
與 takeIf
相同,只是它采用了反向謂詞。當它 不 滿足謂詞時返回接收者,否則返回 null
。因此,上面的示例之一可以用 takeUnless
重寫如下:
val index = input.indexOf(keyword).takeUnless { it < 0 } ?: error("keyword not found")
當你有一個可調用的引用而不是 lambda 時,使用也很方便:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
private fun testTakeUnless(string: String) {
//sampleStart
val result = string.takeUnless(String::isEmpty)
//sampleEnd
println("string = \"$string\"; result = \"$result\"")
}
fun main(args: Array<String>) {
testTakeUnless("")
testTakeUnless("abc")
}
</div>
groupingBy()
此 API 可以用于按照鍵對集合進行分組,并同時折疊每個組。 例如,它可以用于
計算文本中字符的頻率:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
fun main(args: Array<String>) {
val words = "one two three four five six seven eight nine ten".split(' ')
//sampleStart
val frequencies = words.groupingBy { it.first() }.eachCount()
//sampleEnd
println("Counting first letters: $frequencies.")
// 另一種方式是使用“groupBy”和“mapValues”創建一個中間的映射,
// 而“groupingBy”的方式會即時計數。
val groupBy = words.groupBy { it.first() }.mapValues { (_, list) -> list.size }
println("Comparing the result with using 'groupBy': ${groupBy == frequencies}.")
}
</div>
Map.toMap() 和 Map.toMutableMap()
這倆函數可以用來簡易復制映射:
class ImmutablePropertyBag(map: Map<String, Any>) {
private val mapCopy = map.toMap()
}
Map.minus(key)
運算符 plus
提供了一種將鍵值對添加到只讀映射中以生成新映射的方法,但是沒有一種簡單的方法來做相反的操作:從映射中刪除一個鍵采用不那么直接的方式如 Map.filter()
或 Map.filterKeys()
。
現在運算符 minus
填補了這個空白。有 4 個可用的重載:用于刪除單個鍵、鍵的集合、鍵的序列和鍵的數組。
<div class="sample" markdown="1" data-min-compiler-version="1.1">
fun main(args: Array<String>) {
//sampleStart
val map = mapOf("key" to 42)
val emptyMap = map - "key"
//sampleEnd
println("map: $map")
println("emptyMap: $emptyMap")
}
</div>
minOf() 和 maxOf()
這些函數可用于查找兩個或三個給定值中的最小和最大值,其中值是原生數字或 Comparable
對象。每個函數還有一個重載,它接受一個額外的 Comparator
實例,如果你想比較自身不可比的對象的話。
<div class="sample" markdown="1" data-min-compiler-version="1.1">
fun main(args: Array<String>) {
//sampleStart
val list1 = listOf("a", "b")
val list2 = listOf("x", "y", "z")
val minSize = minOf(list1.size, list2.size)
val longestList = maxOf(list1, list2, compareBy { it.size })
//sampleEnd
println("minSize = $minSize")
println("longestList = $longestList")
}
</div>
類似數組的列表實例化函數
類似于 Array
構造函數,現在有創建 List
和 MutableList
實例的函數,并通過
調用 lambda 表達式來初始化每個元素:
<div class="sample" markdown="1" data-min-compiler-version="1.1">
fun main(args: Array<String>) {
//sampleStart
val squares = List(10) { index -> index * index }
val mutable = MutableList(10) { 0 }
//sampleEnd
println("squares: $squares")
println("mutable: $mutable")
}
</div>
Map.getValue()
Map
上的這個擴展函數返回一個與給定鍵相對應的現有值,或者拋出一個異常,提示找不到該鍵。
如果該映射是用 withDefault
生成的,這個函數將返回默認值,而不是拋異常。
<div class="sample" markdown="1" data-min-compiler-version="1.1">
fun main(args: Array<String>) {
//sampleStart
val map = mapOf("key" to 42)
// 返回不可空 Int 值 42
val value: Int = map.getValue("key")
val mapWithDefault = map.withDefault { k -> k.length }
// 返回 4
val value2 = mapWithDefault.getValue("key2")
// map.getValue("anotherKey") // <- 這將拋出 NoSuchElementException
//sampleEnd
println("value is $value")
println("value2 is $value2")
}
</div>
抽象集合
這些抽象類可以在實現 Kotlin 集合類時用作基類。
對于實現只讀集合,有 AbstractCollection
、 AbstractList
、 AbstractSet
和 AbstractMap
,
而對于可變集合,有 AbstractMutableCollection
、 AbstractMutableList
、 AbstractMutableSet
和 AbstractMutableMap
。
在 JVM 上,這些抽象可變集合從 JDK 的抽象集合繼承了大部分的功能。
數組處理函數
標準庫現在提供了一組用于逐個元素操作數組的函數:比較
(contentEquals
和 contentDeepEquals
),哈希碼計算(contentHashCode
和 contentDeepHashCode
),
以及轉換成一個字符串(contentToString
和 contentDeepToString
)。它們都支持 JVM
(它們作為 java.util.Arrays
中的相應函數的別名)和 JS(在
Kotlin 標準庫中提供實現)。
<div class="sample" markdown="1" data-min-compiler-version="1.1">
fun main(args: Array<String>) {
//sampleStart
val array = arrayOf("a", "b", "c")
println(array.toString()) // JVM 實現:類型及哈希亂碼
println(array.contentToString()) // 良好格式化為列表
//sampleEnd
}
</div>
JVM 后端
Java 8 字節碼支持
Kotlin 現在可以選擇生成 Java 8 字節碼(命令行選項 -jvm-target 1.8
或者Ant/Maven/Gradle 中
的相應選項)。目前這并不改變字節碼的語義(特別是,接口和 lambda 表達式中的默認方法
的生成與 Kotlin 1.0 中完全一樣),但我們計劃在以后進一步使用它。
Java 8 標準庫支持
現在有支持在 Java 7 和 8 中新添加的 JDK API 的標準庫的獨立版本。
如果你需要訪問新的 API,請使用 kotlin-stdlib-jre7
和 kotlin-stdlib-jre8
maven 構件,而不是標準的 kotlin-stdlib
。
這些構件是在 kotlin-stdlib
之上的微小擴展,它們將它作為傳遞依賴項帶到項目中。
字節碼中的參數名
Kotlin 現在支持在字節碼中存儲參數名。這可以使用命令行選項 -java-parameters
啟用。
常量內聯
編譯器現在將 const val
屬性的值內聯到使用它們的位置。
可變閉包變量
用于在 lambda 表達式中捕獲可變閉包變量的裝箱類不再具有 volatile 字段。
此更改提高了性能,但在一些罕見的使用情況下可能導致新的競爭條件。如果受此影響,你需要提供
自己的同步機制來訪問變量。
javax.scripting 支持
Kotlin 現在與javax.script API(JSR-223)集成。
其 API 允許在運行時求值代碼段:
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
engine.eval("val x = 3")
println(engine.eval("x + 2")) // 輸出 5
關于使用 API 的示例項目參見這里
。
kotlin.reflect.full
為 Java 9 支持準備,在 kotlin-reflect.jar
庫中的擴展函數和屬性已移動
到 kotlin.reflect.full
包中。舊包(kotlin.reflect
)中的名稱已棄用,將在
Kotlin 1.2 中刪除。請注意,核心反射接口(如 KClass
)是 Kotlin 標準庫
(而不是 kotlin-reflect
)的一部分,不受移動影響。
JavaScript 后端
統一的標準庫
Kotlin 標準庫的大部分目前可以從代碼編譯成 JavaScript 來使用。
特別是,關鍵類如集合(ArrayList
、 HashMap
等)、異常(IllegalArgumentException
等)以及其他
幾個關鍵類(StringBuilder
、 Comparator
)現在都定義在 kotlin
包下。在 JVM 平臺上,一些名稱是相應 JDK 類的
類型別名,而在 JS 平臺上,這些類在 Kotlin 標準庫中實現。
更好的代碼生成
JavaScript 后端現在生成更加可靜態檢查的代碼,這對 JS 代碼處理工具(如
minifiers、 optimisers、 linters 等)更加友好。
external
修飾符
如果你需要以類型安全的方式在 Kotlin 中訪問 JavaScript 實現的類,
你可以使用 external
修飾符寫一個 Kotlin 聲明。(在 Kotlin 1.0 中,使用了 @native
注解。)
與 JVM 目標平臺不同,JS 平臺允許對類和屬性使用 external 修飾符。
例如,可以按以下方式聲明 DOM Node
類:
external class Node {
val firstChild: Node
fun appendChild(child: Node): Node
fun removeChild(child: Node): Node
// 等等
}
改進的導入處理
現在可以更精確地描述應該從 JavaScript 模塊導入的聲明。
如果在外部聲明上添加 @JsModule("<模塊名>")
注解,它會在編譯期間正確導入
到模塊系統(CommonJS或AMD)。例如,使用 CommonJS,該聲明會
通過 require(……)
函數導入。
此外,如果要將聲明作為模塊或全局 JavaScript 對象導入,
可以使用 @JsNonModule
注解。
例如,以下是將 JQuery 導入 Kotlin 模塊的方法:
external interface JQuery {
fun toggle(duration: Int = definedExternally): JQuery
fun click(handler: (Event) -> Unit): JQuery
}
@JsModule("jquery")
@JsNonModule
@JsName("$")
external fun jquery(selector: String): JQuery
在這種情況下,JQuery 將作為名為 jquery
的模塊導入。或者,它可以用作 $-對象,
這取決于Kotlin編譯器配置使用哪個模塊系統。
你可以在應用程序中使用如下所示的這些聲明:
fun main(args: Array<String>) {
jquery(".toggle-button").click {
jquery(".toggle-panel").toggle(300)
}
}
參考資料
Kotlin 開發者社區
國內第一Kotlin 開發者社區公眾號,主要分享、交流 Kotlin 編程語言、Spring Boot、Android、React.js/Node.js、函數式編程、編程思想等相關主題。