在我的上一篇博客java -- IO流之字節流中已經介紹了java IO流中常見的字節流,字節流中大部分類都是JDK1.0出現的。在JDK1.1中對基本的IO流類庫進行了重大的修改,出現了字符流Reader和Writer。
其實JDK1.1設計Reader和Writer繼承層次結構主要是爲了國際化。字節流僅支持8位的字節,並不能很好地處理16位的Unicode字符。由於Unicode用於字符國際化,所以添加Reader和Writer繼承層次結構就是爲了在所有的IO流操作中都支持Unicode。此外,由於字符流操作的是字符,所以比字節流更快。
儘管字符流操作比字節流效果更好,但是字符流終究只能操作與字符相關的,要是操作的是二進制文件或其他字符流不支持的文件,就只能使用字節流了。所以說,最明智的選擇是儘量使用字符流,當字符流不能滿足我們的需求時再考慮使用字節流。
Reader體系常見類
Writer體系常見類
對比java -- IO流之字節流這篇博客中的字節流體系常見類圖就會發現上面字符流體系常見類圖其實跟它是差不多的,不能說一致,但是基本的使用方法還是一樣的。下面介紹常見字符流的基本操作。
InputStreamReader & OutputStreamWriter
InputStreamReader是字節流通向字符流的橋樑。在它的構造器中需要傳入一個InputStream,並可以指定charset讀取字節並將其解碼爲字符,如果沒有指定charset則使用平臺默認的字符集。
OutputStreamWriter是字符流通向字節流的橋樑。在它的構造器中需要傳入一個OutputStream,並可以指定charset將要寫入流中的字符編碼成字節,如果沒有指定charset則使用平臺默認的字符集。
轉換流圖
package com.gk.io.character;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class ConversionStreamDemo {
public static void main(String[] args) throws IOException {
write("conversionStream.txt");
}
public static void write(String fileName) throws IOException {
/*
* 創建OutputStreamWriter對象,需要一個OutputStream參數
* 第二個參數爲指定的字符編碼,如果沒有則使用平臺默認字符集
*/
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(fileName), "utf-8");
osw.write("我愛java"); // 寫入一個字符串
// 釋放資源
osw.close();
}
}
上面演示了使用OutputStreamWriter往文件中寫入數據,支持三種寫入方式:寫入一個字符、一個字符數組和一個字符串。這裏演示的是寫入一個字符串,字符和字符數組的情況請參考java -- IO流之字節流的FileInputStream & FileOutputStream一節,這裏不再贅述。
存數據已經實現了,那麼怎樣才能取出來呢?下面演示使用InputStreamReader取文件裏面的數據。
public static void read(String fileName) throws IOException {
InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "gbk");
char[] ch = new char[1024];
isr.read(ch); // 讀取一個字符數組的數據
System.out.println(new String(ch));
isr.close(); // 釋放資源
System.out.println("-------------------");
char[] ch2 = new char[1024];
InputStreamReader isr2 = new InputStreamReader(new FileInputStream(fileName), "utf-8");
isr2.read(ch2);
System.out.println(new String(ch2));
isr2.close(); // 釋放資源
}
上面InputStreamReader演示了使用不同的字符集解碼OutputStreamWriter中使用”utf-8”寫入文件的中的數據。可以看到使用"gbk"解碼的時候中文出現了亂碼,而使用”utf-8”卻能正常的顯示中文。這就說明了InputStreamReader的解碼方式與OutputStreamWriter的編碼方式要一致。或者像下面演示文件的複製中兩者都不指定字符集,而使用平臺默認的字符集。
public static void copy(String sourceFile, String destFile) throws IOException {
InputStreamReader isr = new InputStreamReader(new FileInputStream(sourceFile));
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(destFile));
int len = 0;
char[] ch = new char[1024];
while((len = isr.read(ch)) != -1){
osw.write(ch, 0, len);
}
// 釋放資源
isr.close();
osw.close();
}
FileReader & FileWriter
當我們使用本地默認編碼的時候(一般程序中也都是使用本地默認編碼),InputStreamReader和OutputStreamWriter就提供了相應的子類FileReader和FileWriter來簡化我們的操作。這兩個類都只有構造器而沒有自己的方法。所有可操作的方法都是繼承自父類。
package com.gk.io.character;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileStreamDemo {
public static void main(String[] args) throws IOException {
copy("sourceFile.txt", "destFile.txt");
}
public static void copy(String sourceFile, String destFile) throws IOException {
FileReader fr = new FileReader(sourceFile);
FileWriter fw = new FileWriter(destFile);
int len = 0;
char[] ch = new char[1024];
while((len = fr.read(ch)) != -1){
fw.write(ch, 0, len);
}
// 釋放資源
fr.close();
fw.close();
}
}
當需要實現文件的追加時,可以這樣使用FileWriter的構造器:FileWriter(String fileName, boolean append),append爲true時表示追加。
BufferedReader & BufferedWriter
使用FileReader或其他一些沒有緩衝功能的流操作文件時,每次調用read()或readLine()都會導致從文件中讀取字節,並將其轉換爲字符後返回,而這是極其低效的。JDK1.1爲我們提供了BufferedReader,BufferedReader具有緩衝的功能,在字符輸入流中讀取文本的時候,能緩衝各個字符,從而實現字符、數組和行的高效讀取。對應的BufferedWriter能實現單個字符、數組和字符串的高效寫入。
由於大多數方法與FileReader和FileWriter差不多,這裏只演示BufferedReader和BufferedWriter特有的功能。
package com.gk.io.character;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedStreamDemo {
public static void main(String[] args) throws IOException {
write("bufferedStream.txt");
}
public static void write(String bufferedStream) throws IOException {
BufferedWriter bw = new BufferedWriter(new FileWriter(bufferedStream));
bw.write("I love Java");
bw.newLine(); // 操作系統下的換行,實現了平臺無關性
bw.write("I love Python,too");
// 釋放資源
bw.close();
}
}
在BufferedWriter中有newLine()方法,此方法是操作系統下的換行,實現了平臺無關性。如果上面代碼中沒有添加bw.newLine();就會出現下圖結果,當然在Windows操作系統下我們可以手動添加換行符”\r\n”(bw.write("I love Java\r\n")),但是這樣在別的操作下運行又不能正確的解析了,從而使程序不能實現跨平臺。
public static void read(String bufferedStream) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(bufferedStream));
StringBuilder sb = new StringBuilder();
String line = br.readLine();
sb.append(line);
line = br.readLine();
sb.append(line);
System.out.println(sb.toString());
// 釋放資源
br.close();
}
bufferedStream.txt文件中的"I love Java"和"I love Python,too"是有換行效果的,但是上面程序的輸出卻沒有換行,我們看下API中對readLine()的解釋:用於讀取一個文本行,但是不包含任何行終止符,如果已到達流末尾,則返回null。通過解釋我們就不難理解上面程序的輸出結果了。
下面再用這兩個方法實現對文件的高效複製。
public static void copy(String sourceFile, String destFile) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(sourceFile));
BufferedWriter bw = new BufferedWriter(new FileWriter(destFile));
String line = null;
while((line = br.readLine()) != null){
bw.write(line);
bw.newLine();
bw.flush();
}
// 釋放資源
br.close();
bw.close();
}
由於readLine()方法獲取的一行字符串中沒有包含換行符,所以BufferedWriter寫入該字符串後再用newLine()實現自動換行。flush()方法不是BufferedWriter特有的方法,Writer類就有的這個方法,所以說整個Writer體系都擁有該方法,用於刷新存放在緩衝區中的字符。我們知道使用write()往文件寫入數據時,並不是一write就馬上往文件寫入數據,而是先存放在緩衝區中,當數據到達一定數量後(默認爲8k)再一起寫入文件,flush()方法可以實現手動刷新。
如果想測試BufferedReader與FileReader的效率,可參考java -- IO流之字節流中BufferedInputStream & BufferedOutputStream這一小節。
LineNumberReader
有時候我們想記錄文件數據的行號,我們可以這樣做:
package com.gk.io.character;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
public class LineNumberReaderDemo {
public static void main(String[] args) throws IOException {
fun();
}
public static void fun() throws IOException {
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
int count = 0;
String line = null;
while((line = br.readLine()) != null){
System.out.println(++count + " : " + line);
}
// 釋放資源
br.close();
}
}
定義一個計數變量count,然後累加記錄行號即可。不過這樣做對於喜歡偷懶的我們來說顯然有些麻煩,於是java就提供了LineNumberReader給我們使用,此類是BufferedReader的直接子類,除了具有BufferedReader的緩衝作用外,最大特點就是能跟蹤行號。上面代碼使用LineNumberReader修改爲:
public static void fun2() throws IOException {
LineNumberReader lnr = new LineNumberReader(new FileReader("br.txt"));
String line = null;
while((line = lnr.readLine()) != null){
System.out.println(lnr.getLineNumber() + " : " + line);
}
// 釋放資源
lnr.close();
}
getLineNumber()方法可以獲取當前行號,默認從0開始,且隨數據讀取在每個行結束符處遞增,由於我們在while中先使用了readLine(),所以第一次獲取行號getLineNumber()就遞增爲1了。
也可以使用setLineNumber(int)來設置起始行號,見下面例子。
public static void fun3() throws IOException {
LineNumberReader lnr = new LineNumberReader(new FileReader("br.txt"));
lnr.setLineNumber(10);
String line = null;
while((line = lnr.readLine()) != null){
System.out.println(lnr.getLineNumber() + " : " + line);
}
// 釋放資源
lnr.close();
}
在使用readLine()之前先用setLineNumber(10)設置了起始行號爲10,跟上面一樣,由於先使用了readLine(),所以第一次getLineNumber()的時候就遞增爲11。
CharArrayReader & CharArrayWriter
此類與下面的StringReader和字節流中的ByteArrayInputStream一樣,都是用於操作內存的流,請參考java -- IO流之字節流中的ByteArrayInputStream & ByteArrayOutputStream這一小節的解釋。這裏只簡單的演示如何使用。
package com.gk.io.character;
import java.io.CharArrayReader;
import java.io.CharArrayWriter;
import java.io.IOException;
public class CharArrayStreamDemo {
public static void main(String[] args) throws IOException {
char[] ch = write();
read(ch);
}
public static char[] write() throws IOException {
CharArrayWriter caw = new CharArrayWriter();
caw.write("I love java");
caw.close(); // 關閉此流無效
caw.write("I love python,too"); // close之後再寫入不會報IOException
return caw.toCharArray();
}
public static void read(char[] ch) throws IOException {
CharArrayReader car = new CharArrayReader(ch);
int len = 0;
while((len = car.read()) != -1){
System.out.print((char)len);
}
// 釋放資源
car.close();
}
}
StringReader & StringWriter
package com.gk.io.character;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
public class StringStreamDemo {
public static void main(String[] args) throws IOException {
String str = write();
read(str);
}
public static String write() throws IOException {
StringWriter sw = new StringWriter();
sw.write("I love java");
sw.close(); // 關閉此流無效
sw.write("I love python,too"); // close之後再寫入不會報IOException
return sw.toString();
}
public static void read(String str) throws IOException {
StringReader sr = new StringReader(str);
// 一次讀取一個字符
/*int len = 0;
while((len = sr.read()) != -1){
System.out.print((char)len);
}*/
// 一次讀取一個字符數組
char[] ch = new char[1024];
while(sr.read(ch) != -1){
System.out.println(new String(ch));
}
// 釋放資源
sr.close();
}
}
PrintWriter
PrintWriter與PrintStream類似,只不過PrintWriter支持寫入字符。當啓動自動刷新功能的時候,與PrintStream不同的是,PrintWriter只有在調用 println、printf 或 format 的其中一個方法時纔可能完成刷新操作,而不是每當正好輸出換行符時才完成。
package com.gk.io.character;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class PrintWriterDemo {
public static void main(String[] args) throws IOException {
write("pw.txt");
}
public static void write(String fileName) throws IOException {
PrintWriter pw = new PrintWriter(new FileWriter(fileName));
pw.println(47);
pw.println(13.14);
pw.println("I love java");
pw.println(true);
//pw.flush();
//pw.close();
}
}
上面例子沒有啓動自動刷新,也沒有手動flush和close,因爲close的時候會先刷新,所以當寫入的數據小於緩衝區大小時數據是先存放在緩衝區的,也就是還沒輸出到文件,所以pw.txt文件是空的。下面演示開啓自動刷新功能。
public static void write2(String fileName) throws IOException {
PrintWriter pw = new PrintWriter(new FileWriter(fileName),true);
pw.println(47);
pw.println(13.14);
pw.println("I love java");
pw.print(true);
//pw.flush();
//pw.close();
}
當指定構造器PrintWriter(Writer out, boolean autoFlush)中的autoFlush爲true時即開啓了自動刷新。前面說過當調用println、printf或 format的其中一個方法時纔可能完成刷新操作,所以pw.print(true);沒有輸出。
自動刷新功能也給我們提供了另一種思路,前面我們在使用BufferedWriter複製文件的時候每寫入一行數據都手動flush,現在我們可以使用PrintWriter改寫上面的程序。
public static void copy(String sourceFile, String destFile) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(sourceFile));
PrintWriter pw = new PrintWriter(new FileWriter(destFile), true);
String line = null;
while((line = br.readLine()) != null){
pw.println(line);
}
// 釋放資源
br.close();
pw.close();
}
由於println有換行功能,且在PrintWriter的構造方法中啓動了自動刷新,所以上面的pw.println(line)就等價於BufferedWriter的bw.write(line);bw.newLine();bw.flush();。我們完全不用擔心上面程序使用PrintWriter而出現的效率問題,在PrintWriter的內部其實幫我們封裝了BufferedWriter,也就是說PrintWriter也具有BufferedWriter的高效。看下PrintWriter的源碼便知。
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;
}
}