1.1 Java Io流的概念
java的io是實現輸入和輸出的基礎,可以方便的實現數據的輸入和輸出操作。在java中把不同的輸入/輸出源(鍵盤,文件,網絡連接等)抽象表述爲“流”(stream)。通過流的形式允許java程序使用相同的方式來訪問不同的輸入/輸出源。stram是從起源(source)到接收的(sink)的有序數據。
注:java把所有的傳統的流類型都放到在java io包下,用於實現輸入和輸出功能。
1.2 Io流的分類:
按照不同的分類方式,可以把流分爲不同的類型。常用的分類有三種:
1.2.1 按照流的流向分,可以分爲輸入流和輸出流。
- 輸入流: 只能從中讀取數據,而不能向其寫入數據。
- 輸出流:只能向其寫入數據,而不能向其讀取數據。
此處的輸入,輸出涉及一個方向的問題,對於如圖15.1所示的數據流向,數據從內存到硬盤,通常稱爲輸出流——也就是說,這裏的輸入,輸出都是從程序運行所在的內存的角度來劃分的。
注:如果從硬盤的角度來考慮,圖15.1所示的數據流應該是輸入流纔對;但劃分輸入/輸出流時是從程序運行所在的內存的角度來考慮的,因此如圖15.1所在的流時輸出流。而不是輸入流。
對於如圖15.2所示的數據流向,數據從服務器通過網絡流向客戶端,在這種情況下,Server端的內存負責將數據輸出到網絡裏,因此Server端的程序使用輸出流;Client端的內存負責從網絡中讀取數據,因此Client端的程序應該使用輸入流。
注:java的輸入流主要是InputStream和Reader作爲基類,而輸出流則是主要由outputStream和Writer作爲基類。它們都是一些抽象基類,無法直接創建實例。
1.2.2 按照操作單元劃分,可以劃分爲字節流和字符流。
字節流和字符流的用法幾乎完成全一樣,區別在於字節流和字符流所操作的數據單元不同,字節流操作的單元是數據單元是8位的字節,字符流操作的是數據單元爲16位的字符。
字節流主要是由InputStream和outPutStream作爲基類,而字符流則主要有Reader和Writer作爲基類。
1.2.3 按照流的角色劃分爲節點流和處理流。
可以從/向一個特定的IO設備(如磁盤,網絡)讀/寫數據的流,稱爲節點流。節點流也被稱爲低級流。圖15.3顯示了節點流的示意圖。
從圖15.3中可以看出,當使用節點流進行輸入和輸出時,程序直接連接到實際的數據源,和實際的輸入/輸出節點連接。
處理流則用於對一個已存在的流進行連接和封裝,通過封裝後的流來實現數據的讀/寫功能。處理流也被稱爲高級流。圖15.4顯示了處理流的示意圖。
從圖15.4可以看出,當使用處理流進行輸入/輸出時,程序並不會直接連接到實際的數據源,沒有和實際的輸入和輸出節點連接。使用處理流的一個明顯的好處是,只要使用相同的處理流,程序就可以採用完全相同的輸入/輸出代碼來訪問不同的數據源,隨着處理流所包裝的節點流的變化,程序實際所訪問的數據源也相應的發生變化。
1.3 流的原理淺析和常用的流的分類表:
1.3.1 流的原理淺析:
java Io流共涉及40多個類,這些類看上去很雜亂,但實際上很有規則,而且彼此之間存在非常緊密的聯繫, Java Io流的40多個類都是從如下4個抽象類基類中派生出來的。
- InputStream/Reader: 所有的輸入流的基類,前者是字節輸入流,後者是字符輸入流。
- OutputStream/Writer: 所有輸出流的基類,前者是字節輸出流,後者是字符輸出流。
對於InputStream和Reader而言,它們把輸入設備抽象成爲一個”水管“,這個水管的每個“水滴”依次排列,如圖15.5所示:
從圖15.5可以看出,字節流和字符流的處理方式其實很相似,只是它們處理的輸入/輸出單位不同而已。輸入流使用隱式的記錄指針來表示當前正準備從哪個“水滴”開始讀取,每當程序從InputStream或者Reader裏面取出一個或者多個“水滴”後,記錄指針自定向後移動;除此之外,InputStream和Reader裏面都提供了一些方法來控制記錄指針的移動。
對於OutputStream和Writer而言,它們同樣把輸出設備抽象成一個”水管“,只是這個水管裏面沒有任何水滴,如圖15.6所示:
正如圖15.6所示,當執行輸出時,程序相當於依次把“水滴”放入到輸出流的水管中,輸出流同樣採用隱示指針來標識當前水滴即將放入的位置,每當程序向OutputStream或者Writer裏面輸出一個或者多個水滴後,記錄指針自動向後移動。
圖15.5和圖15.6顯示了java Io的基本概念模型,除此之外,Java的處理流模型則體現了Java輸入和輸出流設計的靈活性。處理流的功能主要體現在以下兩個方面。
- 性能的提高:主要以增加緩衝的方式來提供輸入和輸出的效率。
- 操作的便捷:處理流可能提供了一系列便捷的方法來一次輸入和輸出大批量的內容,而不是輸入/輸出一個或者多個“水滴”。
處理流可以“嫁接”在任何已存在的流的基礎之上,這就允許Java應用程序採用相同的代碼,透明的方式來訪問不同的輸入和輸出設備的數據流。圖15.7顯示了處理流的模型。
1.3.2 java輸入/輸出流體系中常用的流的分類表
分類 | 字節輸入流 | 字節輸出流 | 字符輸入流 | 字符輸出流 |
---|---|---|---|---|
抽象基類 | InputStream | OutputStream | Reader | Writer |
訪問文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
訪問數組 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
訪問管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
訪問字符串 | StringReader | StringWriter | ||
緩衝流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
轉換流 | InputStreamReader | OutputStreamWriter | ||
對象流 | ObjectInputStream | ObjectOutputStream | ||
抽象基類 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 | PrintStream | PrintWriter | ||
推回輸入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
注:表中粗體字所標出的類代表節點流,必須直接與指定的物理節點關聯:斜體字標出的類代表抽象基類,無法直接創建實例。
2.常用的io流的用法
下面是整理的常用的Io流的特性及使用方法,只有清楚每個Io流的特性和方法。才能在不同的需求面前正確的選擇對應的IO流進行開發。
2.1 Io體系的基類(InputStream/Reader,OutputStream/Writer)。
字節流和字符流的操作方式基本一致,只是操作的數據單元不同——字節流的操作單元是字節,字符流的操作單元是字符。所以字節流和字符流就整理在一起了。
InputStream和Reader是所有輸入流的抽象基類,本身並不能創建實例來執行輸入,但它們將成爲所有輸入流的模板,所以它們的方法是所有輸入流都可使用的方法。
在InputStream裏面包含如下3個方法。
- int read(); 從輸入流中讀取單個字節(相當於從圖15.5所示的水管中取出一滴水),返回所讀取的字節數據(字節數據可直接轉換爲int類型)。
- int read(byte[] b)從輸入流中最多讀取b.length個字節的數據,並將其存儲在字節數組b中,返回實際讀取的字節數。
- int read(byte[] b,int off,int len); 從輸入流中最多讀取len個字節的數據,並將其存儲在數組b中,放入數組b中時,並不是從數組起點開始,而是從off位置開始,返回實際讀取的字節數。
在Reader中包含如下3個方法。
- int read(); 從輸入流中讀取單個字符(相當於從圖15.5所示的水管中取出一滴水),返回所讀取的字符數據(字節數據可直接轉換爲int類型)。
- int read(char[] b)從輸入流中最多讀取b.length個字符的數據,並將其存儲在字節數組b中,返回實際讀取的字符數。
int read(char[] b,int off,int len); 從輸入流中最多讀取len個字符的數據,並將其存儲在數組b中,放入數組b中時,並不是從數組起點開始,而是從off位置開始,返回實際讀取的字符數。
對比InputStream和Reader所提供的方法,就不難發現這兩個基類的功能基本是一樣的。InputStream和Reader都是將輸入數據抽象成如圖15.5所示的水管,所以程序即可以通過read()方法每次讀取一個”水滴“,也可以通過read(char[] chuf)或者read(byte[] b)方法來讀取多個“水滴”。當使用數組作爲read()方法中的參數, 我們可以理解爲使用一個“竹筒”到如圖15.5所示的水管中取水,如圖15.8所示read(char[] cbuf)方法的參數可以理解成一個”竹筒“,程序每次調用輸入流read(char[] cbuf)或read(byte[] b)方法,就相當於用“竹筒”從輸入流中取出一筒“水滴”,程序得到“竹筒”裏面的”水滴“後,轉換成相應的數據即可;程序多次重複這個“取水”過程,直到最後。程序如何判斷取水取到了最後呢?直到read(char[] chuf)或者read(byte[] b)方法返回-1,即表明到了輸入流的結束點。
InputStream和Reader提供的一些移動指針的方法:
- void mark(int readAheadLimit); 在記錄指針當前位置記錄一個標記(mark)。
- boolean markSupported(); 判斷此輸入流是否支持mark()操作,即是否支持記錄標記。
- void reset(); 將此流的記錄指針重新定位到上一次記錄標記(mark)的位置。
- long skip(long n); 記錄指針向前移動n個字節/字符。
OutputStream和Writer:
OutputStream和Writer的用法也非常相似,它們採用如圖15.6所示的模型來執行輸入,兩個流都提供瞭如下三個方法:
- void write(int c); 將指定的字節/字符輸出到輸出流中,其中c即可以代表字節,也可以代表字符。
- void write(byte[]/char[] buf); 將字節數組/字符數組中的數據輸出到指定輸出流中。
- void write(byte[]/char[] buf, int off,int len ); 將字節數組/字符數組中從off位置開始,長度爲len的字節/字符輸出到輸出流中。
因爲字符流直接以字符作爲操作單位,所以Writer可以用字符串來代替字符數組,即以String對象作爲參數。Writer裏面還包含如下兩個方法。
- void write(String str); 將str字符串裏包含的字符輸出到指定輸出流中。
- void write (String str, int off, int len); 將str字符串裏面從off位置開始,長度爲len的字符輸出到指定輸出流中。
2.2 Io體系的基類文件流的使用(FileInputStream/FileReader ,FileOutputStream/FileWriter)
前面說過InputStream和Reader都是抽象類,本身不能創建實例,但它們分別有一個用於讀取文件的輸入流:FileInputStream和FileReader,它們都是節點流——會直接和指定文件關聯。下面程序示範使用FileInputStream和FileReader。
使用FileInputStream讀取文件:
public class MyClass {
public static void main(String[] args)throws IOException{
FileInputStream fis=null;
try {
//創建字節輸入流
fis=new FileInputStream("E:\\learnproject\\Iotest\\lib\\src\\main\\java\\com\\Test.txt");
//創建一個長度爲1024的竹筒
byte[] b=new byte[1024];
//用於保存的實際字節數
int hasRead=0;
//使用循環來重複取水的過程
while((hasRead=fis.read(b))>0){
//取出竹筒中的水滴(字節),將字節數組轉換成字符串進行輸出
System.out.print(new String(b,0,hasRead));
}
}catch (IOException e){
e.printStackTrace();
}finally {
fis.close();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
注:上面程序最後使用了fis.close()來關閉該文件的輸入流,與JDBC編程一樣,程序裏面打開的文件IO資源不屬於內存的資源,垃圾回收機制無法回收該資源,所以應該顯示的關閉打開的IO資源。Java 7改寫了所有的IO資源類,它們都實現了AntoCloseable接口,因此都可以通過自動關閉資源的try語句來關閉這些Io流。
使用FileReader讀取文件:
public class FileReaderTest {
public static void main(String[] args)throws IOException{
FileReader fis=null;
try {
//創建字節輸入流
fis=new FileReader("E:\\learnproject\\Iotest\\lib\\src\\main\\java\\com\\Test.txt");
//創建一個長度爲1024的竹筒
char[] b=new char[1024];
//用於保存的實際字節數
int hasRead=0;
//使用循環來重複取水的過程
while((hasRead=fis.read(b))>0){
//取出竹筒中的水滴(字節),將字節數組轉換成字符串進行輸出
System.out.print(new String(b,0,hasRead));
}
}catch (IOException e){
e.printStackTrace();
}finally {
fis.close();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
可以看出使用FileInputStream和FileReader進行文件的讀寫並沒有什麼區別,只是操作單元不同而且。
FileOutputStream/FileWriter是Io中的文件輸出流,下面介紹這兩個類的用法。
FileOutputStream的用法:
public class FileOutputStreamTest {
public static void main(String[] args)throws IOException {
FileInputStream fis=null;
FileOutputStream fos=null;
try {
//創建字節輸入流
fis=new FileInputStream("E:\\learnproject\\Iotest\\lib\\src\\main\\java\\com\\Test.txt");
//創建字節輸出流
fos=new FileOutputStream("E:\\learnproject\\Iotest\\lib\\src\\main\\java\\com\\newTest.txt");
byte[] b=new byte[1024];
int hasRead=0;
//循環從輸入流中取出數據
while((hasRead=fis.read(b))>0){
//每讀取一次,即寫入文件輸入流,讀了多少,就寫多少。
fos.write(b,0,hasRead);
}
}catch (IOException e){
e.printStackTrace();
}finally {
fis.close();
fos.close();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
運行程序可以看到輸出流指定的目錄下多了一個文件:newTest.txt, 該文件的內容和Test.txt文件的內容完全相同。FileWriter的使用方式和FileOutputStream基本類似,這裏就帶過。
注: 使用java的io流執行輸出時,不要忘記關閉輸出流,關閉輸出流除了可以保證流的物理資源被回收之外,可能還可以將輸出流緩衝區中的數據flush到物理節點中裏(因爲在執行close()方法之前,自動執行輸出流的flush()方法)。java很多輸出流默認都提供了緩存功能,其實我們沒有必要刻意去記憶哪些流有緩存功能,哪些流沒有,只有正常關閉所有的輸出流即可保證程序正常。
緩衝流的使用(BufferedInputStream/BufferedReader, BufferedOutputStream/BufferedWriter):
下面介紹字節緩存流的用法(字符緩存流的用法和字節緩存流一致就不介紹了):
public class BufferedStreamTest {
public static void main(String[] args)throws IOException {
FileInputStream fis=null;
FileOutputStream fos=null;
BufferedInputStream bis=null;
BufferedOutputStream bos=null;
try {
//創建字節輸入流
fis=new FileInputStream("E:\\learnproject\\Iotest\\lib\\src\\main\\java\\com\\Test.txt");
//創建字節輸出流
fos=new FileOutputStream("E:\\learnproject\\Iotest\\lib\\src\\main\\java\\com\\newTest.txt");
//創建字節緩存輸入流
bis=new BufferedInputStream(fis);
//創建字節緩存輸出流
bos=new BufferedOutputStream(fos);
byte[] b=new byte[1024];
int hasRead=0;
//循環從緩存流中讀取數據
while((hasRead=bis.read(b))>0){
//向緩存流中寫入數據,讀取多少寫入多少
bos.write(b,0,hasRead);
}
}catch (IOException e){
e.printStackTrace();
}finally {
bis.close();
bos.close();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
可以看到使用字節緩存流讀取和寫入數據的方式和文件流(FileInputStream,FileOutputStream)並沒有什麼不同,只是把處理流套接到文件流上進行讀寫。緩存流的原理下節介紹。
上面代碼中我們使用了緩存流和文件流,但是我們只關閉了緩存流。這個需要注意一下,當我們使用處理流套接到節點流上的使用的時候,只需要關閉最上層的處理就可以了。java會自動幫我們關閉下層的節點流。
2.3 轉換流的使用(InputStreamReader/OutputStreamWriter):
下面以獲取鍵盤輸入爲例來介紹轉換流的用法。java使用System.in代表輸入。即鍵盤輸入,但這個標準輸入流是InputStream類的實例,使用不太方便,而且鍵盤輸入內容都是文本內容,所以可以使用InputStreamReader將其包裝成BufferedReader,利用BufferedReader的readLine()方法可以一次讀取一行內容,如下代碼所示:
public class InputStreamReaderTest {
public static void main(String[] args)throws IOException {
try {
// 將System.in對象轉化爲Reader對象
InputStreamReader reader=new InputStreamReader(System.in);
//將普通的Reader包裝成BufferedReader
BufferedReader bufferedReader=new BufferedReader(reader);
String buffer=null;
while ((buffer=bufferedReader.readLine())!=null){
// 如果讀取到的字符串爲“exit”,則程序退出
if(buffer.equals("exit")){
System.exit(1);
}
//打印讀取的內容
System.out.print("輸入內容:"+buffer);
}
}catch (IOException e){
e.printStackTrace();
}finally {
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
上面程序將System.in包裝成BufferedReader,BufferedReader流具有緩存功能,它可以一次讀取一行文本——以換行符爲標誌,如果它沒有讀到換行符,則程序堵塞。等到讀到換行符爲止。運行上面程序可以發現這個特徵,當我們在控制檯執行輸入時,只有按下回車鍵,程序纔會打印出剛剛輸入的內容。
2.4 對象流的使用(ObjectInputStream/ObjectOutputStream)的使用:
寫入對象:
public static void writeObject(){
OutputStream outputStream=null;
BufferedOutputStream buf=null;
ObjectOutputStream obj=null;
try {
//序列化文件輸出流
outputStream=new FileOutputStream("E:\\learnproject\\Iotest\\lib\\src\\main\\java\\com\\myfile.tmp");
//構建緩衝流
buf=new BufferedOutputStream(outputStream);
//構建字符輸出的對象流
obj=new ObjectOutputStream(buf);
//序列化數據寫入
obj.writeObject(new Person("A", 21));//Person對象
//關閉流
obj.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
讀取對象:
/**
* 讀取對象
*/
public static void readObject() throws IOException {
try {
InputStream inputStream=new FileInputStream("E:\\learnproject\\Iotest\\lib\\src\\main\\java\\com\\myfile.tmp");
//構建緩衝流
BufferedInputStream buf=new BufferedInputStream(inputStream);
//構建字符輸入的對象流
ObjectInputStream obj=new ObjectInputStream(buf);
Person tempPerson=(Person)obj.readObject();
System.out.println("Person對象爲:"+tempPerson);
//關閉流
obj.close();
buf.close();
inputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
使用對象流的一些注意事項
1.讀取順序和寫入順序一定要一致,不然會讀取出錯。
2.在對象屬性前面加transient關鍵字,則該對象的屬性不會被序列化。
4.何爲NIO,和傳統Io有何區別?
我們使用InputStream從輸入流中讀取數據時,如果沒有讀取到有效的數據,程序將在此處阻塞該線程的執行。其實傳統的輸入裏和輸出流都是阻塞式的進行輸入和輸出。 不僅如此,傳統的輸入流、輸出流都是通過字節的移動來處理的(即使我們不直接處理字節流,但底層實現還是依賴於字節處理),也就是說,面向流的輸入和輸出一次只能處理一個字節,因此面向流的輸入和輸出系統效率通常不高。
從JDk1.4開始,java提供了一系列改進的輸入和輸出處理的新功能,這些功能被統稱爲新IO(NIO)。新增了許多用於處理輸入和輸出的類,這些類都被放在java.nio包及其子包下,並且對原io的很多類都以NIO爲基礎進行了改寫。新增了滿足NIO的功能。
NIO採用了內存映射對象的方式來處理輸入和輸出,NIO將文件或者文件的一塊區域映射到內存中,這樣就可以像訪問內存一樣來訪問文件了。通過這種方式來進行輸入/輸出比傳統的輸入和輸出要快的多。
JDk1.4使用NIO改寫了傳統Io後,傳統Io的讀寫速度和NIO差不了太多。
5.在開發中正確使用Io流
瞭解了java io的整體類結構和每個類的一下特性後,我們可以在開發的過程中根據需要靈活的使用不同的Io流進行開發。下面是我整理2點原則:
- 如果是操作二進制文件那我們就使用字節流,如果操作的是文本文件那我們就使用字符流。
- 儘可能的多使用處理流,這會使我們的代碼更加靈活,複用性更好。