前面我们讲了InputStream和OutputStream,以及与它们相关的FilterInputStream、FilterOutputStream,用来提高读写效率的BufferedInputStream、BuffedOutputStream,这些都是跟字节打交道的,但是我们平时处理文本文件或者进行网络请求,一般都需要直接处理字符,所以从这篇文章开始我们来学习跟字符相关的IO类。这其中最为基础的就是Writer和Reader。

Writer

类定义

writer是用来写入字符流的抽象类。

  1. public abstract class Writer implements Appendable, Closeable, Flushable {}

它实现了Appendable、Closebale和Flushable这三个接口。

重要字段

writer有两个字段我们需要先了解:

  1. /**
  2. * Temporary buffer used to hold writes of strings and single characters
  3. */
  4. private char[] writeBuffer;
  5. /**
  6. * Size of writeBuffer, must be >= 1
  7. */
  8. private static final int WRITE_BUFFER_SIZE = 1024;

第一个writeBuffer作为临时的缓存字符数组,第二个WRITE_BUFFE_SIZE是缓存数组的大小。一会我们会看到缓存字符数组的用处。

方法

单字符写入方法

writer这个抽象类最重要的方法就是write方法,不过它有五个参数不同的write方法,我们一个一个看。
第一个是写入单字符的write方法:

  1. public void write(int c) throws IOException {
  2. synchronized (lock) {
  3. if (writeBuffer == null){
  4. writeBuffer = new char[WRITE_BUFFER_SIZE];
  5. }
  6. writeBuffer[0] = (char) c;
  7. write(writeBuffer, 0, 1);
  8. }
  9. }

首先一个问题,为什么要写入的是字符,而参数类型是int呢?因为java中的char类型是两个字节也就是16位大小的,而int是4字节32位大小的,所以int能完全包含char,那么我们要写入4个字节中的哪两个字节呢?这个方法的注释中给出了解释:
image.png
写入的是低位的两个字节。
我们继续来看逻辑,首先判断writeBuffer也就是上面说的缓存字符数组是否为空,为空的话首先初始化这个字符数组(注意这个方法里面的锁,保证了writeBuffer只会初始化一次),在整个writer类的逻辑中,这个缓存数组都是在write方法中来判断并进行初始化的,并没有在构造方法中初始化,也就是外部完全不知道这个缓存字符数组的存在。然后对于这个写入的int,处理方式是将它作为writeBuffer的第一个元素,然后调用write(char cbuf[],int off,int len)方法来执行写入。而write(char cbuf[],int off,int len)这个方法是一个抽象方法,是子类需要给出实现的:

  1. abstract public void write(char cbuf[], int off, int len) throws IOException;

从现在来看,缓存字符数组与我们之前学到的BufferedOutputStream中的缓存字节数组的功能并不相同,BufferedOutputStream中的字节数组用来对多次的write方法调用中的字节进行累积并保存然后一次性进行写入来提高效率,而Writer中的writeBuffer并没有累积,而是每次调用仅保存一下就直接写入,那这样做有什么意义呢?

多字节写入方法

多字节写入的write方法参数是一个字符数组:

  1. public void write(char cbuf[]) throws IOException {
  2. write(cbuf, 0, cbuf.length);
  3. }

�它调用的也是上面的抽象write方法。

字符串写入方法

字符串写入的write方法的参数是一个字符串:

  1. public void write(String str) throws IOException {
  2. write(str, 0, str.length());
  3. }

它调用的是另外一个写入字符串的方法:

  1. public void write(String str, int off, int len) throws IOException {
  2. synchronized (lock) {
  3. char cbuf[];
  4. if (len <= WRITE_BUFFER_SIZE) {
  5. if (writeBuffer == null) {
  6. writeBuffer = new char[WRITE_BUFFER_SIZE];
  7. }
  8. cbuf = writeBuffer;
  9. } else { // Don't permanently allocate very large buffers.
  10. cbuf = new char[len];
  11. }
  12. str.getChars(off, (off + len), cbuf, 0);
  13. write(cbuf, 0, len);
  14. }
  15. }

它的逻辑也不复杂,就是把要写入的字符串中的字符给到一个字符数组cbuf,如果要写入的字符的长度小于等于默认的字符数组的长度,那么cbuf其实就是writeBuffer,然后同样是调用write(cbuf, 0, cbuf.length)来执行最终的写入。
所以不管是单字符、多字符还是字符串的写入,最后都是通过下面这个方法来实现写入的:

  1. abstract public void write(char cbuf[], int off, int len) throws IOException;

所以Writer的子类至少要给出这个方法的实现才行。

Reader

类定义

  1. public abstract class Reader implements Readable, Closeable{}

Reader实现了Readable和Closeable接口。

方法

Reader提供了很多read方法,子类需要给出下面多字符read方法的实现:

  1. abstract public int read(char cbuf[], int off, int len) throws IOException;

其他的read方法都直接或间接地调用了这个方法。


单字符read方法

  1. public int read() throws IOException {
  2. char cb[] = new char[1];
  3. if (read(cb, 0, 1) == -1)
  4. return -1;
  5. else
  6. return cb[0];
  7. }

它首先声明了一个字符数组cb,然后调用的是抽象的read方法。

多字符read方法

  1. public int read(char cbuf[]) throws IOException {
  2. return read(cbuf, 0, cbuf.length);
  3. }

它也是直接调用抽象的多字符read方法来实现的。

与InputStream、OutputStream的对比

InputStream和OutputStream中对单字节的read、write方法是抽象的,需要子类给出实现,其他的都是通过调用单字节的read、write方法来实现的。而Reader和Writer中都是多字符的read、write方法是抽象的,需要子类给出实现,而单字符的read、write方法是通过调用多字符方法来实现的。