IO流是Java中很重要的一部分內容,常用的資料傳輸,檔案的上傳和下載都和它分不開。那麼與Java中的IO相比,Kotlin中的IO又是怎樣的呢?
Java中的IO
Java中的IO根據處理資料的方式,可以分為位元組流和字元流,同時根據傳輸方向的不同,又可以分為輸入流和輸出流。先來看一張Java IO的框架圖。
在這張圖中,整理了在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在命令列列印輸出。