Kotlin中的IO

博客地址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

猜你喜欢

转载自juejin.im/post/5be0086751882516fc40515f
今日推荐