Kotlin中的IO

ScottSong發表於2018-11-05

部落格地址sguotao.top/Kotlin-2018…

IO流是Java中很重要的一部分內容,常用的資料傳輸,檔案的上傳和下載都和它分不開。那麼與Java中的IO相比,Kotlin中的IO又是怎樣的呢?

Java中的IO

Java中的IO根據處理資料的方式,可以分為位元組流和字元流,同時根據傳輸方向的不同,又可以分為輸入流和輸出流。先來看一張Java IO的框架圖。

20181102154112543525520.png
在這張圖中,整理了在Java 8中根據上述分類的IO流,其中位元組輸入流有28種,位元組輸出流有18種,字元輸入流有9種,字元輸出流有8種,看到這麼多的流,實際開發中經常使用到的只是其中的一部分。比如位元組輸入流中的FileInputStream、BufferedInputStream,位元組輸出流中的FileOutputStream、BufferedOutputStream,字元輸入流中的BufferedReader、InputStreamReader、FileReader,字元輸出流中的BufferedWriter、OutputStreamWriter、FileWriter等。在圖中已用黑色框圖進行了突出標註。

在Java中對流的處理,需要注意異常的處理,同時注意流的關閉操作,否則可能會引起記憶體溢位。比如使用BufferedReader讀取專案目錄下的build.gradle檔案。

public static void main(String[] args) {
        BufferedReader bufferedReader = null;
        try {
            bufferedReader = new BufferedReader(new FileReader(new File("build.gradle")));
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
複製程式碼

Kotlin中的IO

先給出上面使用BufferedReader讀取build.gradle檔案的Kotlin的幾種寫法,然後再來總結一下Kotlin中的IO。

fun main(args: Array<String>) {
    val file = File("build.gradle")
    val bufferedReader = BufferedReader(FileReader(file))
    var line: String

    try {
        while (true) {
            line = bufferedReader.readLine() ?: break
            println(line)
        }
    } catch (e: Exception) {
        e.printStackTrace()
    } finally {
        try {
            bufferedReader.close()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}
複製程式碼

這種寫法似乎與Java的寫法沒有什麼區別,怎麼體現Kotlin的優勢呢?好在Kotlin封裝了很多高階函式,下面給出一個使用高階函式的版本。

fun main(args: Array<String>) {
    val file = File("build.gradle")
    BufferedReader(FileReader(file)).use {
        var line: String
        while (true) {
            line = it.readLine() ?: break
            println(line)
        }
    }
}
複製程式碼

程式碼精簡了不少,省略了一些對異常的捕獲,這是因為這樣的模板程式碼kotlin已經封裝好了,所以再也不用擔心忘記對流進行close了。對一些小檔案的讀取,還有更簡便的寫法:

fun main(args: Array<String>) {
    File("build.gradle").readLines().forEach(::println)
}
複製程式碼

Kotlin中關於IO的擴充套件函式

那麼Kotlin中都提供了哪些擴充套件函式呢?這些擴充套件函式實現的效果又是怎樣的?Kotlin所有與IO相關的都放在kotlin.io這個包中,可以看到Kotlin並沒有對Java中已經存在的IO流進行重複實現,而是對經常用到的一些位元組流,字元流進行了擴充套件。這裡我們可以將擴充套件物件的不同,將這些擴充套件函式分成以下幾類。

關於擴充套件函式的使用,可以將擴充套件函式看作是被擴充套件物件的成員方法,比如bufferedReader(charset: Charset)函式的被擴充套件物件是InputStream,那麼我麼就可以在InputStream及其子類上呼叫該方法,比如:

    val inputStream: InputStream = FileInputStream("build.gradle")
    inputStream.bufferedReader().forEachLine { line ->
        println(line)
    }
複製程式碼

Kotlin對位元組流的擴充套件

Kotlin對位元組流的擴充套件主要位於ByteStreamKt.class中,下面分別介紹一下:

序號 擴充套件函式名 被擴充套件物件 描述
1 buffered((bufferSize: Int) InputStream 對位元組輸入流進行包裝,得到一個帶緩衝區的位元組輸入流BufferedInputStream,緩衝區預設大小為8*1024位元組。
2 bufferedReader(charset: Charset) InputStream 對位元組輸入流進行包裝得到一個帶緩衝區的字元輸入流BufferedReader,預設的字元編碼是UTF-8。
3 copyTo(out: OutputStream, bufferSize: Int ) InputStream 將位元組輸入流複製到給定的輸出流,返回複製的位元組數,緩衝區大小預設為8*1024位元組,需要注意的是兩個流都需要手動的close。
4 readBytes(estimatedSize: Int) InputStream 將位元組輸入流讀入到一個大小不超過8*1024的位元組陣列中。
5 reader(charset: Charset) InputStream 對位元組輸入流進行包裝得到一個字元輸入流InputStreamReader,預設的字元編碼是UTF-8。
6 buffered(bufferSize: Int) OutputStream 對位元組輸入流進行包裝得到一個帶緩衝區的位元組輸出流BufferedOutputStream,緩衝區的預設大小為8*1024位元組。
7 bufferedWriter(charset: Charset) OutputStream 對位元組輸出流進行包裝得到一個帶緩衝區的字元輸出流BufferedWriter,字元的預設編碼是UTF-8。
8 writer(charset: Charset) OutputStream 對位元組輸出流進行包裝得到一個字元輸出流OutputStreamWriter,預設的字元編碼是UTF-8。
9 inputStream() ByteArray 為給定的位元組陣列建立一個位元組輸入輸入流ByteArrayInputStream,來讀取該位元組陣列。
10 inputStream(offset: Int, length: Int) ByteArray 為給定的位元組陣列建立一個位元組輸入流ByteArrayInputStream,來讀取該陣列,其中offset是讀取位置,這個位置是相對起始位置的偏移量,length是讀取長度。
11 byteInputStream(charset: Charset) String 為給定的字串建立一個位元組輸入流ByteArrayInputStream,預設按UTF-8編碼。

Kotlin對字元流的擴充套件

Kotlin對字元流的擴充套件主要位於TextStreamKt.class中,我們對這些擴充套件函式逐個介紹:

序號 擴充套件函式名 被擴充套件物件 描述
1 buffered(bufferSize: Int) Reader 對字元輸入流進行包裝得到一個帶緩衝區的字元輸入流BufferedReader,緩衝區預設大小為8*1024位元組。
2 copyTo(out: Writer, bufferSize: Int) Reader 將字元輸入流複製給一個給定的字元輸出流,返回複製的字元數,緩衝區預設大小為8*1024位元組。需要注意的是兩個流需要手動的close。
3 forEachLine(action: (String) -> Unit) Reader 遍歷字元輸入流Reader讀取的每一行,同時對每一行呼叫傳入的函式,處理完成後會關閉流。這個傳入函式帶一個String型別的引數,沒有返回值。
4 readLines() Reader 將字元輸入流讀取的每一行陣列,存入List,讀取完成後返回該List。需要注意的是不能用該函式讀取比較大的檔案,否則會引起記憶體溢位。
5 readText() Reader 將字元輸入流讀到的內容以字串的形式返回。需要手動關閉流。
6 useLines(block: (Sequence) -> T) Reader 將字元輸入流Reader讀取的內容儲存在一個字元序列中,在字元序列上執行傳入的lambda表示式,處理完後後會關閉流 ,將lambda表示式的返回值作為函式的返回值。
7 buffered(bufferSize: Int) Writer 對字元輸出流進行包裝,得到一個帶緩衝區的字元輸出流BufferedWriter,緩衝區預設大小為8*1024位元組。
8 readBytes() URL 將URL返回的內容讀取到位元組陣列,位元組陣列預設大小為8*1024位元組,需要注意不能讀取大檔案,否則可能會引起記憶體溢位。
9 readText(charset: Charset) URL 將URL返回的內容以字串的形式返回,預設的字元編碼是UTF-8,需要注意不能讀取大檔案,否則可能會引起記憶體溢位。
10 reader() String 為給定的字串建立一個字元輸入流StringReader。

Kotlin對File的擴充套件

Kotlin對File的擴充套件主要位於FileKt.class中,下面介紹一下這些擴充套件函式:

序號 擴充套件函式 被擴充套件物件 描述
1 appendBytes(array: ByteArray) File 對檔案追加指定位元組陣列大小的內容。
2 appendText(text: String, charset: Charset File 對檔案追加指定內容,預設的字元編碼為UTF-8。
3 bufferedReader(charset: Charset, bufferSize: Int ) File 對檔案進行包裝,獲取一個帶緩衝區的字元輸入流,輸入流的預設編碼是UTF-8,緩衝區預設大小為8*1024位元組。
4 bufferedWriter(charset: Charset, bufferSize: Int) File 對檔案進行包裝,獲取一個帶緩衝區的字元輸出流,輸出流的預設編碼是UTF-8,緩衝區預設大小為8*1024位元組。
5 copyRecursively(target: File,overwrite: Boolean, onError: (File, IOException)) File 遞迴地複製檔案,該函式接收三個引數,copy檔案的目的地址target,是否進行覆蓋overwrite,預設值是false不覆蓋,異常處理的onError,預設丟擲異常。函式的返回值true 複製完成,複製過程中被中斷都會返回false。如果指定的目的地址沒有檔案,則建立檔案;如果File指向的是單個檔案,則直接複製檔案到target目錄下;如果File指向的是目錄,則遞迴的複製目錄下所有的子目錄及檔案到target目錄下;如果target指定的File已經存在,根據overwrite來控制是否進行覆寫操作;檔案的一些屬性資訊,如建立日期,讀寫許可權,複製時是不進行儲存的。接受一個表示式來處理異常,預設是丟擲異常。需要注意的是,如果複製失敗,可能會出現部分複製的情況。
6 copyTo(target: File, overwrite: Boolean, bufferSize: Int ) File 複製檔案到指定路徑,如果指定路徑不存在檔案則建立;如果存在根據overwrite引數控制是否進行覆寫;如果target指向的是一個目錄,並且overwrite設定為true,則只有該目錄為空時,檔案才會被複制。如果File指向的是一個目錄,那麼呼叫該方法,只會建立target指定的目錄,不會複製目錄的內容。最後檔案的一些屬性資訊,如建立日期,讀寫許可權,複製時是不進行儲存的。
7 deleteRecursively() File 遞迴刪除檔案,如果檔案指向的是目錄,會遞迴刪除目錄下的內容。需要注意的是,如果遞迴刪除失敗,可能會出現部分刪除的情況。
8 endsWith(other: File) File 判斷檔案的路徑是否以給定的檔案other的路徑結尾。
9 endsWith(other: String) File 判斷檔案的路徑是否與給定字串指向的路徑在同一根目錄下,並且檔案路徑以字串結尾。
10 forEachBlock(action: (buffer: ByteArray, bytesRead: Int) -> Unit) File 該函式接收一個表示式,表示式有兩個引數,位元組陣列buffer和Int型bytesRead,表示式沒有返回值。函式按照位元組陣列(預設大小為4096位元組)的長度來讀取檔案,每當讀取一位元組陣列的內容,就呼叫一次傳入的表示式。比如檔案大小為7409位元組,那麼就會呼叫兩次表示式,第一次是在讀取4096位元組時,第二次是在讀取餘下的3313位元組。
11 forEachBlock(blockSize: Int, action: (buffer: ByteArray, bytesRead: Int) -> Unit) File 該函式實現的功能與第10個函式功能相同,區別是可以指定位元組陣列buffer的大小。
12 forEachLine(charset: Charset = Charsets.UTF_8, action: (line: String) -> Unit) File 該函式接收一個字元編碼引數charset和一個表示式,表示式接收一個String型別引數,沒有返回值。該函式實現按照指定字元編碼(預設是UTF-8)按行來讀取檔案,每讀取一行,就呼叫一次傳入的表示式。該函式可以用來讀取大檔案。
13 inputStream() File 對檔案進行包裝,得到一個位元組輸入流InputStream。
14 normalize() File 移除檔案路徑中包含的. 並且解析..比如檔案路徑為File("/foo/./bar/gav/../baaz")那麼呼叫normalize()之後的結果為File("/foo/bar/baaz")。
15 outputStream() File 對檔案進行包裝,得到一個位元組輸出流OutputStream。
16 printWriter(charset: Charset) File 對檔案進行包裝,得到一個字元輸出流PrintWriter。預設字元編碼是UTF-8 。
17 readBytes() File 將檔案內容讀取到位元組陣列中,並且返回該位元組陣列。該函式不建議讀取大檔案,否則會引起記憶體溢位。函式內部限制位元組陣列的大小不超過2GB。
18 reader(charset: Charset) File 對檔案進行包裝,得到一個字元輸入流InputStreamReader。預設字元編碼是UTF-8。
19 readLines(charset: Charset) File 按照指定字元編碼,將檔案按照行,讀入到一個LIst中,並且返回該List。預設字元編碼是UTF-8。該方法不建議讀取大檔案,可能會引起記憶體溢位。
20 readText(charset: Charset) File 按照指定字元編碼,讀取整個檔案,並且以String的型別返回讀取的內容。**該方法不建議讀取大檔案,可能會引起記憶體溢位。**函式內部限制檔案大小為2GB。
21 relativeTo(base: File) File 計算與指定檔案base的相對路徑。這裡的引數base被當做一個檔案目錄,如果檔案與base的路徑相同,返回一個空路徑。如果檔案與base檔案具有不同的根路徑,會丟擲IllegalArgumentException。
22 File.relativeToOrNull(base: File) File 計算與指定檔案base的相對路徑,如果檔案路徑與base相同,返回一個空路徑,如果檔案路徑與base具有不同的根路徑,返回null。
23 relativeToOrSelf(base: File) File 計算與指定檔案base的相對路徑,如果檔案路徑與base相同,返回一個空路徑,如果檔案路徑與base具有不同的根路徑,返回檔案自身。
24 resolve(relative: File) File 將指定檔案relatived的路徑新增到當前檔案的目錄中,如果relative檔案有根路徑,返回relative檔案本身,否則返回新增後的檔案路徑。比如當前檔案路徑File("/foo/bar")呼叫.resolve(File("gav"))後的結果為File("/foo/bar/gav")。
25 resolve(relative: String) File 將指定路徑relative新增到當前檔案的目錄中。
26 resolveSibling(relative: File) File 將指定檔案relative的路徑新增到當前檔案的上一級目錄中,如果relative檔案有根路徑,返回relative檔案自身,否則返回新增之後的檔案路徑。比如當前檔案路徑File("/foo/bar")呼叫.resolve("gav")後的結果為File("/foo/bar/gav")。
27 resolveSibling(relative: String) File 將指定路徑relative新增到當前檔案目錄的上一級目錄中。
28 startsWith(other: File) File 判斷當前檔案是否與給定檔案other是否有相同的根目錄,並且當前檔案的路徑是否以指定的檔案路徑開頭。
29 startsWith(other: String) File 判斷當前檔案是否與跟定的路徑具有相同的根目錄,並且當前檔案的路徑是否以給定的路徑開頭。
30 toRelativeString(base: File) File 計算當前檔案與指定檔案base的相對路徑,如果當前檔案路徑與base路徑相同,返回一個空字串,如果當前檔案與base具有不同的根路徑,丟擲IllegalArgumentException。
31 useLines(charset: Charset = Charsets.UTF_8, block: (Sequence) -> T) File 該函式接收兩個引數:一個字元編碼charset和一個表示式。預設字元編碼是UTF-8,表示式接收一個字元序列,並且返回一個泛型,表示式的返回值作為該函式的返回值。這個函式與forEachline()函式很相似,區別是該函式返回一個字元序列,並且返回在返回字元序列時會關閉流。
32 walk(direction: FileWalkDirection = FileWalkDirection.TOP_DOWN) File 按照指定的順序(top-down、bottom-up),使用深度優先遍歷當前目錄及目錄下的內容,預設的順序是自頂向下即top-down,得到一個檔案訪問序列。
33 walkBottomUp() File 按照自底向上的順序遍歷當前目錄及目錄下的內容,該函式使用深度優先遍歷,得到一個檔案訪問序列,即先訪問檔案,後訪問檔案目錄的序列。
34 walkTopDown() File 按照自頂向下的順序遍歷當前目錄及目錄下的內容,該函式使用深度優先遍歷,得到一個檔案訪問序列,即先訪問檔案目錄,後訪問檔案的序列。
35 writeBytes(array: ByteArray) File 將指定位元組陣列的內容寫入檔案,如果檔案已經存在,則被覆寫。
36 writer(charset: Charset = Charsets.UTF_8) File 對檔案包裝,得到一個字元輸出流OutputStreamWriter,預設字元編碼是UTF-8。
37 writeText(text: String, charset: Charset = Charsets.UTF_8) File 將指定字串內容,按照預設UTF-8的編碼方式,寫入檔案,如果檔案已經存在,則會被覆寫。

Kotlin其它的IO擴充套件

Java中IO 流就繼承了Closeable介面,在kotlin.io中的CloseableKt.class中,有一個use的擴充套件函式:

public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    var exception: Throwable? = null
    try {
        return block(this)
    } catch (e: Throwable) {
        exception = e
        throw e
    } finally {
        when {
            apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
                try {
                    close()
                } catch (closeException: Throwable) {
                    // cause.addSuppressed(closeException) // ignored here
                }
        }
    }
}
複製程式碼

use函式封裝了try...catch...finally模板程式碼,這就是在kotlin中,在IO流上使用use時,不用對流進行關閉的原因,因為kotlin已經對其進行了封裝。

在kotlin.io中,ConsoleKt.class中封裝瞭如System.out.print等終端IO的操作,在Kotlin中可以直接使用print、println在命令列列印輸出。

學習資料

  1. Kotlin Bootcamp for Programmers
  2. Kotlin Koans

相關文章