[重學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;
成員方法
構造方法 創建一個默認緩衝區大小的緩衝輸出流或者指定大小的緩衝輸出流
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篇
衛宮家今天的飯
博多豚骨拉麪團
注意 輸出到文件 重複執行會覆蓋上一次執行的內容