第15章 Kotlin 文件IO操作與多線程
《Kotlin極簡教程》正式上架:
點擊這里 > 去京東商城購買閱讀
點擊這里 > 去天貓商城購買閱讀
非常感謝您親愛的讀者,大家請多支持!!!有任何問題,歡迎隨時與我交流~
我們在使用 Groovy 的文件 IO 操作的時候,感覺非常便利。同樣的Kotlin也有好用的文件 IO 操作的 API。同樣的在 Kotlin 中對 Java 的正則表達式功能做了一些實用的擴展。還有 Kotlin 中的多線程主要也是對 Java 的多線程 API 作了一些封裝。因為這些 Java 已經有了很多的基礎 API,Kotlin 并沒有自己再去重復實現,而是在 Java 的基礎上進行了實用的功能擴展。
本章我們就來介紹Kotlin 文件 IO 操作、正則表達式以及多線程相關的內容。
15.1 Kotlin IO 簡介
Kotlin的IO操作都在kotlin.io包下。Kotlin的原則就是Java已經有的,好用的就直接使用,沒有的或者不好用的,就在原有類的基礎上進行封裝擴展,例如Kotlin 就給 File 類寫了擴展函數。這跟Groovy的擴展API 的思想是一樣的。
15.2 終端 IO
Java 超長的輸出語句 System.out.println() 居然延續到了現在!同樣的工作在C++里面只需要簡單的 cout<< 就可以完成。當然,如果需要的話,我們可以在工程中直接封裝 System.out.println() 為簡單的打印方法。
在Kotlin里面很簡單,只需要使用println或者print這兩個全局函數即可,我們不再需要冗長的前綴。當然如果我們很懷舊,就是想用 System.out.println() ,Kotlin 依然支持直接這么使用(與 Java 無縫互操作)。
>>> System.out.println("K")
K
>>> println("K")
K
這里的 println 函數Kotlin實現如下
@kotlin.internal.InlineOnly
public inline fun println(message: Any?) {
System.out.println(message)
}
當然,Kotlin 也只是在 System.out.println() 的基礎上進行了封裝。
從終端讀取數據也很簡單,最基本的方法就是全局函數readLine,它直接從終端讀取一行作為字符串。如果需要更進一步的處理,可以使用Kotlin提供的各種字符串處理函數來處理和轉換字符串。
Kotlin 的封裝終端IO 的類在 stdlib/src/kotlin/io/Console.kt 源文件中。
15.3 文件 IO 操作
Kotlin為java.io.File提供了大量好用的擴展函數,這些擴展函數主要在下面三個源文件中:
kotlin/io/files/FileTreeWalk.kt |
---|
kotlin/io/files/Utils.kt |
kotlin/io/FileReadWrite.kt |
同時,Kotlin 也針對InputStream、OutputStream和 Reader 等都做了簡單的擴展。它們主要在下面的兩個源文件中:
kotlin/io/IOStreams.kt |
---|
kotlin/io/ReadWrite.kt |
Koltin 的序列化直接采用的 Java 的序列化類的類型別名:
internal typealias Serializable = java.io.Serializable
下面我們來簡單介紹一下 Kotlin 文件讀寫操作。
15.3.1 讀文件
讀取文件全部內容
我們如果簡單讀取一個文件,可以使用readText()方法,它直接返回整個文件內容。代碼示例如下
/**
* 獲取文件全部內容字符串
* @param filename
*/
fun getFileContent(filename: String): String {
val f = File(filename)
return f.readText(Charset.forName("UTF-8"))
}
我們直接使用 File 對象來調用 readText 函數即可獲得該文件的全部內容,它返回一個字符串。如果指定字符編碼,可以通過傳入參數Charset來指定,默認是UTF-8編碼。
如果我們想要獲得文件每行的內容,可以簡單通過split("\n")
來獲得一個每行內容的數組。
獲取文件每行的內容
我們也可以直接調用 Kotlin 封裝好的readLines函數,獲得文件每行的內容。readLines函數返回一個持有每行內容的 List。
/**
* 獲取文件每一行內容,存入一個 List 中
* @param filename
*/
fun getFileLines(filename: String): List<String> {
return File(filename).readLines(Charset.forName("UTF-8"))
}
直接操作字節數組
我們如果希望直接操作文件的字節數組,可以使用readBytes()。如果想使用傳統的Java方式,在Kotlin 中你也可以像 Groovy 一樣自如使用。
//讀取為bytes數組
val bytes: ByteArray = f.readBytes()
println(bytes.joinToString(separator = " "))
//直接像 Java 中的那樣處理Reader或InputStream
val reader: Reader = f.reader()
val inputStream: InputStream = f.inputStream()
val bufferedReader: BufferedReader = f.bufferedReader()
}
15.3.2 寫文件
和讀文件類似,寫入文件也很簡單。我們可以寫入字符串,也可以寫入字節流。還可以直接使用Java的 Writer 或者 OutputStream。
覆蓋寫文件
fun writeFile(text: String, destFile: String) {
val f = File(destFile)
if (!f.exists()) {
f.createNewFile()
}
f.writeText(text, Charset.defaultCharset())
}
末尾追加寫文件
fun appendFile(text: String, destFile: String) {
val f = File(destFile)
if (!f.exists()) {
f.createNewFile()
}
f.appendText(text, Charset.defaultCharset())
}
15.4 遍歷文件樹
和Groovy一樣,Kotlin也提供了方便的功能來遍歷文件樹。遍歷文件樹需要調用擴展方法walk()。它會返回一個FileTreeWalk對象,它有一些方法用于設置遍歷方向和深度,詳情參見FileTreeWalk API 文檔說明。
提示:FileTreeWalk API 文檔鏈接 https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/-file-tree-walk/
下面的例子遍歷了指定文件夾下的所有文件。
fun traverseFileTree(filename: String) {
val f = File(filename)
val fileTreeWalk = f.walk()
fileTreeWalk.iterator().forEach { println(it.absolutePath) }
}
測試代碼:
@Test fun testTraverseFileTree() {
KFileUtil.traverseFileTree(".")
}
運行上面的測試代碼,它將輸出當前目錄下的所有子目錄及其文件。
我們還可以遍歷當前文件下面所有子目錄文件,存入一個 Iterator<File> 中
fun getFileIterator(filename: String): Iterator<File> {
val f = File(filename)
val fileTreeWalk = f.walk()
return fileTreeWalk.iterator()
}
我們遍歷當前文件下面所有子目錄文件,還可以根據條件過濾,并把結果存入一個 Sequence<File> 中
fun getFileSequenceBy(filename: String, p: (File) -> Boolean): Sequence<File> {
val f = File(filename)
return f.walk().filter(p)
}
測試代碼:
@Test fun testGetFileSequenceBy() {
val fileSequence1 = KFileUtil.getFileSequenceBy(".", {
it.isDirectory
})
fileSequence1.forEach { println("fileSequence1: ${it.absoluteFile} ") }
val fileSequence2 = KFileUtil.getFileSequenceBy(".", {
it.isFile
})
fileSequence2.forEach { println("fileSequence2: ${it.absoluteFile} ") }
val fileSequence3 = KFileUtil.getFileSequenceBy(".", {
it.extension == "kt"
})
fileSequence3.forEach { println("fileSequence3: ${it.absoluteFile} ") }
}
在工程中運行上面的測試代碼,它將會有類似下面的輸出:
...
...
fileSequence3: /Users/jack/kotlin/chapter15_file_io/./src/main/kotlin/com/easy/kotlin/fileio/KFileUtil.kt
fileSequence3: /Users/jack/kotlin/chapter15_file_io/./src/main/kotlin/com/easy/kotlin/fileio/KNetUtil.kt
fileSequence3: /Users/jack/kotlin/chapter15_file_io/./src/main/kotlin/com/easy/kotlin/fileio/KShellUtil.kt
fileSequence3: /Users/jack/kotlin/chapter15_file_io/./src/test/kotlin/com/easy/kotlin/fileio/KFileUtilTest.kt
15.5 網絡IO操作
Kotlin為java.net.URL增加了兩個擴展方法,readBytes和readText。我們可以方便的使用這兩個方法配合正則表達式實現網絡爬蟲的功能。
下面我們簡單寫幾個函數實例。
根據 url 獲取該 url 的響應 HTML函數
fun getUrlContent(url: String): String {
return URL(url).readText(Charset.defaultCharset())
}
根據 url 獲取該 url 響應比特數組函數
fun getUrlBytes(url: String): ByteArray {
return URL(url).readBytes()
}
把 url 響應字節數組寫入文件
fun writeUrlBytesTo(filename: String, url: String) {
val bytes = URL(url).readBytes()
File(filename).writeBytes(bytes)
}
下面這個例子簡單的獲取了百度首頁的源代碼。
getUrlContent("https://www.baidu.com")
下面這個例子根據 url 來獲取一張圖片的比特流,然后調用readBytes()方法讀取到字節流并寫入文件。
writeUrlBytesTo("圖片.jpg", "http://n.sinaimg.cn/default/4_img/uplaod/3933d981/20170622/2fIE-fyhfxph6601959.jpg")
在項目相應文件夾下我們可以看到下載好的 “圖片.jpg” 。
15.6 kotlin.io標準庫
Kotlin 的 io 庫主要是擴展 Java 的 io 庫。下面我們簡單舉幾個例子。
appendBytes
追加字節數組到該文件中
方法簽名:
fun File.appendBytes(array: ByteArray)
appendText
追加文本到該文件中
方法簽名:
fun File.appendText(
text: String,
charset: Charset = Charsets.UTF_8)
bufferedReader
獲取該文件的BufferedReader
方法簽名:
fun File.bufferedReader(
charset: Charset = Charsets.UTF_8,
bufferSize: Int = DEFAULT_BUFFER_SIZE
): BufferedReader
bufferedWriter
獲取該文件的BufferedWriter
方法簽名:
fun File.bufferedWriter(
charset: Charset = Charsets.UTF_8,
bufferSize: Int = DEFAULT_BUFFER_SIZE
): BufferedWriter
copyRecursively
復制該文件或者遞歸復制該目錄及其所有子文件到指定路徑,如果指定路徑下的文件不存在,會自動創建。
方法簽名:
fun File.copyRecursively(
target: File,
overwrite: Boolean = false, // 是否覆蓋。true:覆蓋之前先刪除原來的文件
onError: (File, IOException) -> OnErrorAction = { _, exception -> throw exception }
): Boolean
提示: Kotlin 對 File 的擴展函數 API 文檔https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/index.html |
---|
關于 kotlin.io 下面的API文檔在這里 https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/index.html |
15.7 執行Shell命令行
我們使用 Groovy 的文件 IO 操作感覺非常好用,例如
package com.easy.kotlin
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4)
class ShellExecuteDemoTest {
@Test
def void testShellExecute() {
def p = "ls -R".execute()
def output = p.inputStream.text
println(output)
def fname = "我圖.url"
def f = new File(fname)
def lines = f.readLines()
lines.forEach({
println(it)
})
println(f.text)
}
}
Kotlin 中的文件 IO,網絡 IO 操作跟 Groovy一樣簡單。
另外,從上面的代碼中我們看到使用 Groovy 執行終端命令非常簡單:
def p = "ls -R".execute()
def output = p.inputStream.text
在 Kotlin 中,目前還沒有對 String 類和 Process 擴展這樣的函數。其實擴展這樣的函數非常簡單。我們完全可以自己擴展。
首先,我們來擴展 String 的 execute() 函數。
fun String.execute(): Process {
val runtime = Runtime.getRuntime()
return runtime.exec(this)
}
然后,我們來給 Process 類擴展一個 text函數。
fun Process.text(): String {
var output = ""
// 輸出 Shell 執行的結果
val inputStream = this.inputStream
val isr = InputStreamReader(inputStream)
val reader = BufferedReader(isr)
var line: String? = ""
while (line != null) {
line = reader.readLine()
output += line + "\n"
}
return output
}
完成了上面兩個簡單的擴展函數之后,我們就可以在下面的測試代碼中,可以像 Groovy 一樣執行終端命令了:
val p = "ls -al".execute()
val exitCode = p.waitFor()
val text = p.text()
println(exitCode)
println(text)
實際上,通過之前的很多實例的學習,我們可以看出 Kotlin 的擴展函數相當實用。Kotlin 語言本身API 也大量使用了擴展功能。
15.8 正則表達式
我們在 Kotlin 中除了仍然可以使用 Java中的 Pattern,Matcher 等類之外,Kotlin 還提供了一個正則表達式類 kotlin/text/regex/Regex.kt ,我們通過 Regex 的構造函數來創建一個正則表達式。
15.8.1 構造 Regex 表達式
使用Regex構造函數
>>> val r1 = Regex("[a-z]+")
>>> val r2 = Regex("[a-z]+", RegexOption.IGNORE_CASE)
其中的匹配選項 RegexOption 是直接使用的 Java 類 Pattern中的正則匹配選項。
使用 String 的 toRegex 擴展函數
>>> val r3 = "[A-Z]+".toRegex()
15.8.2 Regex 函數
Regex 里面提供了豐富的簡單而實用的函數,如下表所示
函數名稱 | 功能說明 |
---|---|
matches(input: CharSequence): Boolean | 輸入字符串全部匹配 |
containsMatchIn(input: CharSequence): Boolean | 輸入字符串至少有一個匹配 |
matchEntire(input: CharSequence): MatchResult? | 輸入字符串全部匹配,返回一個匹配結果對象 |
replace(input: CharSequence, replacement: String): String | 把輸入字符串中匹配的部分替換成replacement的內容 |
replace(input: CharSequence, transform: (MatchResult) -> CharSequence): String | 把輸入字符串中匹配到的值,用函數 transform映射之后的新值替換 |
find(input: CharSequence, startIndex: Int = 0): MatchResult? | 返回輸入字符串中第一個匹配的值 |
findAll(input: CharSequence, startIndex: Int = 0): Sequence<MatchResult> | 返回輸入字符串中所有匹配的值MatchResult的序列 |
下面我們分別就上面的函數給出簡單實例。
matches
輸入字符串全部匹配正則表達式返回 true , 否則返回 false。
>>> val r1 = Regex("[a-z]+")
>>> r1.matches("ABCzxc")
false
>>>
>>> val r2 = Regex("[a-z]+", RegexOption.IGNORE_CASE)
>>> r2.matches("ABCzxc")
true
>>> val r3 = "[A-Z]+".toRegex()
>>> r3.matches("GGMM")
true
containsMatchIn
輸入字符串中至少有一個匹配就返回true,沒有一個匹配就返回false。
>>> val re = Regex("[0-9]+")
>>> re.containsMatchIn("012Abc")
true
>>> re.containsMatchIn("Abc")
false
matchEntire
輸入字符串全部匹配正則表達式返回 一個MatcherMatchResult對象,否則返回 null。
>>> val re = Regex("[0-9]+")
>>> re.matchEntire("1234567890")
kotlin.text.MatcherMatchResult@34d713a2
>>> re.matchEntire("1234567890!")
null
我們可以訪問MatcherMatchResult的value熟悉來獲得匹配的值。
>>> re.matchEntire("1234567890")?.value
1234567890
由于 matchEntire 函數的返回是MatchResult? 可空對象,所以這里我們使用了安全調用符號 ?.
。
replace(input: CharSequence, replacement: String): String
把輸入字符串中匹配的部分替換成replacement的內容。
>>> val re = Regex("[0-9]+")
>>> re.replace("12345XYZ","abcd")
abcdXYZ
我們可以看到,"12345XYZ"中12345
是匹配正則表達式 [0-9]+
的內容,它被替換成了 abcd
。
replace(input: CharSequence, transform: (MatchResult) -> CharSequence): String
把輸入字符串中匹配到的值,用函數 transform映射之后的新值替換。
>>> val re = Regex("[0-9]+")
>>> re.replace("9XYZ8", { (it.value.toInt() * it.value.toInt()).toString() })
81XYZ64
我們可以看到,9XYZ8
中數字9和8是匹配正則表達式[0-9]+
的內容,它們分別被transform函數映射 (it.value.toInt() * it.value.toInt()).toString()
的新值 81 和 64 替換。
find
返回輸入字符串中第一個匹配的MatcherMatchResult對象。
>>> val re = Regex("[0-9]+")
>>> re.find("123XYZ987abcd7777")
kotlin.text.MatcherMatchResult@4d4436d0
>>> re.find("123XYZ987abcd7777")?.value
123
findAll
返回輸入字符串中所有匹配的值的MatchResult的序列。
>>> val re = Regex("[0-9]+")
>>> re.findAll("123XYZ987abcd7777")
kotlin.sequences.GeneratorSequence@f245bdd
我們可以通過 forEach 循環遍歷所以匹配的值
>>> re.findAll("123XYZ987abcd7777").forEach{println(it.value)}
123
987
7777
15.8.3 使用 Java 正則表達式類
除了上面 Kotlin 提供的函數之外,我們在 Kotlin 中仍然可以使用 Java 的正則表達式的 API。
val re = Regex("[0-9]+")
val p = re.toPattern()
val m = p.matcher("888ABC999")
while (m.find()) {
val d = m.group()
println(d)
}
上面的代碼運行輸出:
888
999
15.9 Kotlin 的多線程
Kotlin中沒有synchronized關鍵字。
Kotlin中沒有volatile關鍵字。
Kotlin的Any類似于Java的Object,但是沒有wait(),notify()和notifyAll() 方法。
那么并發如何在Kotlin中工作呢?放心,Kotlin 既然是站在 Java 的肩膀上,當然少不了對多線程編程的支持——Kotlin通過封裝 Java 中的線程類,簡化了我們的編碼。同時我們也可以使用一些特定的注解, 直接使用 Java 中的同步關鍵字等。下面我們簡單介紹一下使用Kotlin 進行多線程編程的相關內容。
15.9.1 創建線程
我們在 Java中通常有兩種方法在Java中創建線程:
- 擴展Thread類
- 或者實例化它并通過構造函數傳遞一個Runnable
因為我們可以很容易地在Kotlin中使用Java類,這兩個方式都可以使用。
使用對象表達式創建
object : Thread() {
override fun run() {
Thread.sleep(3000)
println("A 使用 Thread 對象表達式: ${Thread.currentThread()}")
}
}.start()
此代碼使用Kotlin的對象表達式創建一個匿名類并覆蓋run()方法。
使用 Lambda 表達式
下面是如何將一個Runnable傳遞給一個新創建的Thread實例:
Thread({
Thread.sleep(2000)
println("B 使用 Lambda 表達式: ${Thread.currentThread()}")
}).start()
我們在這里看不到Runnable,在Kotlin中可以很方便的直接使用上面的Lambda表達式來表達。
還有更簡單的方法嗎? 且看下文解說。
使用 Kotlin 封裝的 thread 函數
例如,我們寫了下面一段線程的代碼
val t = Thread({
Thread.sleep(2000)
println("C 使用 Lambda 表達式:${Thread.currentThread()}")
})
t.isDaemon = false
t.name = "CThread"
t.priority = 3
t.start()
后面的四行可以說是樣板化的代碼。在 Kotlin 中把這樣的操作封裝簡化了。
thread(start = true, isDaemon = false, name = "DThread", priority = 3) {
Thread.sleep(1000)
println("D 使用 Kotlin 封裝的函數 thread(): ${Thread.currentThread()}")
}
這樣的代碼顯得更加精簡整潔了。事實上,thread()函數就是對我們編程實踐中經常用到的樣板化的代碼進行了抽象封裝,它的實現如下:
public fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit): Thread {
val thread = object : Thread() {
public override fun run() {
block()
}
}
if (isDaemon)
thread.isDaemon = true
if (priority > 0)
thread.priority = priority
if (name != null)
thread.name = name
if (contextClassLoader != null)
thread.contextClassLoader = contextClassLoader
if (start)
thread.start()
return thread
}
這只是一個非常方便的包裝函數,簡單實用。從上面的例子我們可以看出,Kotlin 通過擴展 Java 的線程 API,簡化了樣板代碼。
15.9.2 同步方法和塊
synchronized不是Kotlin中的關鍵字,它替換為@Synchronized 注解。 Kotlin中的同步方法的聲明將如下所示:
@Synchronized fun appendFile(text: String, destFile: String) {
val f = File(destFile)
if (!f.exists()) {
f.createNewFile()
}
f.appendText(text, Charset.defaultCharset())
}
@Synchronized 注解與 Java中的 synchronized 具有相同的效果:它會將JVM方法標記為同步。 對于同步塊,我們使用synchronized() 函數,它使用鎖作為參數:
fun appendFileSync(text: String, destFile: String) {
val f = File(destFile)
if (!f.exists()) {
f.createNewFile()
}
synchronized(this){
f.appendText(text, Charset.defaultCharset())
}
}
跟 Java 基本一樣。
15.9.3 可變字段
同樣的,Kotlin沒有 volatile 關鍵字,但是有@Volatile注解。
@Volatile private var running = false
fun start() {
running = true
thread(start = true) {
while (running) {
println("Still running: ${Thread.currentThread()}")
}
}
}
fun stop() {
running = false
println("Stopped: ${Thread.currentThread()}")
}
@Volatile會將JVM備份字段標記為volatile。
當然,在 Kotlin 中我們有更好用的協程并發庫。在代碼工程實踐中,我們可以根據實際情況自由選擇。
本章小結
Kotlin 是一門工程實踐性很強的語言,從本章介紹的文件IO、正則表達式以及多線程等內容中,我們可以領會到 Kotlin 的基本原則:充分使用已有的 Java 生態庫,在此基礎之上進行更加簡單實用的擴展,大大提升程序員們的生產力。從中我們也體會到了Kotlin 編程中的極簡理念——不斷地抽象、封裝、擴展,使之更加簡單實用。
本章示例代碼:https://github.com/EasyKotlin/chapter15_file_io
另外,筆者綜合了本章的內容,使用 SpringBoot + Kotlin 寫了一個簡單的圖片爬蟲 Web 應用,感興趣的讀者可參考源碼:https://github.com/EasyKotlin/chatper15_net_io_img_crawler
在下一章,也我們的最后一章中,讓我們脫離 JVM,直接使用 Kotlin Native 來開發一個直接編譯成機器碼運行的 Kotlin 應用程序。
Kotlin 開發者社區
國內第一Kotlin 開發者社區公眾號,主要分享、交流 Kotlin 編程語言、Spring Boot、Android、React.js/Node.js、函數式編程、編程思想等相關主題。