從接收輸入值說起
在日常的開發應用中,有時候需要直接接收外部設備如鍵盤等的輸入值,而對於這種數據的接收方式,我們一般有三種方法:字節流讀取,字符流讀取,Scanner
工具類讀取。
字節流讀取
直接看一個例子:
public class Demo01SystemIn {
public static void main(String[] args) throws IOException {
int a = System.in.read();
System.out.println(a);
char c = 'a';
System.out.println((int) c);
}
}
運行程序之后,會被 read
方法阻塞,這時候在控制台輸入一個字符 a
,那么上面的程序兩句話都會輸出 97
,這個沒問題,因為小寫字母 a
對應的就是 97
,那么假如我們輸入一個中文會出現什么結果呢?
把上面示例中的 a
修改為 中
,然后運行程序,在控制台同樣輸入 中
,則會得到 228
和 20013
,這就說明我們控制台輸入的 中
並沒有全部讀取,原因就是 read
只能讀取 1
個字節,為了進一步驗證結論,我們將上面的例子進行改寫:
public class Demo01SystemIn {
public static void main(String[] args) throws IOException {
char a = (char) System.in.read();//讀取一個字節
System.out.println(a);
char c = '中';
System.out.println(c);
}
}
運行之后得到如下結果:
可以看到,第一個輸出亂碼了,因為 System.in.read()
一次只能讀取一個字節,而中文在 utf-8
編碼下占用了 3
個字節。正因為 read
方法一次只能讀取一個字節,所以其范圍只能在 -1~255
之間,-1
表示已經讀取到了結尾。
那么如果想要完整的讀取中文應該怎么辦呢?
字符流讀取
我們先看下面一個例子:
public class Demo01SystemIn {
public static void main(String[] args) throws IOException {
InputStreamReader inputStreamReader1 = new InputStreamReader(System.in);
int b = inputStreamReader1.read();//只能讀一個字符
System.out.println(b);
InputStreamReader inputStreamReader2 = new InputStreamReader(System.in);
char[] chars = new char[2];
int c = inputStreamReader2.read(chars);//讀入到指定char數組,返回當前讀取到的字符數
System.out.println("讀取的字符數為:" + c);
System.out.println(chars[0]);
System.out.println(chars[1]);
}
}
運行之后,輸出結果如下所示:
這個時候我們已經能完成的讀取到一個字符了,當然,有時候為了優化,我們需要使用 BufferedReader
進行進一步的包裝:
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
這種方式雖然解決了讀取中文會亂碼問題,但是使用起來也不是很方便,所以一般讀取鍵盤輸入信息我們都會采用 Scnner
來讀取。
Scanner 讀取
Scanner
實際上還是對 System.in
進行了封裝,並提供了一系列方法來讀取不同的字符類型,比如 nextInt
,nextFloat
,以及 next
等。
public class Demo02Scnner {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextInt()){
System.out.println(scanner.nextInt());
}
}
}
什么是 IO 流
流是一種抽象概念,它代表了數據的無結構化傳輸(摘自百度百科)。IO
流對應的就是 InPut
和 Output
,也就是輸入和輸出。輸入和輸出這個概念是針對於應用程序而言,比如當前程序中需要讀取文件中的內容,那么這就是輸入,而如果需要將應用程序本身的數據發送到其他應用,就對應了輸出。
字節流和字符流
根據流的處理方式又可以將流可以分為兩種類型:字節流和字符流。
字節流
字節流讀取的基本單位為字節,采用的是 ASCII
編碼,通常用來處理二進制數據,其頂層抽象類為 InputStream
和 OutputStream
,比如上面示例中的 System.in
實際上就是獲取到了一個 InputStream
類。
Java
中的流家族非常龐大,提供了非常多的具有不同功能的流,在實際應用中我們可以選擇不同的組合達到目的。
字節輸入流
下圖為字節輸入流家族關系示意圖:
從上圖可以看出這些結構非常清晰,首先是一個最頂層的接口,其次就是一些不同功能的基礎流,比如我們最常用的 FileInputStream
就是用來讀取文件的,這其中有一個 FilterInputStream
流,這個流主要是用來擴展基礎流功能,其本身只是簡單的覆蓋了父類 InputStream
中的所有方法,並沒有做什么特殊處理,真正的功能擴展需要依賴於其眾多的子類,比如最常用的 BufferedInputStream
提供了數據的緩沖,從而提升讀取流的效率,而 DataInputStream
是可以用來處理二進制數據等等。
通過這些眾多不同功能的流來組合,可以靈活的讀取我們需要的數據。比如當我們需要讀取一個二進制文件,那么就需要使用 DataInputStream
,而 DataInputStream
本身不具備直接讀取文件內容的功能,所以需要結合 FileInputStream
:
FileInputStream fin = new FileInputStream("E:\\test.txt");
DataInputStream din = new DataInputStream(fin);
System.out.println(din.readInt());
同時,如果我們想要使用緩沖機制,又可以進一步組裝 BufferedInputStream
:
FileInputStream fin = new FileInputStream("E:\\test.txt");
DataInputStream din = new DataInputStream(new BufferedInputStream(fin));
System.out.println(din.readInt());
還有一種流比較有意思,那就是 PushbackInputStream
,這個流可以將讀出來的數據重新推回到流中:
public class Demo03 {
public static void main(String[] args) throws IOException {
FileInputStream fin = new FileInputStream("E:\\test.txt");//文檔內存儲 abcd
PushbackInputStream pin = new PushbackInputStream(new BufferedInputStream(fin));
int a = pin.read();//讀取到a
System.out.println(a);
if (a != 'b'){
pin.unread(a);//將 a 推回流中
}
System.out.println(pin.read());//再次讀取到 a
System.out.println(pin.read());//讀取到 b
System.out.println(pin.read());// 讀取到 c
}
}
字節輸出流
下圖為字節輸出流家族關系示意圖:
這個結構和輸入流的結構基本類似,同樣的我們也可以通過組合來實現不同的輸出。
比如普通的輸出文件,可以使用 FileOutputStream
流:
FileOutputStream fout = new FileOutputStream("E:\\test2.txt");
fout.write(1);
fout.write(2);
如果想要輸出二進制格式,那么就可以組合 DataOutputStream
流:
FileOutputStream fout = new FileOutputStream("E:\\test2.txt");
DataOutputStream dout = new DataOutputStream(fout);
dout.write(9);
dout.write(10);
緩沖流的原理
IO
操作是一個比較耗時的操作,而字節流的 read
方法一次只能返回一個字節,那么當我們需要讀取多個字節時就會出現每次讀取都要進行一次 IO
操作,而緩沖流內部定義了一個大小為 8192
的 byte
數組,當我們使用了緩沖流時,讀取數據的時候則會一次性最多讀取 8192
個字節放到內存,然后一個個依次返回,這樣就大大減少了 IO
次數;同樣的,寫數據時,緩沖流會將數據先寫到內存,當我們寫完需要寫的數據時再一次性刷新到指定位置,如磁盤等。
字符流
字符流讀取的基本單位為字符,采用的是 Unicode
編碼,其 read
方法返回的是一個 Unicode
碼元(0~65535)。
字符流通常用來處理文本數據,其頂層抽象類為 Reader
和 Write
,比如文中最開始的示例中的 InputStreamReader
就是繼承自 Reader
類。
字符輸入流
下圖為字符輸入流家族關系示意圖:
上圖可以看出,除頂層 Reader
類之外,字符流也提供了一些基本的字符流來處理文本數據,比如我們需要從文本讀取內容:
public class Demo05Reader {
public static void main(String[] args) throws Exception {
//字節流
FileInputStream fin = new FileInputStream("E:\\test.txt");//文本內容為“雙子孤狼”
System.out.println(fin.read());//372
//字符流
InputStreamReader ir = new InputStreamReader(new FileInputStream("E:\\test.txt"));//文本內容為“雙子孤狼”
System.out.println(ir.read());//21452
char s = '雙';
System.out.println((int)s);//21452
}
}
輸出之后可以很明顯看出區別,字節流一次讀入一個字節,而字符流一次讀入一個字符。
當然,我們也可以采用自由組合的方式來更靈活的進行字符讀取,比如我們結合 BufferedReader
來讀取一整行數據:
public class Demo05Reader {
public static void main(String[] args) throws Exception {
InputStreamReader ir = new InputStreamReader(new FileInputStream("E:\\test.txt"));//文本內容為“雙子孤狼”
BufferedReader br = new BufferedReader(ir);
String s;
while (null != (s = br.readLine())){
System.out.println(s);//輸出雙子孤狼
}
}
}
字符輸出流
下圖為字符輸出流家族關系示意圖:
文本輸出,我們用的最多的就是 PrintWriter
,這個類我想絕大部分朋友都使用過:
public class Demo06Writer {
public static void main(String[] args) throws Exception{
PrintWriter printWriter = new PrintWriter("E:\\test3.txt");
printWriter.write("雙子孤狼");
printWriter.flush();
}
}
這里和字節流的區別就是寫完之后需要手動調用 flush
方法,否則數據就會丟失,並不會寫到文件中。
為什么字符流需要 flush,而字節流不需要
字節流不需要 flush
操作是因為字節流直接操作的是字節,中途不需要做任何轉換,所以直接就可以操作文件,而字符流,說到底,其底層還是字節流,但是字符流幫我們將字節轉換成了字符,這個轉換需要依賴字符表,所以就需要在字符和字節完成轉換之后通過 flush
操作刷到磁盤中。
需要注意的是,字節輸出流最頂層類 OutputStream
中也提供了 flush
方法,但是它是一個空的方法,如果有子類有需要,也可以實現 flush
方法。
RandomAccessFile
RandomAccessFile
是一個隨機訪問文件類,其可以在文件中的任意位置查找或者寫入數據。
public class Demo07RandomAccessFile {
public static void main(String[] args) throws Exception {
//文檔內容為 lonely wolf
RandomAccessFile inOut = new RandomAccessFile(new File("E:\\test.txt"),"rw");
System.out.println("當前指針在:" + inOut.getFilePointer());//默認在0
System.out.println((char) inOut.read());//讀到 l
System.out.println("當前指針在:" + inOut.getFilePointer());
inOut.seek(7L);//指針跳轉到7的位置
System.out.println((char) inOut.read());//讀到 w
inOut.seek(7);//跳回到 7
inOut.write(new byte[]{'c','h','i','n','a'});//寫入 china,此時 wolf被覆蓋
inOut.seek(7);//繼續跳回到 7
System.out.println((char) inOut.read());//此時因為 wolf 被 china覆蓋,所以讀到 c
}
}
根據上面的示例中的輸出結果,可以看到 RandomAccessFile
類可以隨機指定指針,並隨機進行讀寫,功能非常強大。
另外需要說明的是,構造 RandomAccessFile
時需要傳入一個模式,模式主要有 4
種:
- r:只讀模式。此時調用任何
write
相關方法,會拋出IOException
。 - rw:讀寫模式。支持讀寫,如果文件不存在,則會創建。
- rws:讀寫模式。每當進行寫操作,會將內容或者元數據同步刷新到磁盤。
- rwd:讀寫模式。每當進行寫操作時,會將變動的內容用同步刷新到磁盤。
總結
本文主要將 Java
中的 IO
流進行了梳理,通過將其分成字節流和字符流,以及輸入流和輸出流分別統計,來建立一個對 Java
中 IO
流全局的概念,最后通過一些實例來演示了如何通過不同類型的流來組合實現強大靈活的輸入和輸出,最后,介紹了同時支持輸入和輸出的 RandomAccessFile
。