博客地址https://sguotao.top/Kotlin-2018-11-01-Kotlin%E4%B8%AD%E7%9A%84IO.html#more
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) {
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) {
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) {
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 |
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 |
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.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在命令行打印输出。
学习资料
- Kotlin Bootcamp for Programmers
- Kotlin Koans