[重学Java基础][Java IO流][Part.2]缓冲字符输入输出流

[重学Java基础][JavaIO流][Part.2]缓冲字符输入输出流

BufferedReader

概述

BufferedReader缓冲字符数组输入流,继承了所有字符输入流的超类Reader类,自带缓存区,可以一次读入缓冲区大小的字符内容,并且提供了按行读取功能
通过包装Reader对象来发挥作用
很明显 这是一个处理流 包装流 一般包装InputStreamReader,FileReader对象

官方注释

从一个输入的字符流中读取文本,为字符、数组、一行文本的高效读取提供字符缓冲功能。

缓冲区的大小可能是特殊设定值,可能是使用默认的大小。默认的大小已经足够解决大部分问题。

一般情况下,每一个read请求由一个Reader类发起,使底层进行对字节流或byte流执行相应的读取请求。

对一些read()函数开销较大的的Reader类,(例如:FileReaders和InputStreamReaders)使用BufferedReader类进行封装是很明智的。

例:

 BufferedReader in  = new BufferedReader(new FileReader("file.in"));

如果没由进行缓冲,对read()和readline()调用会直接从文件中读取一次byte数据,转换成字符格式并返回,这是十分不明智的做法。

程序对文本数据使用DataInputStream时,在合适的情况下可以局部替换DataInputStream为BufferedReader

源码分析

成员属性

public class BufferedReader extends Reader {

读取的数据源  
private Reader in;
缓冲字符数组 流的实际内容体
private char cb[];

nChars 缓冲区中字符总个数
nextChar 下一个要读取的字符位置
private int nChars, nextChar;

“标记无效”的标志 
private static final int INVALIDATED = -2;
未设置标记
private static final int UNMARKED = -1;
默认情况下无标记字符 所以markedChar是未设置标记
private int markedChar = UNMARKED;
可标记位置能标记的最大长度 只有先设置了markedChar后此变量才能生效
private int readAheadLimit = 0; 

是否跳过换行字符 就是是否忽略换行 默认不忽略
private boolean skipLF = false;

设置标记时 是否忽略换行
private boolean markedSkipLF = false;

默认缓冲字符数组大小
private static int defaultCharBufferSize = 8192;
默认每一行的字符个数
private static int defaultExpectedLineLength = 80;
……
}

成员方法

这里写图片描述

  • 构造方法

接受一个输入流对象 并按照入参sz的大小创建字符缓冲区

 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;
        }

接受一个输入流大小 并创建默认大小的字符缓冲区

    public BufferedReader(Reader in) {
            this(in, defaultCharBufferSize);
        }

填入缓冲区方法 真正将数据填入缓冲区的方法 其他读取方法都是此方法的装饰器
缓冲区没有数据时,通过fill()法以向缓冲区填充数据 缓冲区数据被读完,需更新时,通过fill()可以更新缓冲区的数据

private void fill() throws IOException {
    缓冲数组填充的开始位置
    int dst;
    if (markedChar <= UNMARKED) {
        无标记的情况下 默认初始位置为缓冲区头部位置0
        dst = 0;
    } else {
        如果有标记 则标记区间长度delta 等于下一个要读取的位置与被标记的位置的距离
        int delta = nextChar - markedChar;

        if (delta >= readAheadLimit) {
             若标记区间长度delta大于读取的限制(超过了标记上限,
             即流下次读取的位置已经超过了标记位置) 则标记无效
            markedChar = INVALIDATED;
            readAheadLimit = 0;
            dst = 0;
        } else {
            if (readAheadLimit <= cb.length) {
                若未超过标记上限,则将下一个读取位置到标记位置开始的流内容复制到cb
                System.arraycopy(cb, markedChar, cb, 0, delta);
                markedChar = 0;
                dst = delta;
            } else {
                如果标记上限大于缓冲流的总长度 则将下一个读取位置到标记位置开始的                    
                流内容复制到新的字符数组ncb中 再将cb设置为ncb
                char ncb[] = new char[readAheadLimit];
                System.arraycopy(cb, markedChar, ncb, 0, delta);
                cb = ncb;
                markedChar = 0;
                dst = delta;
            }
            nextChar = nChars = delta;
        }
    }

    int n;
    do {
    从输入流in中读取数据 并存储到cb中 读取长度为cb.length-dst
    如果没读到就继续读取

        n = in.read(cb, dst, cb.length - dst);
    } while (n == 0);
    if (n > 0) {
     如果从输入流in中读到了数据,则设置nChars(cb中字符的数目)=dst+n,
     并且nextChar(下一个被读取的字符的位置)=dst。
        nChars = dst + n;
        nextChar = dst;
    }
}

从数据源读取数据方法 每次读入一个字符

 public int read() throws IOException {
        线程加锁 说明读取时线程安全的
        synchronized (lock) {
            ensureOpen();
            for (;;) {
            若下一个读取的数据位置大于缓冲区的大小(缓冲区数据全部读取)
            则先用fill()按方法刷新缓冲区 再读取
                if (nextChar >= nChars) {
                    fill();
                    刷新后缓冲区仍然没有新数据 则返回-1 读入结束
                    if (nextChar >= nChars)
                        return -1;
                }
                是否忽略换行符
                if (skipLF) {
                    skipLF = false;
                    if (cb[nextChar] == '\n') {
                        nextChar++;
                        continue;
                    }
                }
                return cb[nextChar++];
            }
        }
    }

从数据源读取数据方法 每次读入指定长度字符内容

private int read1(char[] cbuf, int off, int len) throws IOException {
    if (nextChar >= nChars) {
    如果读取的字符游标位置超过了当前的缓冲区大小
    且读取的长度小于缓冲区大小 未设置标记 不忽略换行符
    则直接从输入流读取 不经过缓冲区
        if (len >= cb.length && markedChar <= UNMARKED && !skipLF) {
            return in.read(cbuf, off, len);
        }
        fill();
    }

    刷新缓冲区之后 读取的字符游标位置仍然超过了当前的缓冲区大小
    则直接读入停止
    if (nextChar >= nChars) return -1;
    如果忽略换行符
    if (skipLF) {
        重置标记
        skipLF = false;
        下个读入字符为换行符 跳过
        if (cb[nextChar] == '\n') { 
            nextChar++;
            超过缓冲区位置 刷新缓冲区
            if (nextChar >= nChars)
                fill();
            仍然超过则停止读入
            if (nextChar >= nChars)
                return -1;
        }
    }
    int n = Math.min(len, nChars - nextChar);
    System.arraycopy(cb, nextChar, cbuf, off, n);
    nextChar += n;
    return n;
}

从数据源读取数据方法 每次读入一行内容
readLine()方法实际上调用了一个包内部方法 readLine(boolean ignoreLF)

 public String readLine() throws IOException {
        return readLine(false);
    }


String readLine(boolean ignoreLF) throws IOException {
    StringBuffer s = null;
    int startChar;

    synchronized (lock) {
        ensureOpen();
        确认这一行读取是否需要读取换行符
        boolean omitLF = ignoreLF || skipLF;

    bufferLoop:
        for (;;) {

            if (nextChar >= nChars)
                fill();
            下个读入字符游标位置超过缓冲区大小 读入结束
            if (nextChar >= nChars) { 
                if (s != null && s.length() > 0)
                    return s.toString();
                else
                    return null;
            }
            boolean eol = false;
            char c = 0;
            int i;

            是否跳过一个换行符
            if (omitLF && (cb[nextChar] == '\n'))
                nextChar++;
            skipLF = false;
            omitLF = false;

        可以看到读取一行方法 实际上也是判断字符是否是换行符或者回车符 
        如果是 停止读入 把结束标志置位真
        charLoop:
            for (i = nextChar; i < nChars; i++) {
                c = cb[i];
                if ((c == '\n') || (c == '\r')) {
                    eol = true;
                    break charLoop;
                }
            }

            startChar = nextChar;
            nextChar = i;
            如果结束符为真 则返回读入内容字符串
            if (eol) {
                String str;
                if (s == null) {
                    str = new String(cb, startChar, i - startChar);
                } else {
                    s.append(cb, startChar, i - startChar);
                    str = s.toString();
                }
                nextChar++;
                if (c == '\r') {
                    skipLF = true;
                }
                return str;
            }

            if (s == null)
                s = new StringBuffer(defaultExpectedLineLength);
            s.append(cb, startChar, i - startChar);
        }
    }
}

代码示例

例1 按行读入文件中字符 注意文件必须是UTF-8格式的 否则会乱码

    BufferedReader br = new 
         BufferedReader(new FileReader("d:/a.txt"));


    String str;
    while((str=br.readLine())!=null)
    {
        System.out.println(str);
    }

输出

第一行 四月是你的谎言
第二行 比宇宙更远的地方
第三行 不死者之王
第四行 关于我的女友是个一本正经的碧池这件事

例2 按字符读入并转换

 BufferedReader br = new BufferedReader(new FileReader("d:/a.txt"));
 int c;
        while((c=br.read())!=-1)
        {
            System.out.print((char)c);
        }

结果同上

例3 标记并重置

    BufferedReader br = new BufferedReader(new FileReader("d:/a.txt"));
注意这里的读入限制为大于读入字符数 读入字符数为55 小于等于这个值reset时会报错
    br.mark(100);
    String str;
    while((str=br.readLine())!=null)
    {
        System.out.println(str);
    }

    br.reset();
    int c;
    while((c=br.read())!=-1)
    {
        System.out.print((char)c);
    }

输出

第一行 四月是你的谎言
第二行 比宇宙更远的地方
第三行 不死者之王
第四行 关于我的女友是个一本正经的碧池这件事
第一行 四月是你的谎言
第二行 比宇宙更远的地方
第三行 不死者之王
第四行 关于我的女友是个一本正经的碧池这件事

典型错误

1.

        BufferedReader br = new BufferedReader(new FileReader("d:/a.txt"));

    String str;
    while((str=br.readLine())!=null)
    {
        System.out.println(br.readLine());
    }

输出

    第二行 比宇宙更远的地方
    第四行 关于我的女友是个一本正经的碧池这件事

隔一行丢失了一行数据 因为流被读取后游标移动 不会重复读入 所以只能按例1方式操作

BuffererWriter

这里写图片描述

概述

BufferedWriter缓冲字符数组输出流,继承了所有字符输出流的超类Writer类,自带缓存区,可以一次写出缓冲区大小的字符内容,并且提供了按行写出功能
通过包装Writer对象来发挥作用
很明显 这是一个处理流 包装流 一般包装InputStreamWriter,FileWriter对象

官方注释

向字符输出流写入文本时,对字符进行缓冲处理是一种高效的处理方式,尤其是逐个的字符写入,数组结构写入或者字符串结构写入

缓冲区的大小可被设定,默认大小足以满足一般情况的需求

提供了newLine()方法,提供了基于不同平台的行分割方法,因为不是所有平台都是用’\n’作为换行符,所以使用newLine方法是首选。

一般情况下,Writer类总是立即将数据输出到底层的字符流或比特流中。除非程序需要即刻输出,否则使用BufferedWriter封装时最好的选择。(对于write()方法开销较大的Writer类十分必要)

例如:

  PrintWriter out  = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));

对PrintWriter类进行了缓冲。如果没有缓冲,任意一个对print方法的调用将会使字符转化为byte并且立刻写入到文件中,这是非常低效率的。

源码分析

成员属性

    输出的数据汇
    private Writer out;
    缓冲字符数组
    private char cb[];
    下个读入字符游标位置nextChar 缓冲区最大长度nChars
    private int nChars, nextChar;
    默认的缓冲区大小
    private static int defaultCharBufferSize = 8192;
    文本换行符 流创建的时刻会自动赋值 
    private String lineSeparator;

成员方法

Alt text

构造方法 创建一个默认缓冲区大小的缓冲输出流或者指定大小的缓冲输出流

public BufferedWriter(Writer out) {
        this(out, defaultCharBufferSize);
    }


public BufferedWriter(Writer out, int sz) {
    super(out);
    if (sz <= 0)
        throw new IllegalArgumentException("Buffer size <= 0");
    this.out = out;
    cb = new char[sz];
    nChars = sz;
    nextChar = 0;
    可以看到lineSeparator 文本换行符是通过底层方法创建的 
    按当前系统的方式创建分本换行符
    lineSeparator = java.security.AccessController.doPrivileged(
        new sun.security.action.GetPropertyAction("line.separator"));
}

刷新输出流 可以看到刷新方法flush()内部调用了包级方法flushBuffer()方法
flushBuffer()方法则真正实现了将缓冲区字符数组写入到输出流的操作

 public void flush() throws IOException {
        synchronized (lock) {
            flushBuffer();
            out.flush();
        }
    }

  void flushBuffer() throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (nextChar == 0)
                return;
            out.write(cb, 0, nextChar);
            nextChar = 0;
        }
    }

换行方法 写入一个换行符 避免显示输出换行符可能引起的一些问题

  public void newLine() throws IOException {
        write(lineSeparator);
    }

写入方法 读入字符数组缓冲内容 并写入到底层流

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;
            }

        如果写入的字符数组大于缓冲区大小 则刷新缓冲区 并直接写入下层流
        可以看到是直接调用底层流out的写入方法
        if (len >= nChars) {
            flushBuffer();
            out.write(cbuf, off, len);
            return;
        }

        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();
        }
    }
}

代码示例

向控制台写入字符

    BufferedReader br = new BufferedReader(new FileReader("d:/b.txt"));
    BufferedWriter bwo=new BufferedWriter(new PrintWriter(System.out));

    String str;
    while((str=br.readLine())!=null)
    {
        bwo.write(str);
        bwo.newLine();
    }
    bwo.flush();

输出结果

1.紫罗兰永恒花园
2.龙王的工作!
3.Fate/EXTRA Last Encore
4.citrus~柑橘味香气~

写入到文件

BufferedWriter bw=new BufferedWriter(new FileWriter("d://c.txt"));

List<String> list= Arrays.asList("比宇宙更远的地方","魔卡少女樱 CLEAR CARD篇","卫宫家今天的饭","博多豚骨拉面团");

    for (String s : list) {
        bw.write(s,0,s.length());
        bw.newLine();
    }

    bw.flush();

输出结果

比宇宙更远的地方
魔卡少女樱 CLEAR CARD篇
卫宫家今天的饭
博多豚骨拉面团

注意 输出到文件 重复执行会覆盖上一次执行的内容

猜你喜欢

转载自blog.csdn.net/u011863951/article/details/80003970