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

字符流(二)BufferedReader和BufferedWriter源码理解

程序员文章站 2022-04-30 15:25:04
...

1.BufferedReader

1.1 继承关系

public class BufferedReader extends Reader {
//这个又是装饰模式
private Reader in;
}

1.2 构造方法

public BufferedReader(Reader in) {
	this(in, defaultCharBufferSize);
}
//默认缓存数组的大小
private static int defaultCharBufferSize = 8192;
//构造方法
public BufferedReader(Reader in, int sz) {
        //这个方法可参考前面的Writer源码,只要是将锁赋值
	super(in);
	if (sz <= 0)
	    throw new IllegalArgumentException("Buffer size <= 0");
	//装饰模式。。
	this.in = in;
	cb = new char[sz];
	nextChar = nChars = 0;
}
//两个上面用到的参数,用于缓存数据,是字符(char)数组,不是字节(byte)数组。
private char cb[];
private int nChars, nextChar;

1.3 标记有关

在看read方法之前先看一眼 标记mark有关的方法有点帮助。为看懂read做铺垫

//标记流中的当前位置,带入的参数表示标记所占的空间
public void mark(int readAheadLimit) throws IOException {
	if (readAheadLimit < 0) {
	    throw new IllegalArgumentException("Read-ahead limit < 0");
	}
	synchronized (lock) {
	    ensureOpen();
	    this.readAheadLimit = readAheadLimit;
	    markedChar = nextChar;
	    markedSkipLF = skipLF;
	}
}
//回到标记位置
public void reset() throws IOException {
	synchronized (lock) {
	    ensureOpen();
	    if (markedChar < 0)
		throw new IOException((markedChar == INVALIDATED)
				      ? "Mark invalid"
				      : "Stream not marked");
            //下面两个参数在读方法中会有详细解释
	    nextChar = markedChar;
	    skipLF = markedSkipLF;
	}
}

 

1.4 read

这个方法中fill()是重点,有点绕,但看懂后就觉得很清晰,能完全理解bufferedReader的原理。

看完这个方法再回去看3.3的标记部分,就很容易看懂。

public int read() throws IOException {
        //锁,看来读得时候也只能一个方法读。
	synchronized (lock) {
	    //确保输入流不是空。
	    ensureOpen();
	    //这个循环和while一样。
	    for (;;) {
                //下面的判断为是否下一个读取的字符超出了缓存数组中实际包含数据的大小。
		if (nextChar >= nChars) {
                    //下一个字符超出或者等于缓存数组的大小
                    //这个是核心的方法,里面有标记的内容,详细的看下面内容。
		    fill();
                    //如果还是超出,则表示输入流读完了。
		    if (nextChar >= nChars)
			return -1;
		}
                //如果下一个字符是换行符.这个变量只有在readLine里面才变为true。和\n\r有关,可忽略。针对不同的平台的
		if (skipLF) {
		    skipLF = false;
		    if (cb[nextChar] == '\n') {
			nextChar++;
			continue;
		    }
		}
                //返回当前读的字符,并将要读字符+1
		return cb[nextChar++];
	    }
	}
}

//下面的变量是用于fill方法里的
//下面两个变量是标记的状态, -1为未启动标记,-2为标记失效。
private static final int INVALIDATED = -2;
private static final int UNMARKED = -1;
//标记的位置
private int markedChar = UNMARKED;
//nChars表示现在缓存数组中已经存在多少个字符。
//nextChar表示下一个读取的位置,从0开始,这个只是缓存数组中的位置,并不是读取流的位置。
private int nChars, nextChar;
//标记分配的空间大小。超出后,如果缓存数组重新处置,则标记失效。
private int readAheadLimit = 0;

//将字符数组读满,然后直接返回数组中的某个值。里面主要考虑的是标记的问题。
//这个和BufferedInputStream差不多,一个是byte[],这个是char[]
private void fill() throws IOException {
        //计算这次缓存数据的起始位置,起始位置之前保存的是标记的内容。
	int dst;
	if (markedChar <= UNMARKED) {
	    //这里表示没有使用标记,或者标记失效。
	    dst = 0;
	} else {
	    //表示使用标记
	    //这个变量表示标记之后实际使用了多少空间
	    int delta = nextChar - markedChar;
	    if (delta >= readAheadLimit) {
	        //如果超过了标记初始的空间。
                //标记失效
                markedChar = INVALIDATED;
                //标记空间赋0
		readAheadLimit = 0;
		//缓存数据起点0
		dst = 0;
	    } else {
	        //如果未超过标记初始的空间。
	        if (readAheadLimit <= cb.length) {
                    //分配的标记空间小于缓存数组的长度
		    //将标记后实际使用长度复制到数组的开始。
		    System.arraycopy(cb, markedChar, cb, 0, delta);
		    //将标记的位置赋0,标记所占空间仍然是原来的空间,不会缩小。
		    markedChar = 0;
		    //数据缓存的起点
		    dst = delta;
		} else {
		    //长度不够,新建一个。
		    char ncb[] = new char[readAheadLimit];
		    //和上面一样,复制标记到最前面
		    System.arraycopy(cb, markedChar, ncb, 0, delta);
		    //将引用更新
		    cb = ncb;
		    markedChar = 0;
		    dst = delta;
		}
                nextChar = nChars = delta;
	    }
	}
	//下面是读数据,读出一定长度,默认cb的长度8192,cb在BufferedReader中是唯一的缓存数组。
	int n;
	//这个地方读的方法中不可能返回0.所以只会执行一次
	do {
	    //从标签之后读,读满cb字符数组,注意,这里是调用in的读方法。
	    n = in.read(cb, dst, cb.length - dst);
	} while (n == 0);
        //读到数据的情况,没有读到的话就不做任何操作。
	if (n > 0) {
            //现在缓存空间中已有的真实缓存数量
	    nChars = dst + n;
            //下一个读取的位置。
	    nextChar = dst;
	}
}

其它的read方法和这个类似。

一次读出很多字符的时候,处理的策略是:

a.缓存数组不够,就用in直接读,不经过缓存.

b.缓存数组够,就将缓存中读出。

c.缓存数组够,但读完后还没读满,则继续从in中接着读,不够的部分不过缓存数组。

 

1.5 readLine

这个是用的比较多的方法,所以列出来。这个方法在有上面的基础上,还是很好懂的。

String readLine(boolean ignoreLF) throws IOException {
        //传入的布尔值默认为false
	StringBuffer s = null;
	int startChar;

        synchronized (lock) {
            ensureOpen();
	    boolean omitLF = ignoreLF || skipLF;
        //这个是什么?goto?
	bufferLoop:
            //while
	    for (;;) {
                //下一个字符超出缓存数组大小,这里nextChar是从0开始的,所以相等的时候就代表已经超出了缓存数组范围。
		if (nextChar >= nChars)
		    fill();
		//下面的if是判断流的末尾,读完了就返回null,或者将之前读的内容返回
		if (nextChar >= nChars) {
		    if (s != null && s.length() > 0)
			return s.toString();
		    else
			return null;
		}
		//表示没有到末尾.
		boolean eol = false;
		char c = 0;
		int i;
                //这个是处理\r\n的情况,不进行两次判断,忽略
		if (omitLF && (cb[nextChar] == '\n')) 
                    nextChar++;
		skipLF = false;
		omitLF = false;

	    charLoop:
		//遍历缓存数组,直到\n或者\r
		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.6 其它方法

skip 就是先把缓存数组中跳过去,如果缓存数组不够,就再将数据读入缓存数组,再跳,一直循环。

ready 表示缓存是否读完了,没什么用处。

markSupported 是否支持标记

close 流等需要关闭的东西都关闭。

 

2.BufferedWriter

2.1 继承关系

public class BufferedWriter extends Writer {
    //装饰模式
    private Writer out;
}

2.2 构造函数

将缓存数组初始化,并且根据平台初始化换行符号。

    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;
        //获取换行的符号\n \r,和方法System.getProperty("line.separator")一样
	lineSeparator =	(String) java.security.AccessController.doPrivileged(
               new sun.security.action.GetPropertyAction("line.separator"));
    }

2.3 write有关

public void write(int c) throws IOException {
	synchronized (lock) {
            //判断是否有输出流
	    ensureOpen();
            //如果缓存数组写满了,就flush数组。
	    if (nextChar >= nChars)
		flushBuffer();
            //将内容写入缓存数组中
	    cb[nextChar++] = (char) c;
	}
}
//flush,这个方法就知道flush的重要性,
void flushBuffer() throws IOException {
	synchronized (lock) {
	    ensureOpen();
	    if (nextChar == 0)
		return;
            //将数据写入流
	    out.write(cb, 0, nextChar);
	    nextChar = 0;
	}
}

用的比较多的写字符串。就是将字符串转变成字符数组。

public void write(String s, int off, int len) throws IOException {
	synchronized (lock) {
	    ensureOpen();
            
	    int b = off, t = off + len;
	    while (b < t) {
		int d = min(nChars - nextChar, t - b);
                //将字符串转为字符数组
		s.getChars(b, b + d, cb, nextChar);
                //写入缓存数组
		b += d;
		nextChar += d;
		if (nextChar >= nChars)
                    //如果写满了,就写入流中。
		    flushBuffer();
	    }
	}
}

2.4 其它

a.writeLine 写一个换行

public void newLine() throws IOException {
        //同样写到缓存数组里
	write(lineSeparator);
}

b.flush,这个也不多说了。

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

c.close 关闭所有该关闭的.

public void close() throws IOException {
	synchronized (lock) {
	    if (out == null) {
		return;
	    }
	    try {
                //最后还释放了一次。不过没有执行flush方法,所以在close前还是要执行一次flush!
	        flushBuffer();
	    } finally {
	        out.close();
	        out = null;
	        cb = null;
	    }
	}
}

3.结束

看完这个字符流,清晰了好多,开始看的比较慢,但是后来越来越快,水到渠成。