第9章 文件IO操做、正則表達式與多線程《Kotlin 項目實戰教程》

第9章 文件IO操做、正則表達式與多線程 《Kotlin 項目實戰教程》

咱們在《第6章 擴展函數與屬性》中已經介紹過Kotlin中的類擴展的特性。使用Kotlin的擴展函數功能,咱們能夠直接爲 String 類實現一個 inc() 函數,這個函數把字符串中的每個字符值加1html

"abc".inc() // bcd

這個擴展函數實現以下java

fun String.inc(): String {
    var result = ""
    this.map { result += it + 1 }
    return result
}

正是由於有了強大的擴展函數,咱們能夠在Java類庫的基礎上擴展出大量「看似Java 類中的原生方法」 。而實際上Kotlin的標準庫kotlin-stdlib中大量的API都是經過擴展Java的類來實現的。git

本章咱們將要介紹的文件IO操做、正則表達式與多線程等相關內容都是Kotlin經過擴展Java已有的類來實現的。首先,咱們來介紹文件的讀寫。程序員

9.1 文件 IO 操做

Kotlin IO 操做的 API 在 kotlin.io 包下。Kotlin的原則就是Java已經有好用的就直接使用,沒有的或者很差用的,就在原有類的基礎上進行功能擴展。例如Kotlin 就給 File 類寫了擴展函數。github

Kotlin爲 java.io.File 類擴展了大量好用的擴展函數,這些擴展函數都在 kotlin/io/FileReadWrite.kt 源代碼文件中。咱們將在下文中介紹。正則表達式

同時,Kotlin 也針對InputStream、OutputStream和 Reader 等都作了簡單的擴展。它們主要在下面的兩個源文件中:shell

kotlin/io/IOStreams.kt
kotlin/io/ReadWrite.kt

Koltin 的序列化直接採用的 Java 的序列化類的類型別名:編程

internal typealias Serializable = java.io.Serializable

下面咱們來簡單介紹一下 Kotlin 文件讀寫操做。Kotlin中經常使用的文件讀寫 API 以下表所示api

函數簽名 功能說明
File.readText(charset: Charset = Charsets.UTF_8): String 讀取該文件的全部內容做爲一個字符串返回
File.readLines(charset: Charset = Charsets.UTF_8): List<String> 讀取該文件的每一行內容,存入一個List<String> 返回
File.readBytes(): ByteArray 讀取文件全部內容以ByteArray的方式返回
File.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit 覆蓋寫入text字符串到文件中
File.writeBytes(array: ByteArray): Unit 覆蓋寫入ByteArray字節流數組
File.appendText(text: String, charset: Charset = Charsets.UTF_8): Unit 在文件末尾追加寫入text字符串
File.appendBytes(array: ByteArray): Unit 在文件末尾追加寫入ByteArray字節流數組

9.1.1 讀文件

readText : 獲取文件所有內容字符串

咱們若是簡單讀取一個文件,可使用readText()方法,它直接返回整個文件內容。代碼示例以下數組

fun getFileContent(filename: String): String {
        val f = File(filename)
        return f.readText(Charset.forName("UTF-8"))
    }

咱們直接使用 File 對象來調用 readText 函數便可得到該文件的所有內容,它返回一個字符串。若是指定字符編碼,能夠經過傳入參數Charset來指定,默認是UTF-8編碼。

readLines : 獲取文件每行的內容

若是咱們想要得到文件每行的內容,能夠簡單經過 split("n") 來得到一個每行內容的數組。咱們也能夠直接調用 Kotlin 封裝好的readLines函數,得到文件每行的內容。readLines函數返回一個持有每行內容的字符串 List。

fun getFileLines(filename: String): List<String> {
        return File(filename).readLines(Charset.forName("UTF-8"))
    }

readBytes:讀取字節流數組

咱們若是但願直接操做文件的字節數組,可使用readBytes 函數

//讀取爲bytes數組
    val bytes: ByteArray = f.readBytes()
    println(bytes.joinToString(separator = " "))

    //與 Java 互操做,直接調用Java 中的 InputStream 和 InputStream類
    val reader: Reader = f.reader()
    val inputStream: InputStream = f.inputStream()
    val bufferedReader: BufferedReader = f.bufferedReader()
}

bufferedReader

獲取該文件的BufferedReader

方法簽名:

fun File.bufferedReader(
    charset: Charset = Charsets.UTF_8, 
    bufferSize: Int = DEFAULT_BUFFER_SIZE
): BufferedReader

9.1.2 寫文件

使用Kotlin擴展的函數,寫入文件也變得至關簡單。更讀取文件相似,咱們能夠寫入字符串,也能夠寫入字節流,還能夠直接調用 Java的 Writer 或者 OutputStream 類。寫文件一般分爲覆蓋寫(一次性寫入)和追加寫入兩種狀況。

writeText: 覆蓋寫文件

咱們使用 writeText 函數直接向一個文件中寫入字符串 text 的內容

fun writeFile(text: String, destFile: String) {
        val f = File(destFile)
        if (!f.exists()) {
            f.createNewFile()
        }
        f.writeText(text, Charset.defaultCharset())
    }

其中,destFile 參數是目標文件名(帶目錄)。

appendFile: 末尾追加寫文件

使用 appendFile 函數向一個文件的末尾追加寫入內容 text

fun appendFile(text: String, destFile: String) {
        val f = File(destFile)
        if (!f.exists()) {
            f.createNewFile()
        }
        f.appendText(text, Charset.defaultCharset())
    }

appendBytes

追加字節數組到該文件中

方法簽名:

fun File.appendBytes(array: ByteArray)

bufferedWriter

獲取該文件的BufferedWriter

方法簽名:

fun File.bufferedWriter(
    charset: Charset = Charsets.UTF_8, 
    bufferSize: Int = DEFAULT_BUFFER_SIZE
): BufferedWriter

提示: Kotlin 對 File 的擴展函數 API 文檔https://kotlinlang.org/api/la...

9.1.3 遍歷文件樹

Kotlin 中提供了方便的功能來遍歷文件樹。

walk 函數: 遍歷文件樹

下面的例子遍歷了指定文件夾下的全部文件。

fun traverseFileTree(filename: String) {
        val f = File(filename)
        val fileTreeWalk = f.walk()
        fileTreeWalk.iterator().forEach { println(it.absolutePath) }
    }

測試代碼:

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)
    }

遍歷文件樹須要調用擴展方法walk()。它會返回一個FileTreeWalk對象,它有一些方法用於設置遍歷方向和深度,詳情參見FileTreeWalk API 文檔說明。

提示:FileTreeWalk API 文檔連接 https://kotlinlang.org/api/la...

遞歸複製文件

複製該文件或者遞歸複製該目錄及其全部子文件到指定路徑,若是指定路徑下的文件不存在,會自動建立。

copyRecursively 函數簽名:

fun File.copyRecursively(
    target: File, 
    overwrite: Boolean = false, // 是否覆蓋。true:覆蓋以前先刪除原來的文件
    onError: (File, IOException) -> OnErrorAction = { _, exception -> throw exception }
): Boolean

9.2 網絡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」 。

9.3 執行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 也大量使用了擴展功能。

9.4 正則表達式

咱們在 Kotlin 中除了仍然可使用 Java中的 Pattern,Matcher 等類以外,Kotlin 還提供了一個正則表達式類 kotlin/text/regex/Regex.kt ,咱們經過 Regex 的構造函數來建立一個正則表達式。

9.4.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()

9.4.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 函數

replace 函數簽名以下

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

9.4.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

9.5 多線程編程

Kotlin中沒有synchronized、volatile關鍵字。Kotlin的Any相似於Java的Object,可是沒有wait(),notify()和notifyAll() 方法。

那麼併發如何在Kotlin中工做呢?放心,Kotlin 既然是站在 Java 的肩膀上,固然少不了對多線程編程的支持——Kotlin經過封裝 Java 中的線程類,簡化了咱們的編碼。同時咱們也可使用一些特定的註解, 直接使用 Java 中的同步關鍵字等。下面咱們簡單介紹一下使用Kotlin 進行多線程編程的相關內容。

9.5.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,簡化了樣板代碼。

9.5.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 基本同樣。

9.5.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...

另外,筆者綜合了本章的內容,使用 SpringBoot + Kotlin 寫了一個簡單的圖片爬蟲 Web 應用,感興趣的讀者可參考源碼:https://github.com/EasyKotlin...

相關文章
相關標籤/搜索