流的概念
JAVA程序通過流來完成輸入/輸出。流是生產或消費信息的抽象,流通過JAVA的輸入輸出與物理設備鏈接,盡管與它們鏈接的物理設備不盡相同,所有流的行為具有相同的方式。這樣就意味一個輸入流能夠抽象多種不同類型的輸入:從磁盤文件、從鍵盤或從網絡套接字;同樣,一個輸出流可以輸出到控制台、磁盤文件或相連的網絡。
在我們平時接觸的輸入/輸出流中,有這樣一個概念必須要弄明白,何謂輸入、何謂輸出?討論這個問題的前提是要知道以什么為參考物,這個參考物就是程序或者內存。輸入:就是從磁盤文件或者網絡等外部的數據流向程序或者內存。輸出就是相反的過程。其中這個“外部”可以是很多種介質:
1、本地磁盤文件、遠程磁盤文件。
2、數據庫鏈接
3、TCP、UDP、HTTP網絡通信
4、進程間通信
5、硬件設備(鍵盤、串口)
流的分類
1、從功能上:輸入流、輸出流
2、從結構上:字節流、字符流
3、從來源上:節點流、過濾流
其中InputStream/OutputStream是為字節流而設計的,Reader/Writer是為字符流而設計的。處理字節或者二進制對象使用字節流,處理字符或者字符串使用字符流。
在最底層,所有的輸入/輸出都是字節形式的,基於字符的流只在處理字符的時候提供方便有效的方法。
節點流是從特定的地方讀寫的流,例如磁盤或者內存空間,也就是這種流是直接對接目標的。
過濾流是使用節點流作為輸入輸出的,就是在節點流的基礎上進行包裝,新增一些特定的功能。
InputStream
其中有底色標注的為節點流,無底色標注的為過濾流,其中FilterInputStream在JDK中的定義為:包含其他一些輸入流,它將這些流用作其基本數據源,可以直接傳輸數據或提供一些額外的功能,這個類本身並不經常被我們使用,常用的是它的子類。
定義了字節輸入模式的抽象類,該類提供了三個重載的read方法:
我們可以看到,三個read方法中,其中有一個是抽象的。那在這里思考這樣一個問題:為什么只有第一個是抽象的, 其他兩個是具體的?
因為后面兩個方法內部最終會去調用第一個方法,所以在InputStream派生類中只需要重寫第一個方法就可以了。在這里可以看到第一個read方法是與具體的I/O設備相關的,需要子類去實現。
讀寫數據的邏輯步驟為:
open a stream
while more information
read/write information
close the stream
一、FileInputStream
1 public static void main(String[] args) throws IOException
2 { 3 InputStream is = new FileInputStream("c:/test.txt"); 4 int length = 0; 5 byte[] buffer = new byte[20]; 6 while(-1 != (length = is.read(buffer,0,20))) 7 { 8 String str = new String(buffer,0,length); 9 System.out.print(str); 10 } 11 is.close(); 12 }
執行結果:
中國移動基地咪咕閱讀
中國移動基地咪咕音樂
a
1、首先我們看下讀取文件test的內容:
2、在這里我們使用的是輸入流,讀取的是我本機C盤中名為test文件中的內容。
3、每次讀取的內容存放在buffer這個字節數組中,然后轉換成String字符串打印在控制台中。
4、我們來看下第6行代碼:因為一個文件內容有多少我們事先並不知道,所以在這里只能分批次讀取,每次最多讀取20個字節(即buffer數組定義的長度)。
5、length的作用是表示在最后一次讀取的時候,讀取的長度小於等於buffer數組的長度,while循環體執行結束后,下一次再來讀取已經沒有內容了,read方法在這個時候會返 回-1,然后跳出循環。
6、對於流的操作,最后一步永遠是調用close方法,釋放資源。
總結:文本中內容故意定義為41個字節長度,然后打印的時候用了換行。從執行結果可以看到,前兩次讀取的長度都是20個字節,第三次是1個字節,最后一次沒有內容返回-1,然后就跳出循環體了。這個DEMO雖然很簡單,但是也是IO種最基本最需要掌握的一部分。
二、FileOutputStream
這個類與FileInputStream是成對的,用法也類似:
1 public static void main(String[] args) throws IOException 2 { 3 OutputStream os = new FileOutputStream("c:/test1.txt"); 4 String str = "中國移動手機閱讀"; 5 byte[] b = str.getBytes(); 6 os.write(b); 7 os.close(); 8 }
執行結果:
1、這個DEMO主要是將字符串寫入磁盤文件中。
2、在第3行的構造函數處要注意下,這個方法中如果指定的文件不存在,則會創建一個新的;如果指定的文件存在,在后面的寫入操作會覆蓋原有的內容。
這個大家會有這樣一個疑問,如果我不想覆蓋原有的內容,只想在后面追加內容呢?
1 public static void main(String[] args) throws IOException 2 { 3 OutputStream os = new FileOutputStream("c:/test.txt",true); 4 String str = "注冊用戶6億"; 5 byte[] b = str.getBytes(); 6 os.write(b); 7 os.close(); 8 }
執行結果:
1、從執行結果看,這個是在原有內容后面進行追加;程序唯一的區別就在於第三行的構造函數。
OutputStream os = new FileOutputStream("c:/test.txt",true);
如果第二個參數為 true
,則將字節寫入文件末尾處,而不是寫入文件開始處。
三、BufferedOutputStream
在前面介紹的FileOutputStream,是在程序里面讀取一個字節,就往外寫一個字節。在這種情況下,頻繁的跟IO設備打交道,I/O的處理速度跟CPU的速度根本就不在一個量級上(I/O是一種物理設備),在信息量很多的時候,就比較消耗性能。基於這種問題,JAVA提供了緩沖字節流,通過這種流,應用程序就可以將各個字節寫入底層輸出流中,而不必針對每次字節寫入調用底層系統。
1 public static void main(String[] args) throws IOException 2 { 3 OutputStream os = new FileOutputStream("c:/test.txt"); 4 OutputStream bs = new BufferedOutputStream(os); 5 byte[] buffer = "中國移動閱讀基地".getBytes(); 6 bs.write(buffer); 7 bs.close(); 8 os.close(); 9 }
執行結果:
緩沖輸出流在輸出的時候,不是直接一個字節一個字節的操作,而是先寫入內存的緩沖區內。直到緩沖區滿了或者我們調用close方法或flush方法,該緩沖區的內容才會寫入目標。才會從內存中轉移到磁盤上。下面來看看不調用close方法會出現什么情況:
1 public static void main(String[] args) throws IOException 2 { 3 OutputStream os = new FileOutputStream("c:/test1.txt"); 4 OutputStream bs = new BufferedOutputStream(os); 5 byte[] buffer = "中國移動閱讀基地".getBytes(); 6 bs.write(buffer); 7 // bs.close(); 8 // os.close(); 9 }
執行結果:
在這里沒有調用close方法,相當於內容還在內存的緩沖區中。BufferedOutputStream本身沒有close方法,調用的是父類FilterOutputStream的close方法:
1 public void close() throws IOException { 2 try { 3 flush(); 4 } catch (IOException ignored) { 5 } 6 out.close(); 7 }
在這里可以看到這個方法的本質是在在關閉資源之前,調用的flush方法。
而flush在JDK中的定義為:刷新此緩沖的輸出流。這迫使所有緩沖的輸出字節被寫出到底層輸出流中
四、DataOutputStream
數據輸出流允許應用程序以適當方式將基本 Java 數據類型寫入輸出流中。然后,應用程序可以使用數據輸入流將數據讀入。
1 public static void main(String[] args) throws IOException 2 { 3 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream( 4 new FileOutputStream("c:/data.txt"))); 5 byte b = 4; 6 char c = 'c'; 7 int i = 12; 8 float f = 3.3f; 9 dos.writeByte(b); 10 dos.writeChar(c); 11 dos.writeInt(i); 12 dos.writeFloat(f); 13 dos.close();
執行結果:
打開之后,里面是亂碼,程序寫入之后是一個二進制文件。我們的程序中是將java的基本數據類型寫入文本,注意這里不是字符串,而是基本數據類型。我們這樣寫入是沒有意義的,下面我們用同樣的方式去讀取。
1 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream( 2 new FileOutputStream("c:/data.txt"))); 3 byte b = 4; 4 char c = 'c'; 5 int i = 12; 6 float f = 3.3f; 7 dos.writeByte(b); 8 dos.writeChar(c); 9 dos.writeInt(i); 10 dos.writeFloat(f); 11 dos.close(); 12 13 DataInputStream dis = new DataInputStream(new BufferedInputStream( 14 new FileInputStream("c:/data.txt"))); 15 System.out.println(dis.readByte()); 16 System.out.println(dis.readChar()); 17 System.out.println(dis.readInt()); 18 System.out.println(dis.readFloat()); 19 dis.close(); 20
執行結果:
4
c
12
3.3
這里看到,我們的數據輸入流允許應用程序以與機器無關方式從底層輸入流中讀取基本JAVA數據類型。
這里特別注意的地方是:讀取數據類型的順序與當初寫入的數據類型的順序要一致,否則會出現亂碼或者讀取的信息不准確。
這個原因我們可以這樣來理解:寫入的時候是不同類型的基本數據,不同的基本數據類型的字節長度不一樣,如果讀取時候順序不一致,會導致字節的組合混亂,導致亂碼或者走義。