【java I/O流总结】字符流文件读写

最近想学习学习I/O流相关的内容,看到一篇博客:
java IO流学习总结

这篇博客总结得很好,但我还是想按照自己的思路把一些疑点梳理一下。我们知道,IO流分为字符流读写和字节流读写,本篇博客以文件读写作为切入点,总结字符流文件读写相关知识点(字节流暂放)。下面附上IO图解:
image
由图可知字符流的所有类都继承于Writer和Reader,其中直接操作文件读写的类是FileReader和FileWriter,这两者继承InputStreamReader和OutputStreamWriter。而InputStreamReader和OutputStreamWriter是字符和字节的桥梁,也可称之为字符转换流,这个先不细说,两者的区别暂且只需要记住以下这点:

  • FileReader和FileWriter作为其子类,仅作为操作字符文件的便捷类来使用,文件读写采用平台默认的编码,不能手动设置编码。若平台编码跟文件编码不一致,便会出现乱码,此时要使用字符转换流,也要与字节流配套使用,这是后话。

File与FileDescriptor

这两者都是描述文件的相关类,区别如下

  • File:是磁盘文件和目录路径名的一种抽象形式,可表示文件,也可表示文件夹。其直接继承自 Object,实现了 Serializable 接口和 Comparable 接口;实现 Serializable 接口意味着 File 对象可以被序列化,而实现 Comparable 接口意味着 File 对象可以比较大小;此外 File 并没有什么特殊之处,就是对文件的一种上层抽象封装实现,方便操作文件。

  • FileDescriptor:是文件描述符,用来表示开放文件、开放套接字等。当 FileDescriptor 表示文件时,我们可以通俗的将 FileDescriptor 看成是该文件,但是不能直接通过 FileDescriptor 对该文件进行操作,若要通过 FileDescriptor 对该文件进行操作,则需要新创建 FileDescriptor 对应的 FileOutputStream,然后再对文件进行操作。此外,其中的静态变量in/out/err分别代表标准输入、输出、错误流的描述符,例如以下代码:

try {
    FileOutputStream out = new FileOutputStream(FileDescriptor.out);
    out.write('A');
    out.close();
} catch (IOException e) {
}

out就是一个FileDescriptor对象,它是通过构造函数FileDescriptor(int fd)创建的。以上代码相当于System.out.print()的效果,同理,in和err对标System.in和System.err。除此之外,FileDescriptor可以用来定义“文件”、“Socket”等的文件描述符。

字符流中操作文件使用File即可,例如:

File fileHome = new File("C:\\_study\\repo\\JavaTest\\JavaIO\\files");
File srcFile = new File(fileHome,"src.txt");
File destFile = new File(fileHome,"res.txt");

FileWriter fileReader = new FileReader(srcPath);
FileWriter fileWriter = new FileWriter(destFile);

FileWriter与FileReader

使用这两个类读写文件的代码如下所示:

package CharStream;

import java.io.*;
import java.util.Arrays;

public class FileCtrByChar {
    private final FileWriter fileWriter;
    private final FileReader fileReader;

    public FileCtrByChar(File srcFile, File destFile) throws IOException {
        this.fileWriter = new FileWriter(destFile);
        this.fileReader = new FileReader(srcFile);
    }
    /**
     *文本文件复制,逐个读取字符
     */
    public void readWriteByChar() throws IOException {
        //long startTime = System.currentTimeMillis();
        int ch;
        //按照码点来读取
        while((ch = fileReader.read())!=-1){//依次读到文件末尾
            fileWriter.write(ch);//这只是将数据写入缓冲区,需要flush才能将缓冲区中数据写入文件,缓冲区默认大小8K,超过8K会自动写入
        }
    }
    /**
     *设置char数组,一次读一个字符数组
     */
    public void readWriteByCharArr() throws IOException {
        char[] chs = new char[40];
        int len = 0;
        while((len = fileReader.read(chs))!=-1){
            fileWriter.write(chs);
        }
    }

    public void close() throws IOException {
        fileWriter.close();//close之前会调用flush
        fileReader.close();
    }
}

可见,FileWriter的输出过程中也是存在缓冲区的。

flush()和close()区别

  • flush()将缓冲区的内容写入目标,并使缓冲区为空以供其他数据存储,但不会永久关闭流。 这意味着仍然可以向流中写入更多数据。

    扫描二维码关注公众号,回复: 10102703 查看本文章
  • close()会永久关闭流。 如果要进一步写入一些数据,则必须再次重新打开流,并将数据附加到现有数据之后。

注意:因为Writer带有flush(),所以其所有子类都带有缓冲区功能。因此,FileWriter和BufferWriter都有缓冲区;注意读写结束时,一定要刷新或关闭,不然数据可能留在缓冲区内无法完全写入。

使用Buffer进行包装

FileWriter和FileReader可以使用BufferWriter和BufferReader进行包装,代码如下所示。

public void readWriteByCharInBuffer() throws IOException {
    BufferedReader bufferedReader = new BufferedReader(fileReader);
    BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

    int ch;
    while((ch = bufferedReader.read())!=-1){
        bufferedWriter.write(ch);
    }
    bufferedReader.close();
    bufferedWriter.close();
}

public void readWriteByCharArrInBuffer() throws IOException {
    char[] chs = new char[50];
    int len = 0;
    BufferedReader bufferedReader = new BufferedReader(fileReader);
    BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
    while((len = bufferedReader.read(chs))!=-1){
        bufferedWriter.write(chs);
    }
    bufferedReader.close();
    bufferedWriter.close();

}

public void readWriteByLine() throws IOException {
    BufferedReader bufferedReader = new BufferedReader(fileReader);
    BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
    String line;
    while((line = bufferedReader.readLine())!=null){
        bufferedWriter.write(line);
    }
    bufferedReader.close();
    bufferedWriter.close();
}

至于使用buffer包装的好处,网上的一致说法是,引入了缓冲区存取效率更快。缓冲区的好处毋庸置疑:

为什么使用缓冲区效率更高?

  • 当设置缓冲区时,计算机会使用DMA执行IO操作,数据存取完毕之前,会腾出cpu的时间,结束后通知cpu接管后续操作。不使用缓冲时,cpu会主动地进行周期轮询等等操作,查看IO设备是否读完数据,此时cpu不能做其它操作,效率更低。

但是我仍然产生了困惑,FileWriter写文件过程中不是也有缓冲区嘛,读文件不是也可以使用char数组进行缓存嘛,那么Buffer包装的好处究竟体现在哪呢?

于是我进行了一个实验:准备一个140kB的文本文件,使用带Buffer和不带Buffer的方法对其进行读写测试,即将文本复制进另一个文本(复制操作代码见上文),测试读写效率。通过程序执行时间来表达读写效率,记录时间的代码如下:

long startTime = System.currentTimeMillis();
······
long endTime = System.currentTimeMillis();
System.out.println("运行时间:" + (endTime - startTime) + "ms");

分别测试三次,测试结果如下:

方法 FileWriter&FileReader BufferWriter&BufferReader
按字符读写 30ms,38ms,41ms 16ms,16ms,16ms
按char[]读写 16ms,14ms,16ms 15ms,16ms,15ms

测试结果一目了然。如果设置字符数组,读取文件字符数据时,使用Buffer包装和不使用相比,读写效率几乎一致;但按字符逐个读取,使用Buffer的效率就要高出不少。

实际上,阅读源码可知,FileReader和BufferReader两者的read(char[] buf)都继承自Reader,没有差别;但是read()方法,BufferReader对其进行了重写,做了优化。即便是对于一个字符的读取,BufferedReader依然是先读取一个数组的数据到缓冲区中,然后从缓冲区中一个个的取,因此效率更高。

除此之外,BufferReader还有一些FileReader没有的特性,例如提供了readLine()方法,可以逐行读取(相对应的,BufferedWriter中提供newLine()方法支持换行),还可以指定缓冲区的大小,对缓冲区的支持更专业。

因此,在实践中可以无需纠结地使用BufferReader与BufferWriter。

猜你喜欢

转载自www.cnblogs.com/buptleida/p/12556023.html