Java IO源码分析 - Reader,Writer系列(二)

说明

整个系列的文章全部参考或直接照搬下面两位作者的文章,这里只是根据自己需要对原作者的文章梳理的总结,仅给自己日后复习时提供思路,如有读者看到学习时建议移步原作。再次重申并非我所写

另两篇本人总结的IO系列

HikariCP:Java I/O源码分析 - InputStream,OutputStream系列
HikariCP:Java IO源码分析 - Reader,Writer系列(一)

PipedReader,PipedWriter

PipedReader与PipedInputStream极其相似,PipedWriter与PipedOutputStream也极其相似。

PipedReader与PipedWriter分别为字符管道输入流和字符管道输出流。管道输入流通过连接到管道输出流实现了类似管道的功能,用于线程之间的通信。

通常,由某个线程向管道输出流中写入数据。根据管道的特性,这些数据会自动发送到与管道输出流对应的管道输入流中。这时其他线程就可以从管道输入流中读取数据,这样就实现了线程之间的通信。

PipedReader为字符管道输入流,用于读取对应的字符管道输出流写入其内置字符缓存数组buffer中的字符、借此来实现线程之间的通信。

PipedWriter为字符管道输出流、用于将当前线程的指定字符写入到与此线程对应的管道字符输入流中。

PipedReader

public class PipedReader extends Reader {
    //管道输出流是否关闭
    boolean closedByWriter = false;
    //管道输入流是否关闭
    boolean closedByReader = false;
    //管道输入流是否被连接
    boolean connected = false;

    //从管道中读取数据的线程
    Thread readSide;
    //向管道中写入数据的线程
    Thread writeSide;

   /**
    * 管道循环输入缓冲区的默认大小。
    */
    private static final int DEFAULT_PIPE_SIZE = 1024;

    /**
     * 放置数据的循环缓冲区。
     */
    char buffer[];

    /**
     * 缓冲区的位置,当从连接的管道输出流中接收到下一个数据字符时,会将其存储到该位置。
     */
    int in = -1;

    /**
     * 缓冲区的位置,此管道输入流将从该位置读取下一个数据字节。
     */
    int out = 0;
    
    /**
     * 创建PipedReader,并指定其对应的PipedWriter。
     */
    public PipedReader(PipedWriter src) throws IOException {
        this(src, DEFAULT_PIPE_SIZE);
    }

    /**
     * 创建一个PipedReader,使其连接到管道输出流src,并指定管道大小为pipeSize。
     * @since      1.6
     */
    public PipedReader(PipedWriter src, int pipeSize) throws IOException {
        initPipe(pipeSize);
        connect(src);
    }


    /**
     * 创建尚未连接的PipedReader。
     */
    public PipedReader() {
        initPipe(DEFAULT_PIPE_SIZE);
    }

    /**
     * 创建一个尚未连接的PipedReader,并指定管道大小为pipeSize。
     * @since      1.6
     */
    public PipedReader(int pipeSize) {
        initPipe(pipeSize);
    }
}

对于Piped系列的字符字节输入输出流,刚开始对其中的in和out两个变量还不太明白。第二遍刷类似的内容,果然一目了然

receive,

/**
 * 接收一个字符,将其插入到缓冲区。如果没有可用的输入,方法会阻塞。
 */
synchronized void receive(int c) throws IOException {
    //检查PipedReader的状态是否正常。
    if (!connected) {
        throw new IOException("Pipe not connected");
    } else if (closedByWriter || closedByReader) {
        throw new IOException("Pipe closed");
    } else if (readSide != null && !readSide.isAlive()) {
        throw new IOException("Read end dead");
    }

    ///获取将数据写入管道的线程,状态设置
    writeSide = Thread.currentThread();
    // 如果被写入管道的数据刚好被读完 或者
    // 管道已经被塞满 两种情况
    while (in == out) {
        if ((readSide != null) && !readSide.isAlive()) {
            throw new IOException("Pipe broken");
        }
        /* full: kick any waiting readers */
        // 不分作用线程的情况直接唤醒一个跑,能跑通的跑就行
        notifyAll();
        try {
            wait(1000);
        } catch (InterruptedException ex) {
            throw new java.io.InterruptedIOException();
        }
    }
    //???
    if (in < 0) {
        in = 0;
        out = 0;
    }
    //将数据字节写入到缓冲区中
    buffer[in++] = (char) c;
    //如果in已经超出了缓冲区的范围,将in置为0,从头开始写
    if (in >= buffer.length) {
        in = 0;
    }
}

对于函数中notifyAll()函数调用的解释:

因为该输入输出流就是供多线程数据交换使用,可能此时锁外挂着一堆读写线程。全部唤醒,因为无论这两种情况的哪一种不分线程是谁哪个能跑下去让它跑就行,跑不了的全部wait 1秒。

疑问

对于这种处理方式的==疑问==:

虽然程序肯定会自己调通,但是不懂的是应该这样效率很差,为什么不用ReentrantLock解决。分成两种锁控制,根据不同的条件来控制作用线程的执行?难道是因为无法判断此时缓冲池是满还是空?(自己想了想好像是无法判断) 那新问题就是为什么不新加一个volatile变量控制当前线程的前一次作用缓冲池的线程是writeSide还是readSide?

receivedLast,read,close


/**
 * 管道输出流关闭时(PipedWriter.close()中会调用此方法),通知其已经关闭。
 */
synchronized void receivedLast() {
    closedByWriter = true;
    notifyAll();
}

/**
 * 将最多len个数据字节从此管道输入流读入char数组。
 * 
 * 如果已到达数据流的末尾,或者len超出管道缓冲区大小,则读取的字符数将少于len。
 * 
 * 如果len为0,则不读取任何字节并返回0;
 * 否则,在至少1个输入字符可用、检测到流末尾、抛出异常前,该方法将一直阻塞。
 */
public synchronized int read(char cbuf[], int off, int len)  throws IOException {
    // 状态判断
    if (!connected) {
        throw new IOException("Pipe not connected");
    } else if (closedByReader) {
        throw new IOException("Pipe closed");
    } else if (writeSide != null && !writeSide.isAlive()
               && !closedByWriter && (in < 0)) {
        throw new IOException("Write end dead");
    }

    // 参数判断
    if ((off < 0) || (off > cbuf.length) || (len < 0) ||
        ((off + len) > cbuf.length) || ((off + len) < 0)) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return 0;
    }

    // 特殊情况判断
    /* possibly wait on the first character */
    int c = read();
    if (c < 0) {
        return -1;
    }
    cbuf[off] =  (char)c;
    int rlen = 1;
    while ((in >= 0) && (--len > 0)) {
        cbuf[off + rlen] = buffer[out++];
        rlen++;
        if (out >= buffer.length) {
            out = 0;
        }
        if (in == out) {
            /* now empty */
            in = -1;
        }
    }
    return rlen;
}

/**
 * 关闭此传送流并释放与该流相关的所有系统资源。
 */
public void close()  throws IOException {
    in = -1;
    closedByReader = true;
}

从close的设计可以看出,由于早期设计架构的思考,该类的开发人员明显的清楚,该类对于输入流操作的函数进入与非执行结束退出的关键判断条件是in和closedByReader变量。所以这里只要对其状态进行设置即可。

感悟

写代码一定要像这些大师一样,特别有条理,1、先对该PipedReader类进行状态的判断,2、然后进行输入参数的判断,3、然后进行特殊情况的判断处理,4、进行程序优化。可见目前自己青铜

总结

  • PipedReader和PipedInputStream的几乎一模一样。区别在于PipedReader操作的是字符,PipedInputStream操作的是字节;
  • PipedReader有ready方法来判断是否可以从PipedReader中读数据,而PipedInputStream则根据available()方法进行判断。

PipedWriter

flush

/**
 * 刷新此输出流并强制写出所有缓冲的输出字节。
 * 这将通知所有读取数据的线程,告知它们管道中的字符处于等待中。
 */
public synchronized void flush() throws IOException {
    if (sink != null) {
        if (sink.closedByReader || closed) {
            throw new IOException("Pipe closed");
        }
        synchronized (sink) {
            sink.notifyAll();
        }
    }
}

我们知道sink在PipedWriter类中指的是和其连接的PipedReader对象,也即两者操作的是同一个对象。所以如果PipedReader类的当前对象在进行receive函数或者read函数被wait的时候会挂到该对象的线程等待集合中。此时缓冲池装填可能是满了,也可能空了。

所以当flush被调用时,明确的是该缓冲池中的数据要被读走了。然后唤醒该对象等待队列上的线程即可,无所谓唤醒的是读线程还是写线程,读线程一定可以正常运行下去,因为其进入wait情况的先决条件是in<0很明显这时不可能,相反写线程肯定会继续卡住,因为其进入wait条件的先决条件是in==out很明显这很有可能。

总结

  • PipedWriter和PipedOutputStream的几乎一模一样。区别在于PipedReader操作的是字符,PipedInputStream操作的是字节。

InputStreamReader,OutputStreamWriter

InputStreamReader和OutputStreamWriter 是字节流通向字符流的桥梁:它使用指定的 charset 读写字节并将其解码为字符。

  • InputStreamReader 的作用是将“字节输入流”转换成“字符输入流”。它继承于Reader。
  • OutputStreamWriter 的作用是将“字节输出流”转换成“字符输出流”。它继承于Writer。

InputStreamReader

由于InputStreamReader类的函数全是依赖其内部声明的StreamDecoder对象来作用的。所以这里我们大概了解一下该类的各个函数的返回结果即可。

// 将“字节输入流”转换成“字符输入流”
public class InputStreamReader extends Reader {
    // InputStreamReader的功能是依赖StreamDecoder完成的
    private final StreamDecoder sd;

    // 根据in创建InputStreamReader,使用默认的编码
    public InputStreamReader(InputStream in) {
        super(in);
        try {
            sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
        } catch (UnsupportedEncodingException e) {
            // The default encoding should always be available
            throw new Error(e);
        }
    }
    
    // 获取解码器
    public String getEncoding() {
        return sd.getEncoding();
    }

    // 读取并返回一个字符
    public int read() throws IOException {
        return sd.read();
    }

    // 将InputStreamReader中的数据写入cbuf中,从cbuf的offset位置开始写入,写入长度是length
    public int read(char cbuf[], int offset, int length) throws IOException {
        return sd.read(cbuf, offset, length);
    }

    // 能否从InputStreamReader中读取数据
    public boolean ready() throws IOException {
        return sd.ready();
    }

    // 关闭InputStreamReader
    public void close() throws IOException {
        sd.close();
    }
}

感觉像是给InutStream穿了个外衣。将字节流通过流解码器StreamDecoder,解码成了字符流。

OutputStreamWriter

OutputStreamWriter 作用和原理都与InputStreamReader一模一样。
作用就是将“字节输出流”转换成“字符输出流”。它的原理是,我们创建“字符输出流”对象时,会指定“字节输出流”以及“字符编码”。

演示程序

/**
 * InputStreamReader 和 OutputStreamWriter 测试程序
 *
 * @author skywang
 */
public class StreamConverter {

    private static final String FileName = "file.txt";
    private static final String CharsetName = "utf-8";
    //private static final String CharsetName = "gb2312";

    public static void main(String[] args) {
        testWrite();
        testRead();
    }

    /**
     * OutputStreamWriter 演示函数
     *
     */
    private static void testWrite() {
        try {
            // 创建文件“file.txt”对应File对象
            File file = new File(FileName);
            // 创建FileOutputStream对应OutputStreamWriter:将字节流转换为字符流,即写入out1的数据会自动由字节转换为字符。
            OutputStreamWriter out1 = new OutputStreamWriter(new FileOutputStream(file), CharsetName);
            // 写入10个汉字
            out1.write("字节流转为字符流示例");
            // 向“文件中”写入"0123456789"+换行符
            out1.write("0123456789\n");

            out1.close();

        } catch(IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * InputStreamReader 演示程序
     */
    private static void testRead() {
        try {
            // 方法1:新建FileInputStream对象
            // 新建文件“file.txt”对应File对象
            File file = new File(FileName);
            InputStreamReader in1 = new InputStreamReader(new FileInputStream(file), CharsetName);

            // 测试read(),从中读取一个字符
            char c1 = (char)in1.read();
            System.out.println("c1="+c1);

            // 测试skip(long byteCount),跳过4个字符
            in1.skip(6);

            // 测试read(char[] cbuf, int off, int len)
            char[] buf = new char[10];
            in1.read(buf, 0, buf.length);
            System.out.println("buf="+(new String(buf)));

            in1.close();
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

c1=字
buf=流示例0123456

问题1,char为什么能表示中文呢

看了下面这篇文章很好理解这篇博文

答:因为java是用unicode字符编码表来对应字符的,"字"这个中文字符的unicode就是2个字节(且所有收录的中文都是2个字节的,本人粗略算了一下2万多个收录了)。且如果其给定的int值范围超过0-65535的范围,如果强制转换(char c = (char)i;)其会自动mod 65536(此处只针对大于)。java会根据该规则自动转换到0-65535区间中的一个。而“字”这个字的unicode编码其实是23383。

问题2,UTF-8方式存储的数据,StreamDecoder类怎么精准算出原数据占几个字节,并还原原始数据

看了下面这篇阮一峰老师的文章很好理解字符编码笔记:ASCII,Unicode 和 UTF-8

答:也就是因为UTF-8方式读取字符的时候可以精准的找到整个字符所占的所有字节呀!这也就是为什么它的中文占3甚至4个字节的原因,因为UTF-8编码方式的每个字节都有标识位,需要耗费一些空间。还是挺好理解的

重点3

一定要搞懂一件事:

Unicode 是「字符集」
UTF-8 是「编码规则」

其中:

  • 字符集:为每一个「字符」分配一个唯一的 ID(学名为码位 / 码点 / Code Point)
  • 编码规则:将「码位」转换为字节序列的规则(编码/解码 可以理解为 加密/解密 的过程)

参考:
邱昊宇的回答

总结

  • InputStreamReader,字节流通向字符流的桥梁:它使用指定的charset读取字节并将其解码为字符。
  • 每次调用InputStreamReader中的read方法都会导致从底层输入流读取一个或多个字节,然后调用编码转换器将字节转化为字符。为避免频繁调用转换器(StreamDecoder类内部底层调用),实现从字节到字符的高效转换,可以提前从底层流读取更多的字节。为了达到最高效率,可要考虑在BufferedReader内包装InputStreamReader。
  • InputStreamReader的功能是依赖于StreamDecoder完成的。
  • OutputStreamWriter,字符流通向字节流的桥梁:它使用指定的charset将要写入流中的字符编码成字节。
  • 每次调用write()方法都会导致在给定字符(或字符集)上调用编码转换器(StreamEncoder类内部底层调用)。为避免频繁调用转换器,在写入底层输出流之前,可以将得到的这些字节积累在缓冲区。例如,可考虑将OutputStreamWriter包装到BufferedWriter中。
  • OutputStreamWriter的功能是依赖于StreamEncoder完成的。

FileReader,FileWriter

  • FileReader 是用于读取字符流的类,它继承于InputStreamReader。要读取原始字节流,请考虑使用 FileInputStream。
  • FileWriter 是用于写入字符流的类,它继承于OutputStreamWriter。要写入原始字节流,请考虑使用 FileOutputStream。

需要注意的是该类并非直接继承自Reader,Writer类,而是继承其子类并在其子类基础上进行扩展。

大致看了一下源码,我们可以看出FileReader是基于InputStreamReader实现的。相对的FileWriter是基于OutputStreamWriter实现的。

public class FileWriter extends OutputStreamWriter {

    public FileWriter(String fileName) throws IOException {
        super(new FileOutputStream(fileName));
    }
}

public class FileReader extends InputStreamReader {

    public FileReader(String fileName) throws FileNotFoundException {
        super(new FileInputStream(fileName));
    }
}

都是如此。只是几个构造函数参数不同

演示程序

FileWriter

// 创建文件“file.txt”对应File对象
File file = new File(FileName);
// 创建FileOutputStream对应FileWriter:将字节流转换为字符流,即写入out1的数据会自动由字节转换为字符。
FileWriter out1 = new FileWriter(file);
// 写入10个汉字
out1.write("字节流转为字符流示例");
// 向“文件中”写入"0123456789"+换行符
out1.write("0123456789\n");

FileReader

// 新建文件“file.txt”对应File对象
File file = new File(FileName);
FileReader in1 = new FileReader(file);

// 测试read(),从中读取一个字符
char c1 = (char)in1.read();
System.out.println("c1="+c1);

// 测试skip(long byteCount),跳过4个字符
in1.skip(6);

BufferedReader,BufferedWriter

==注意:== 其实看过其他类的源码后,真正理解了字节,字符输入输出流的作用机制后,就很清楚了,其实BufferedReader比起其他输入流没任何优势,只是他的缓冲池大,所以它缓冲的数据是其他输入流的n倍。也就为其他输入流提供了缓冲作用

BufferedReader

BufferedReader,字符缓冲输入流,作用是为其他输入流提供缓冲功能。BufferedReader从其他字符输入流中读取数据内容,缓冲各个字符,从而实现字符、数组和行的高效读取。当通过其read函数读取不到BufferedRead流中的数据时会调用fill()函数读取源输入流的数据来填充BufferedReader缓冲池的数据。

通常,Reader所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。因此,建议用BufferedReader包装所有其read()操作可能开销很高的Reader(如FileReader和InputStreamReader)。例如,
BufferedReader in = new BufferedReader(new FileReader("foo.in"));将缓冲指定文件的输入。如果没有缓冲,则每次调用read()或readLine()都会导致从文件中读取字节,并将其转换为字符后返回,而这是极其低效的。而BufferedReader就可以一次性帮我们读更多,从而减少了读的次数,也就减少了字符转化的次数。

// BufferedInputStream
private void fill() throws IOException {
    // ...
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    // ...
}

问题:提供缓冲为什么能实现字符、数组和行的高效读取?

  1. 是提高了读取的效率
  2. 是减少了打开存储介质的连接次数。缓冲中的数据实际上是保存在内存中,而原始数据可能是保存在硬盘中。从内存中读取数据的速度比从硬盘读取数据的速度至少快10倍以上。

问题:为什么不一次性将Reader中全部数据都读取到缓冲中呢?

  1. 读取全部的数据所需要的时间可能会很长。
  2. 内存价格很贵,容量远没有硬盘那么大。我们能做的就是在效率和成本之间找到平衡点。大多数情况下,默认值就足够大了。
public class BufferedReader extends Reader {

    // 底层字符输入流,BufferedReader实际操作的字符输入流
    private Reader in;
    // 字符缓冲区
    private char cb[];
    // nChars是cb字符缓冲区中字符的总的个数
    // nextChar是下一个要读取的字符在cb缓冲区中的位置
    private int nChars, nextChar;
    // 表示标记无效。设置了标记,但是被标记位置由于某种原因导致标记无效
    private static final int INVALIDATED = -2;
    //表示没有标记
    private static final int UNMARKED = -1;
    //标记位置初始化为UNMARKED
    private int markedChar = UNMARKED;
    //在仍保留该标记的情况下,对可读取字符数量的限制。
    //在读取达到或超过此限制的字符后,尝试重置流可能会失败。
    //限制值大于输入缓冲区的大小将导致分配一个新缓冲区,其大小不小于该限制值。因此应该小心使用较大的值。
    private int readAheadLimit = 0; /* Valid only when markedChar > 0 */
    //表示是否跳过换行符。(skipLF ,skip line feed)
    private boolean skipLF = false;
    //表示当做了标记时,是否忽略换行符。
    private boolean markedSkipLF = false;
    //字符缓冲区默认大小
    private static int defaultCharBufferSize = 8192;
    //每行默认的字符个数
    private static int defaultExpectedLineLength = 80;

    /**
     * 创建指定底层字符输入流in和指定字符缓冲区大小sz的BufferedReader
     */
    public BufferedReader(Reader in, int sz) {
        super(in);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        this.in = in;
        cb = new char[sz];
        nextChar = nChars = 0;
    }

    /**
     * 创建指定底层字符输入流in和默认字符缓冲区大小defaultCharBufferSize的BufferedReader
     */
    public BufferedReader(Reader in) {
        this(in, defaultCharBufferSize);
    }
}

fill,read

/**
     * 填充缓冲区。
     * 如果标记有效,要考虑标记。
     */
    private void fill() throws IOException {
        //cb中填充数据的起始位置,记录了此次填充时是从缓冲池的什么位置开始填充数据的
        int dst;
        //如果没有标记,从缓冲区索引为0的位置开始填充
        //这个if的处理逻辑是填充缓冲池前的原数据加载行为,
        //即找到缓冲池中原本标记的有效数据,读取readAheadLimit个后,舍弃其他数据。
        if (markedChar <= UNMARKED) {
            dst = 0;
        } else {//如果有标记
            //delta为标记位置与下个读取字符之间的距离
            int delta = nextChar - markedChar;
            //如果delta超出readAheadLimit,标记即为无效。
            if (delta >= readAheadLimit) { 
                markedChar = INVALIDATED;
                readAheadLimit = 0;
                dst = 0;
            } else {
                //如果delta没有超出readAheadLimit,即标记有效
                //且readAheadLimit小于等于缓冲区长度
                //将markedChar与nextChar之间的字符写入到缓冲区中
                if (readAheadLimit <= cb.length) {
                    /* Shuffle in the current buffer */
                    System.arraycopy(cb, markedChar, cb, 0, delta);
                    markedChar = 0;
                    dst = delta;
                } else {
                    //如果delta没有超出readAheadLimit,即标记有效
                    //且readAheadLimit大于缓冲区长度
                    //将重新设置缓冲区大小,markedChar与nextChar之间的字符写入到缓冲区中
                    char ncb[] = new char[readAheadLimit];
                    //这里触发的条件是缓冲区已经标记,且标记后读取的规定的字符数要超过计算的个数(nextChar - markedChar),
                    //且要读取的个数超过了缓冲区总长度。
                    System.arraycopy(cb, markedChar, ncb, 0, delta);
                    cb = ncb;
                    markedChar = 0;
                    dst = delta;
                }
                nextChar = nChars = delta;
           }
    }

    int n;
    //从底层输入流中读取数据,并存储到缓冲区cb中
    //如果没有读取到数据,就继续读(可能有数据为空的情况),直到读到数据或者到达流末尾为止(只要不返回-1,就说明没结束)
    do {
        n = in.read(cb, dst, cb.length - dst);
    } while (n == 0);
    //如果读到了数据
    if (n > 0) {
        nChars = dst + n;
        nextChar = dst;
    }
}

/**
 * 读取单个字符。
 *
 * @return 作为一个整数(其范围从0到65535( 0x00-0xffff))返回,如果已到达流末尾,则返回 -1
 */
public int read() throws IOException {
    synchronized (lock) {
        //确认BufferedReader是否处于开启状态
        ensureOpen();
        //???什么意义?
        for (;;) {
            //如果缓冲区数据已被读完,填充缓冲区。如果填充缓冲区后缓冲区依然是空的,说明已到达流末尾,返回-1。
            if (nextChar >= nChars) {
                fill();
                if (nextChar >= nChars)
                    return -1;
            }
            //如果skipLF为true,说明要跳过换行符
            if (skipLF) {
                //说明只作用一次
                skipLF = false;
                //如果缓冲区内下个字符为换行符,跳过它。
                if (cb[nextChar] == '\n') {
                    nextChar++;
                    continue;
                }
            }
            //返回缓冲区中下个字符,然后nextChar+1
            return cb[nextChar++];
        }
    }
}

/**
 * 从缓冲区中读取数据,写入到cbuf中。off为开始存储字符处的偏移量。len为要读取的最大字符数。
 * 如有必要,从底层输入流中读取数据。
 * 
 * 该方法在read(char cbuf[], int off, int len)中被调用
 * 
 * @param   b     目标字符数组
 * @param   off   开始存储字符处的偏移量
 * @param   len   要读取的最大字符数
 * @return  实际读入cbuf的总字节数,如果由于已到达流末尾而不再有数据,则返回-1。
 */
private int read1(char[] cbuf, int off, int len) throws IOException {
    //如果缓冲区已被读完
    if (nextChar >= nChars) {
        //如果要读取的长度大于等于缓冲区大小,且没有标记,且不跳过换行符
        if (len >= cb.length && markedChar <= UNMARKED && !skipLF) {
            //直接从底层输入流中读取数据到cbuf中,
            //???这里有个搞不懂的地方,那这样的话那么缓冲区的暂存数据还未被读走不是选择性[跳]着读了吗?
            return in.read(cbuf, off, len);
        }
        //填充缓冲区
        fill();
    }
    //如果以上步骤执行完后,缓冲区还是没有可读数据,说明已经到达流末尾,返回-1
    if (nextChar >= nChars) return -1;
    //如果跳过换行符
    if (skipLF) {
        //说明换行符只不被读一次
        skipLF = false;
        //如果下个字符是换行符
        if (cb[nextChar] == '\n') {
            //跳过它
            nextChar++;
            //如果缓冲区已被读完,填充缓冲区
            if (nextChar >= nChars)
                fill();
            //如果填充缓冲区后,缓冲区依然是空的。说明已经到达流末尾,返回-1
            if (nextChar >= nChars)
                return -1;
        }
    }
    // 如果缓冲池中还有数据的话也就不填了,该次读取仅读有的这些
    // 所以说,如果一个数组过来读没读满的话,不能说明原数据没数据了。
    int n = Math.min(len, nChars - nextChar);
    //读取
    System.arraycopy(cb, nextChar, cbuf, off, n);
    //缓冲区当前位置+n
    nextChar += n;
    return n;
}

==注意== : 这里带上read1函数的一个主要原因是源码中的设计有个地方不太明白。注释中已经说明

其他地方无论readLine读行还是skip跳字符等,都和之前研究过的BufferedInputStream一样,不一样的只是多了关于换行符的判断。

BufferedWriter

BufferedWriter,字符缓冲输出流,作用是为其他输出流提供缓冲功能。BufferedWriter将文本写入其他字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。

通常Writer将其输出立即发送到底层字符或字节流。除非要求提示输出,否则建议用BufferedWriter包装所有其write()操作可能开销很高的 Writer(如FileWriters和OutputStreamWriters)。例如,PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out"))); 将缓冲PrintWriter对文件的输出。如果没有缓冲,则每次调用print()方法会导致将字符转换为字节,然后立即写入到文件,而这是极其低效的。

// BufferedOutputStream
private void flushBuffer() throws IOException {
    if (count > 0) {
        out.write(buf, 0, count);// 一次性向OutPutStream输出更多数据
        count = 0;
    }
}

BufferedWriter是如何为其他输出流提供缓冲功能的

创建BufferedWriter时,我们会通过它的构造函数指定某个底层字符输出流Writer为参数。当程序中每次将单个字符、数组和字符串写入到BufferedWriter中时、都会检查BufferedWriter中的缓存区是否存满,如果没有存满则将字符写入到缓存区中;如果存满,则调用底层的writer(char[] b, int off, int len)将缓存区中的所有字符一次性写入到底层Writer中。

提供缓冲为什么能实现单个字符、数组和字符串的高效写入

如果没有缓冲,使用底层字符输出流向目的文件中写入单个字符、数组和字符串时,每写入一次就要打开一次到目的文件的连接。这样频繁的访问效率非常底下。比如,将一个非常大的数据写入到目的文件中,如果每写入一个字符就要打开一次到目的文件的连接,可以想象效率是如何低下。

有了缓冲,当使用底层字符输出流向目的文件中写入单个字符、数组和字符串时,将单个字符、数组和字符串先写入到BufferedWriter的内置缓存空间中,然后当达到一定数量时一次性写入Writer流中。此时,Writer就可以打开一次通道,将这个数据块写入到文件中。这样虽然不能达到一次访问就将所有数据写入磁盘中的效果,但也大大提高了效率和减少了对磁盘的访问量。

write,newLine

/**
 * 写入字符数组的某一部分。
 * 将字符数组的cbuf中从下标off开始的len个字符写入缓冲区cb中
 */
public void write(char cbuf[], int off, int len) throws IOException {
    synchronized (lock) {
        ensureOpen();
        //检查参数是否合法
        if ((off < 0) || (off > cbuf.length) || (len < 0) ||
            ((off + len) > cbuf.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }

        //如果cbuf的数据的长度大于等于缓冲区长度
        if (len >= nChars) {
            //刷新缓冲区,将要写入的数据直接写入到底层输出流中
            flushBuffer();
            //Writer抽象类的不同子类实现不同,因为该函数在Writer抽象类中声明的是抽象方法,也就是准备给子类各自去不同实现的
            //CharArrayWriter的实现方式是如果缓冲池大小小于len则扩容成(缓冲池大小1倍,len大小)两者较大的量。
            //PipedWriter的实现方式是迭代len次的调用write(int a),逐个写入
            out.write(cbuf, off, len);
            return;
        }

        //如果cbuf的数据的长度小于缓冲区长度,将数据写入缓冲区中。
        int b = off, t = off + len;
        while (b < t) {
            int d = min(nChars - nextChar, t - b);
            System.arraycopy(cbuf, b, cb, nextChar, d);
            b += d;
            nextChar += d;
            //如果写入过程中缓冲区满了,刷新缓冲区,继续将数据写入缓冲区。
            if (nextChar >= nChars)
                flushBuffer();
        }
    }
    
    /**
     * 写入一个行分隔符。
     * 行分隔符字符串由系统属性line.separator定义,并且不一定是单个新行('\n')符。
     */
    public void newLine() throws IOException {
        // 这里写入一个行分隔符,所以当你再写数据的时候,如果操作的是文件
        // 新数据就会出现在文件另一行。
        write(lineSeparator);
    }

}

这里写上write函数的原因是方法中out.write(cbuf, off, len);这句曾困扰我一会儿,突然想不通当len >= nChars时具体他会怎么操作了。

后来想明白了,还操作个啥,BufferedWriter才不会管,让实际操作的***Writer输入流自己去处理去,因为本身该方法在抽象父类Writer中就是抽象函数,也就是任意Writer的非抽象子类都必须重写。所以它根本必须要管,也没权利管。

总结

  • BufferedReader,字符缓冲输入流,作用是为其他输入流提供缓冲功能。BufferedReader从其他字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
  • BufferedReader支持标记功能。
  • BufferedWriter,字符缓冲输出流,作用是为其他输出流提供缓冲功能。BufferedWriter将文本写入其他字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。

PrintWriter

PrintWriter 是字符类型的打印输出流,它继承于Writer。

PrintStream 用于向文本输出流打印对象的格式化表示形式。它实现在 PrintStream 中的所有 print 方法。它不包含用于写入原始字节的方法,对于这些字节,程序应该使用未编码的字节流进行写入。

==需要注意的是==,该类和其对应的字节输出流的自动刷新功能不同,它只有在调用printf,及println函数的时候才会自动刷新。这一点下面的源码注释中我有写。

PrintWriter

public class PrintWriter extends Writer {

    /**
     * PrintWriter的底层字符输出流
     */
    protected Writer out;
    //是否自动刷新。
    //如果为true,每次执行printf()[format函数的原因], 
    // println()[newLine函数的原因]函数,都会调用flush()函数。
    private final boolean autoFlush;
    //是否有异常
    //当PrintWriter有异常产生时,会被本身捕获,并设置trouble为true
    private boolean trouble = false;
    //用于格式化字符串的对象
    private Formatter formatter;
    //字节打印流
    //用于checkError方法
    private PrintStream psOut = null;

    /**
     * 行分隔符
     * 在PrintWriter被创建时line.separator属性的值。
     */
    private final String lineSeparator;

    /**
     * 返回csn(字符集名字)对应的Chaset
     * csn为null或是不支持的字符集,抛出异常
     */
    private static Charset toCharset(String csn)
        throws UnsupportedEncodingException
    {
        Objects.requireNonNull(csn, "charsetName");
        try {
            return Charset.forName(csn);
        } catch (IllegalCharsetNameException|UnsupportedCharsetException unused) {
            // UnsupportedEncodingException should be thrown
            throw new UnsupportedEncodingException(csn);
        }
    }

    /**
     * 创建新的PrintWriter。
     * 指定底层输出流,默认不会自动flush,采用默认字符集
     */
    public PrintWriter (Writer out) {
        this(out, false);
    }

    /**
     * 创建新的PrintWriter。
     * 指定底层输出流,指定是否自动flush,采用默认字符集
     */
    public PrintWriter(Writer out,
                       boolean autoFlush) {
        super(out);
        this.out = out;
        this.autoFlush = autoFlush;
        //line.separator属性的值
        lineSeparator = java.security.AccessController.doPrivileged(
            new sun.security.action.GetPropertyAction("line.separator"));
    }

    /**
     * 创建新的PrintWriter。
     * 指定底层输出流,不自动flush,采用默认字符集
     */
    public PrintWriter(OutputStream out) {
        this(out, false);
    }

    /**
     * 创建新的PrintWriter。
     * 指定底层输出流,指定是否自动flush,采用默认字符集
     */
    public PrintWriter(OutputStream out, boolean autoFlush) {
        this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);

        // save print stream for error propagation
        if (out instanceof java.io.PrintStream) {
            psOut = (PrintStream) out;
        }
    }

    /**
     * 创建新的PrintWriter。
     * 指定文件名,默认不自动flush,采用默认字符集
     */
    public PrintWriter(String fileName) throws FileNotFoundException {
        this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))),
             false);
    }

    /**
     * 私有构造方法。创建新的PrintWriter。
     * 指定文件名,默认不自动flush,采用指定字符集
     */
    private PrintWriter(Charset charset, File file)
        throws FileNotFoundException
    {
        this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), charset)),
             false);
    }

    /**
     * 创建新的PrintWriter。
     * 指定文件名,默认不自动flush,采用指定字符集
     */
    public PrintWriter(String fileName, String csn)
        throws FileNotFoundException, UnsupportedEncodingException
    {
        this(toCharset(csn), new File(fileName));
    }

    /**
     * 创建新的PrintWriter。
     * 指定文件名,默认不自动flush,采用默认字符集
     */
    public PrintWriter(File file) throws FileNotFoundException {
        this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))),
             false);
    }

    /**
     * 创建新的PrintWriter。
     * 指定文件名,默认不自动flush,采用指定字符集
     */
    public PrintWriter(File file, String csn)
        throws FileNotFoundException, UnsupportedEncodingException
    {
        this(toCharset(csn), file);
    }
}

光构造函数就有9个,其实底层用的就只有一个。

public PrintWriter(Writer out,boolean autoFlush) {
    super(out);
    this.out = out;
    this.autoFlush = autoFlush;
    lineSeparator = java.security.AccessController.doPrivileged(
        new sun.security.action.GetPropertyAction("line.separator"));
}

checkError,setError,clearError

该类除构造函数会抛出异常外,其余所有的函数调用均不会抛出异常。在调用时如果抛出异常会被catch掉,然后设置trouble变量为true,并不会显示抛出。

/**
 * 如果流没有关闭,则刷新流且检查其错误状态。
 */
public boolean checkError() {
    //如果流没有关闭,则刷新流
    if (out != null) {
        flush();
    }
    //检查错误状态
    if (out instanceof java.io.PrintWriter) {
        PrintWriter pw = (PrintWriter) out;
        return pw.checkError();
    } else if (psOut != null) {
        return psOut.checkError();
    }
    //如果抛出了异常,返回true
    return trouble;
}

/**
 * 指示已发生错误。
 * 在调用clearError()之前,此方法将导致checkError()的后续调用返回 true。
 */
protected void setError() {
    trouble = true;
}

/**
 * 清除此流的错误状态。
 */
protected void clearError() {
    trouble = false;
}

write

/**
 * 写入字符数组。
 * 此方法不能从Writer类继承,因为它必须取消I/O异常。
 */
public void write(char buf[]) {
    write(buf, 0, buf.length);
}

很多时候我们不得不重新自己定义一些函数。因为我们有些原因不能重写父类的函数

其实大多数函数都是直接底层调用,除了会有字符输入输出流特有的安全检查机制ensureOpen对于底层实际操作的输入输出流的null判断外。大部分都是直接调用,然后区别只是catch到异常的时候不再往外抛,而是设置trouble变量为true。如下:

/**
 * 写入字符串的某一部分。
 */
public void write(String s, int off, int len) {
    try {
        synchronized (lock) {
            ensureOpen();
            out.write(s, off, len);
        }
    }
    catch (InterruptedIOException x) {
        Thread.currentThread().interrupt();
    }
    catch (IOException x) {
        trouble = true;
    }
}

printf

// 其实底层调用的就是Formatter类的format方法
public PrintWriter printf(String format, Object ... args) {
    return format(format, args);
}

总结

  • 从构造方法中可以看到,BufferedWriter包装了底层输出流,为其提供了缓冲功能。
  • 此类中的方法不会抛出I/O异常,尽管其某些构造方法可能抛出异常。客户端可能会查询调用checkError()是否出现错误。
  • print方法可以打印boolean、char 、char[]、double 、float、int、 long、Object、String这些类型。都是按照平台的默认字符串编码将String.valueOf() 方法生成的字符串转换为字节,并完全以write(int)方法的方式向输出流中写入这些字节。
  • println(type param)方法可以打印boolean、char 、char[]、double 、float、int、 long、Object、String这些类型。都是先调用print方法打印,再调用println()方法换行。
  • printf方法和format方法的效果是相同的。因为printf方法是依赖于调用format方法实现的。
  • append方法其实是依赖于out.write方法实现的。

RandomAccessFile

  • RandomAccessFile 是随机访问文件 (包括读/写)的类。它支持对文件随机访问的读取和写入,即我们可以从指定的位置读取/写入文件数据。
  • 需要注意的是,RandomAccessFile 虽然属于java.io包,但它不是InputStream或者OutputStream的子类;它也不同于FileInputStream和FileOutputStream。 FileInputStream 只能对文件进行读操作,而FileOutputStream 只能对文件进行写操作;但是,RandomAccessFile 同时支持文件的读和写,并且它支持随机访问。

RandomAccessFile 模式说明

RandomAccessFile共有4种模式:"r", "rw", "rws"和"rwd"。

"r"    以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。  
"rw"   打开以便读取和写入。
"rws"  打开以便读取和写入。相对于 "rw","rws" 还要求对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。 这里的s是synchronized的意思
"rwd"  打开以便读取和写入,相对于 "rw","rwd" 还要求对“文件的内容”的每个更新都同步写入到基础存储设备。  和rws的区别是不包含原数据

==说明:==

  1. 什么是“元数据”,即metadata?

metadata是“关于数据的数据”。在文件系统中,数据被包含在文件和文件夹中;metadata信息包括:“数据是一个文件,一个目录还是一个链接”,“数据的创建时间(简称ctime)”,“最后一次修改时间(简称mtime)”,“数据拥有者”,“数据拥有群组”,“访问权限”等等。也就是数据的修饰信息,状态信息。而非具体的数据内容

  1. "rw", "rws", "rwd" 的区别?
  • 当操作的文件是存储在本地的基础存储设备上时(如硬盘, NandFlash等),"rws" 或 "rwd", "rw" 才有区别。
  • 当模式是 "rws" 并且 操作的是基础存储设备上的文件;那么,每次“更改文件内容[如write()写入数据]” 或 “修改文件元数据(如文件的mtime)”时,都会将这些改变同步到基础存储设备上
  • 当模式是 "rwd" 并且 操作的是基础存储设备上的文件;那么,每次“更改文件内容[如write()写入数据]”时,都会将这些改变同步到基础存储设备上。
  • 当模式是 "rw" 并且 操作的是基础存储设备上的文件;那么,关闭文件时,会将“文件内容的修改”同步到基础存储设备上。至于,“更改文件内容”时,是否会立即同步,取决于系统底层实现。

作者的演示程序

还是很好理解的。

/**
* RandomAccessFile 测试程序
*
* 运行结果(输出如下):
* c1=a
* c2=b
* buf=9876543210
* 
* 此外,
* (01) 在源文件所在目录生成了file.txt。
* (02) 注意RandomAccessFile写入boolean, byte, char, int,所占的字符个数。
*
* @author skywang
*/
public class RandomAccessFileTest {

   private static final String FileName = "file.txt";

   public static void main(String[] args) {
       // 若文件“file.txt”存在,则删除该文件。
       File file = new File(FileName);
       if (file.exists())
           file.delete();

       testCreateWrite();
       testAppendWrite();
       testRead();
   }

   /**
    * 若“file.txt”不存在的话,则新建文件,并向文件中写入内容
    */
   private static void testCreateWrite() {
       try {
           // 创建文件“file.txt”对应File对象
           File file = new File(FileName);
           // 创建文件“file.txt”对应的RandomAccessFile对象
           RandomAccessFile raf = new RandomAccessFile(file, "rw");

           // 向“文件中”写入26个字母+回车
           raf.writeChars("abcdefghijklmnopqrstuvwxyz\n");
           // 向“文件中”写入"9876543210"+回车
           raf.writeChars("9876543210\n");

           raf.close();
       } catch(IOException e) {
           e.printStackTrace();
       }
   }

   /**
    * 向文件末尾追加内容
    */
   private static void testAppendWrite() {
       try {
           // 创建文件“file.txt”对应File对象
           File file = new File(FileName);
           // 创建文件“file.txt”对应的RandomAccessFile对象
           RandomAccessFile raf = new RandomAccessFile(file, "rw");

           // 获取文件长度
           long fileLen = raf.length();
           // 将位置定位到“文件末尾”
           raf.seek(fileLen);

           // 以下向raf文件中写数据  
           raf.writeBoolean(true); // 占1个字节  
           raf.writeByte(0x41);    // 占1个字节  
           raf.writeChar('a');     // 占2个字节  
           raf.writeShort(0x3c3c); // 占2个字节  
           raf.writeInt(0x75);     // 占4个字节  
           raf.writeLong(0x1234567890123456L); // 占8个字节  
           raf.writeFloat(4.7f);  // 占4个字节  
           raf.writeDouble(8.256);// 占8个字节  
           raf.writeUTF("UTF严"); // UTF-8格式写入
           raf.writeChar('\n');   // 占2个字符。“换行符”

           raf.close();
       } catch(IOException e) {
           e.printStackTrace();
       }
   }

   /**
    * 通过RandomAccessFile读取文件
    */
   private static void testRead() {
       try {
           // 创建文件“file.txt”对应File对象
           File file = new File(FileName);
           // 创建文件“file.txt”对应的RandomAccessFile对象,以只读方式打开
           RandomAccessFile raf = new RandomAccessFile(file, "r");

           // 读取一个字符
           char c1 = raf.readChar();
           System.out.println("c1="+c1);
           // 读取一个字符
           char c2 = raf.readChar();
           System.out.println("c2="+c2);

           // 跳过54个字节。
           raf.seek(54);

           // 测试read(byte[] buffer, int byteOffset, int byteCount)
           byte[] buf = new byte[20];
           raf.read(buf, 0, buf.length);
           System.out.println("buf="+(new String(buf)));

           raf.close();
       } catch(IOException e) {
           e.printStackTrace();
       }
   }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容