java中I/O操作主要是指使用‘java.io‘包下的內容,進行輸入(Input)、輸出(Output)操作。
輸入:讀取數據
輸出:寫入數據。
流:數據(字符、字節)
根據數據的流向可以分為2類
- 輸入流:數據從其他設備讀取到內存上的流
- 輸出流:數據從內存寫出到其他設備上的流
一切文件數據(圖片,音頻,文檔)都以二進制形式存儲在電腦內,字節即為基本的存儲單元,所以都可以用一個一個的字節表示。即一切皆為字節。
有輸入數據或者輸出數據,便有字節輸入流和字節輸出流
字節輸入流和字節輸出流皆有頂級父類。字節輸入流的頂級父類是java.io包下的InputStream,字節輸出流的頂級父類是java.io包下的OutputStream.
輸入流 | 輸出流 |
InputStream | OutputStream |
這兩個類都是抽象類,其中包含了輸入流或者輸出流的共有的方法。
字節輸出流
輸入流原理:將10進制數據轉換成二進制寫入文件中。當文件打開時會查詢系統的編碼表,將二進制的數據變為字符輸出。
OutputStream抽象類下的方法:
1 */ public void close():關閉此輸出流並釋放與此流相關聯的任何系統資源。 2 public void flush() :刷新此輸出流並強制任何緩沖的輸出字節被寫出。 3 public void write(byte[] b):將 b.length字節從指定的字節數組寫入此輸出流。 4 public void write(byte[] b, int off, int len) :從指定的字節數組寫入 len字節,從偏移量 off開始輸出到此輸出流。 5 public abstract void write(int b):將指定的字節輸出流。*/
OutputStream的子類重寫抽象方法后,可以創建對象調用。最簡單的子類FileOutputStream用於將內存中的數據寫入文件中。
FileOutputStream的構造方法:
- public FileOutputStream(String name):傳入字符串類型的文件路徑
- public FileOutputStream(File file):傳入文件對象
構造方法作用:1.創建FileOutputStream對象 2.將FileOutputStream對象指向構造方法中要寫入的文件
當創建一個流對象時,必須傳入一個文件路徑。該路徑下,如果沒有這個文件,會創建該文件。如果有這個文件,會清空這個文件的數據。
數據寫入文件的步驟:
- 創建FileOutputStream對象
- 調用write()方法,寫入數據
- 調用close()方法,關閉輸出流,釋放系統資源
最簡單地調用write(int b)方法,每次寫入一個字節的內容,int參數為ASCII碼編號。int雖然是4個字節,但是最后只會保留1個字節的內容寫出。
FileOutputStream fileOutputStream = new FileOutputStream("b.txt");//傳入相對路徑
fileOutputStream.write(97);//字符a
fileOutputStream.write(98);//字符b
fileOutputStream.write(99);//字符c
fileOutputStream.close();
在b.txt中的結果是 abc。方法調用很簡單,但是每次輸入一個字節的內容過於低效,利用write(byte[] b)方法可以一次輸入多個字節內容
write(byte[] b)方法,將字節數組中全部內容寫入到文件中,共b.length(數組長度)個字節
byte[] bytes={97, 98, 99, 100}; FileOutputStream fileOutputStream = new FileOutputStream("b.txt");//傳入相對路徑
fileOutputStream.write(bytes);//依次寫入abcd
fileOutputStream.close();
write(byte[] b, int off, int len)方法用於部分寫入字節數組中內容
offset為偏移量,即相對於數組起始位置的偏移量,len為寫入的字節數目
byte[] bytes={97, 98, 99, 100}; FileOutputStream fileOutputStream = new FileOutputStream("b.txt");//傳入相對路徑
fileOutputStream.write(bytes,1,2);//依次寫入bc
fileOutputStream.close();
批量寫入操作可以通過write完成,但是每一次運行都會創建一個FileOutputStream對象,構造方法遇到相同路徑會清空原有數據來進行寫入,無法進行追加書寫。
輸出流中的追加與換行
追加:
在構造方法中加入第二個參數可以解決這個問題。第二個參數為布爾值,如果布爾值為true,則會追加書寫,布爾值為false將會清空原有數據
public FileOutputStream(String name, boolean append) public FileOutputStream(File file, boolean append)
舉例如下:
byte[] bytes={97, 98, 99, 100}; FileOutputStream fileOutputStream = new FileOutputStream("b.txt",true);//傳入相對路徑
fileOutputStream.write(bytes);//根據上一次運行結果,b.txt中的內容為bc,本次追加書寫,結果是bcabcd
fileOutputStream.close();
換行:
換行符:\n 到下一行(newline) 回車符:\r 指針回到每一行開頭(retrun)
根據操作系統指令不同,換行方式不同。
Windows:\r\n
Unix:\n
MacOS:\r
字節輸入流
InputStream抽象類下的方法:
1 /*
2 public void close() :關閉此輸入流並釋放與此流相關聯的任何系統資源。 3 public abstract int read(): 從輸入流讀取數據的下一個字節。 4 public int read(byte[] b): 從輸入流中讀取一些字節數,並將它們存儲到字節數組 b中 。*/
InputStream的子類重寫抽象方法后,可以創建對象調用。最簡單的子類FileInputStream用於將文件中的數據讀入內存當中。
FileInputStream的構造方法:
- public FileInputStream(String name):傳入字符串類型的文件路徑
- public FileInputStream(File file):傳入文件對象
構造方法作用:1. 創建FileInputStream對象 2.將FileInputStream對象指向構造方法中要讀取的目標文件
與FileOutputStream文件輸出流不同的是,FileInputStream文件輸入流的構造方法必須傳入一個實際存在的文件路徑或者文件對象,不然會拋出FileNotFoundException。
讀取文件數據的步驟:
- 創建FileInputStream對象
- 調用read()方法,讀取數據到內存。每一次調用此方法,指針指向下一個字節。
- 調用close()方法,關閉輸入流,釋放系統資源
調用read()方法,每次讀取目標文件一個字節的內容,將數據提升為int類型,讀到文末的時候返回-1
1 byte[] bytes={97, 98, 99, 100}; 2 FileOutputStream fileOutputStream = new FileOutputStream("b.txt");//傳入相對路徑
3 FileInputStream fileInputStream=new FileInputStream("b.txt"); 4 fileOutputStream.write(bytes);//寫入abcd
5 int read = fileInputStream.read();//第一次調用read方法,讀取字節信息提升為int類型返回,指針自動指向下一位
6 System.out.println((char) read);//讀取a
7 read = fileInputStream.read();//第二次調用read方法,讀取字節信息提升為int類型返回,指針自動指向下一位
8 System.out.println((char) read);//讀取b
9 read = fileInputStream.read();//以上類似於上面注釋
10 System.out.println((char) read); 11 read = fileInputStream.read(); 12 System.out.println((char) read); 13 fileOutputStream.close(); 14 fileInputStream.close();
以上代碼演示了read()方法無參調用,可以看出代碼有多次重復,效率低下,可以使用循環來代替。
使用循環代替,就必須明確循環體與判斷的條件。循環條件可以由read方法在讀到文末時候返回-1得出。
int read; while ((read = fileInputStream.read()) != -1) { System.out.println((char) read); }
此處代碼可以用來代替上文中多次重復的代碼,因為read方法中會自動將指針指向下一位的特殊性質,每次循環只能調用一次,所以需要定義一個變量來接收read方法的結果,便於循環體中的打印。循環條件中
(read = fileInputStream.read()) != -1同時完成了方法的一次調用和判斷。
使用字節數組讀取文件數據,調用方法read(byte[] b)。
1 byte[] bytes = {97, 98, 99, 100}; 2 FileOutputStream fileOutputStream = new FileOutputStream("b.txt");//傳入相對路徑
3 FileInputStream fileInputStream = new FileInputStream("b.txt"); 4 fileOutputStream.write(bytes);//寫入abcd
5 byte[] b=new byte[3];//創建新字節數組作為存放讀取出來的數據的容器
6 int len; 7 while ((len=fileInputStream.read(b))!=-1){ 8 System.out.println(new String(b)); 9 } 10 fileOutputStream.close(); 11 fileInputStream.close(); 12 }
此處有兩個注意點:
- 新建數組b用於存放讀取出來的字節信息,一次讀取3(b.length)個字節的數據,並依次存入其中。每一次調用方法指針向下移動3(b.length)位
- 整數len用於表示每一次讀取的有效位數。第一次讀取有效位數3個,第二次有效位數僅1個。
但是代碼會存在一個問題:代碼顯然循環次數2次,第一次循環可以得到正常結果abc,第二循環便會因為read遇到文末出現錯誤結果dbc

這個問題產生的原因是賦值操作時,上一次讀取的數據沒有被完全替換。
為了解決這個問題可以改變String參數的構造方法解決,將上面while循環內部代碼修改為如下代碼
1 while ((len=fileInputStream.read(b))!=-1){ 2 System.out.println(new String(b,0,len)); 3 }
String構造方法變為3個參數,len為有效讀取字節數,0是b數組偏移量,構造方法是將b數組從0索引開始的len個索引內容放入字符串。第一次l循環len為3放入abc,第二次循環len為1放入d,代碼正確。
只使用字節輸入英文和一些基本符號是沒有問題的,但是想用字節流輸入或讀取中文時就會有一些問題,因為一個中文字符占用多個字節。所以java還有以字符為單位進行輸入和輸出的流,稱為字符輸入流和字符輸出流。
字符輸入流(Reader)
java.io.Reader是讀取字符流的父類,里面存放了一些共用的讀取方法。
/*public void close() :關閉此流並釋放與此流相關聯的任何系統資源。 public int read(): 從輸入流讀取一個字符。 public int read(char[] cbuf): 從輸入流中讀取一些字符,並將它們存儲到字符數組 cbuf中 */
FileReader 是Reader的子類,用於讀取文件中的字符,構造時使用系統默認的字符編碼和默認字節緩沖區。
小貼士:
1. 字符編碼:字節與字符的對應規則。Windows系統的中文編碼默認是GBK編碼表。
idea中UTF-8
2. 字節緩沖區:一個字節數組,用來臨時存儲字節數據。
FileReader構造方法:
- FileReader(File file): 創建一個新的 FileReader ,給定要讀取的File對象。
- FileReader(String fileName): 創建一個新的 FileReader ,給定要讀取的文件的名稱
類似於FileInputStream,FileReader在構造時必須傳入有效的文件名或者文件對象
read方法讀取字符:基本方法和之前講的FileInputStream一致
FileReader fe=new FileReader("b.txt"); int len; while((len=fe.read())!=-1){ System.out.println((char) len);/將得到的字符結果打印出來 }
read方法每次讀取一個字符,自動將指針指向下一位,讀到文末時返回-1。
利用字符數組讀取字符調用方法read(char[] cubf)來完成
FileReader fe=new FileReader("b.txt"); char[] chars=new char[3]; int len; while((len=fe.read(chars))!=-1){ System.out.println(new String(chars)); }
len仍為有效讀取字節個數
新建數組chars用來存放讀取出來的字符數據,通過賦值操作獲取。與剛才所講的FileInputStream類似,以上代碼會出現上一次循環結束后數組中的數據沒有被完全替代的問題。
與之前FileInputStream處理的方法一樣,利用String的構造方法來規避這個問題
1 while((len=fe.read(chars))!=-1){ 2 System.out.println(new String(chars,0,len)); 3 }
String構造方法變為3個參數,len為有效讀取字節數,0是b數組偏移量,構造方法是將b數組從0索引開始的len個索引內容放入字符串。第一次l循環len為3放入甲乙丙,第二次循環len為1放入丁,代碼正確。
字符輸入流(Writer)
java.io.Writer抽象類是所有用字符流寫出數據的類的父類其中定義了帶有共性的方法。
*/ void write(int c) 寫入單個字符。 void write(char[] cbuf) 寫入字符數組。 abstract void write(char[] cbuf, int off, int len) 寫入字符數組的某一部分,off數組的開始索引,len寫的字符個數。 void write(String str) 寫入字符串。 void write(String str, int off, int len) 寫入字符串的某一部分,off字符串的開始索引,len寫的字符個數。 void flush() 刷新該流的緩沖。 void close() 關閉此流,但要先刷新它。*/
java.io.FileWriter 類是寫出字符到文件的便利類。構造時使用系統默認的字符編碼和默認字節緩沖區
FileWriter構造方法:
- FileWriter(File file): 創建一個新的 FileWriter,給定要讀取的File對象。
- FileWriter(String fileName): 創建一個新的 FileWriter,給定要讀取的文件的名稱。
write()方法無參調用,可以寫入一個字符的信息,與FileOutputStream類似,但是與字節輸出流不同的是,字符輸出流必須在write()方法之后使用flush()方法將緩沖區內的數據輸入到文件中。
FileWriter fw=new FileWriter("b.txt"); fw.write('你'); fw.write('好'); fw.flush();
fw.close();
當調用close方法時,flush方法會自動調用,將緩沖區內數據寫入文件。當我們需要既寫出數據到文件中,又要繼續使用(不能關閉)流對象時要用到flush()方法。
利用數組可以批量寫出字符到文件中,調用write(char[] cbuf)方法
FileWriter fw=new FileWriter("b.txt"); char[] chars = "丹尼格林".toCharArray(); fw.write(chars); fw.close();//不需要繼續使用流對象,直接關閉自動調用flush方法即可
write(char[] cbuf,int off,int len )可以從數組第off個索引開始寫出之后的len個索引的內容,與FileOutputStream類似。
FileWriter fw=new FileWriter("b.txt"); char[] chars = "丹尼格林".toCharArray(); fw.write(chars,2,1);//寫入結果為 ‘格’ fw.close();
字節、字符輸入流和輸出流會在創建變量和調用方法時拋出異常,需要處理之后才可以正常運行。
使用try catch finally處理異常(以字符輸出流舉例)
1 FileWriter fw = null;//提升變量作用域並設置一個默認值初始化變量 2 try {//創建對象,調用write方法會拋出異常(IOException),必須處理了異常才能正常運行 3 fw = new FileWriter("b.txt"); 4 char[] chars = "丹尼格林".toCharArray(); 5 fw.write(chars, 2, 1);//寫入結果為 ‘格’ 6 } catch (IOException e) { 7 e.printStackTrace(); 8 } finally { 9 if (fw != null) {//判斷對象是否創建成功 10 try { 11 fw.close();//close方法也會拋出異常,也需要處理 12 } catch (IOException e) { 13 e.printStackTrace(); 14 } 15 16 } 17 18 19 }
finally中必須執行的代碼是釋放資源代碼,保證程序運行的效率。創建對象和調用寫出方法都可能產生異常,放入try代碼塊中。
JDK7中新特性,可以在try后面加入一個括號(),在括號里面定義流對象(可以定義多個對象,相互之間用分號隔開),流對象的作用域僅在try代碼塊中,try代碼塊執行完自動關閉釋放,不用寫finally
1 try (FileWriter fw = new FileWriter("b.txt")) { 2 char[] chars = "丹尼格林".toCharArray(); 3 fw.write(chars, 2, 1);//寫入結果為 ‘格’ 4 } catch (IOException e) { 5 e.printStackTrace(); 6 }
JDK9中新特性,可以在try前面定義對象,然后直接在try后面的括號中使用對象的名稱,但是在try之前定義對象仍然會拋出異常,需要拋出。
1 FileWriter fw = new FileWriter("b.txt"); 2 try (fw) { 3 char[] chars = "丹尼格林".toCharArray(); 4 fw.write(chars, 2, 1);//寫入結果為 ‘格’ 5 } catch (IOException e) { 6 e.printStackTrace(); 7 }