欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-06 16:17:56
...

[重学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;
……
}

成员方法

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

  • 构造方法

接受一个输入流对象 并按照入参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

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

概述

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;

成员方法

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

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

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篇
卫宫家今天的饭
博多豚骨拉面团

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