Java IO流基礎
什麼是IO流
Java中的IO
瞭解什麼是IO流之前,要先知道什麼是IO。IO,就是in和out(即輸入和輸出),指應用程式和外部裝置之間的資料傳遞,常見的外部裝置包括檔案、管道、網路連線等。
流的概念與特性
概念
Java 中是通過流處理IO 的。流(Stream),是一個抽象的概念,是指一連串的資料(字元或位元組),是以先進先出的方式傳送資訊的通道。當程式需要讀取資料時,就會開啟一個通向資料來源的流,同理,當程式需要寫入資料時,就會開啟一個通向目的地的流。
特性
關於流的特性有以下幾點:
- 先進先出:最先寫入輸出流的資料最先被輸入流讀取到。
- 順序存取:可以一個接一個地往流中寫入一串位元組,讀出時也將按寫入順序讀取一串位元組,不能隨機訪問中間的資料。(
RandomAccessFile
- 只讀或只寫:每個流只能是輸入流或輸出流的一種,不能同時具備兩個功能,輸入流只能進行讀操作,對輸出流只能進行寫操作。在一個數據傳輸通道中,如果既要寫入資料,又要讀取資料,則要分別提供兩個流。
IO流的分類
IO分類圖如下(詳細內容可檢視API文件):
IO流主要的分類方式有以下3種:
- 按資料流的方向:輸入流、輸出流
- 按處理資料單位:位元組流、字元流
- 按功能:節點流、處理流
輸入流與輸出流
這裡要注意的是:輸入與輸出是相對於應用程式而言的,比如檔案讀寫,讀取檔案是輸入流,寫檔案是輸出流(即流向記憶體是輸入流,流出記憶體的輸出流),不要搞反,不是很清楚可以看下圖。
位元組流與字元流
Unicode 編碼中,一個英文為1個位元組,一箇中文為2個位元組。而在UTF-8編碼中,一箇中文字元是3個位元組。如果使用位元組流處理中文,如果一次讀寫一個字元對應的位元組數就不會有問題,一旦將一個字元對應的位元組分裂開來,就會出現亂碼了。為了更方便地處理中文這些字元,Java就推出了字元流。
位元組流和字元流的用法幾乎完全一樣,區別在於位元組流和字元流所操作的資料單元不同,位元組流操作的單元是資料單元是8位的位元組(即1個位元組),字元流操作的是資料單元為16位的字元(即1個字元或2個位元組)。
位元組流和字元流的其他區別:
- 位元組流一般用來處理影象、視訊、音訊、PPT、Word等型別的檔案。字元流一般用於處理純文字型別的檔案,如TXT檔案等,但不能處理影象視訊等非文字檔案。簡單說就是就是:位元組流可以處理一切檔案,而字元流只能處理純文字檔案。
- 位元組流本身沒有緩衝區,緩衝位元組流相對於位元組流,效率提升非常高。而字元流本身就帶有緩衝區,緩衝字元流相對於字元流效率提升就不是那麼大了。
節點流和處理流
- 節點流:直接操作資料讀寫的流類,比如
FileInputStream
- 處理流:對一個已存在的流的連結和封裝,通過對資料進行處理為程式提供功能強大、靈活的讀寫功能,例如
BufferedInputStream
(緩衝位元組流)
處理流和節點流應用了Java的裝飾者設計模式。下圖就很形象地描繪了節點流和處理流,處理流是對節點流的封裝,最終的資料處理還是由節點流完成的。
緩衝流:
在諸多處理流中,有一個非常重要,那就是緩衝流。程式與磁碟的互動相對於記憶體運算是很慢的,容易成為程式的效能瓶頸。減少程式與磁碟的互動,是提升程式效率一種有效手段。緩衝流,就應用這種思路:普通流每次讀寫一個位元組,而緩衝流在記憶體中設定一個快取區,緩衝區先儲存足夠的待操作資料後,再與記憶體或磁碟進行互動。這樣,在總資料量不變的情況下,通過提高每次互動的資料量,減少了互動次數。需要注意的是,緩衝流效率不一定高,在某些情形下,緩衝流效率反而更低,具體請見IO流效率對比。
位元組流
一切皆為位元組
一切檔案資料(文字、圖片、視訊等)在儲存時,都是以二進位制數字的形式儲存,都一個一個的位元組,同理傳輸時一樣如此,所以位元組流可以傳輸任意檔案資料。在操作流的時候要時刻明確,無論使用什麼樣的流物件,底層傳輸的始終為二進位制資料。位元組輸出流【OutputStream】
java.io.OutputStream 抽象類是表示位元組輸出流的所有類的超類,將指定的位元組資訊寫出到目的地。它定義了位元組輸出流的基本共性功能方法如下:- public void close() :關閉此輸出流並釋放與此流相關聯的任何系統資源。
- public void flush() :重新整理此輸出流並強制任何緩衝的輸出位元組被寫出。
- public void write(byte[] b) :將 b.length位元組從指定的位元組陣列寫入此輸出流。
- public void write(byte[] b, int off, int len) :從指定的位元組陣列寫入 len(長度)位元組,從偏移量 off(索引)開始輸出到此輸出流。
- public abstract void write(int b) :將指定的位元組輸出流。
小貼士:
- close()方法,當完成流的操作時,必須呼叫此方法,釋放系統資源。
- 在建立位元組輸出流物件和使用write()方法寫出資料時,會分別丟擲FileNotFoundException 和IOException異常,要注意的是,IOException異常是FileNotFoundException異常的父類,所以這裡我們直接丟擲IOException異常就可以了。
FileOutputStream類
OutputStream 有很多子類,我們從最常見的一個類FileOutputStream類開始。java.io.FileOutputStream 類是檔案輸出流,用於將資料寫出到檔案。
構造方法
- public FileOutputStream(File file) :建立一個向指定File物件表示的檔案中寫入資料的檔案輸出流。
- public FileOutputStream(String name) : 建立一個向具有指定名稱的檔案中寫入資料的輸出檔案流。
引數 File file:目的地是一個檔案 String name:目的地是一個檔案的路徑
注意:當你建立一個流物件時,必須傳入一個檔案路徑。該路徑下,如果沒有這個檔案,會建立該檔案。如果有這個檔案,會清空這個檔案的資料。作用:
- 建立一個Fileoutputstream物件
- 會根據構造方法中傳遞的檔案/檔案路徑,建立一個空的檔案
- 會把FiLeOutputStream物件指向建立好的檔案
構造方法程式碼演示:
public class FileOutputStreamConstructor throws IOException { public static void main(String[] args) { // 使用File物件建立流物件 File file = new File("a.txt"); FileOutputStream fos = new FileOutputStream(file); // 使用檔名稱建立流物件 FileOutputStream fos = new FileOutputStream("b.txt"); } }
寫出位元組資料:
原理:(記憶體-->硬碟)
java程式-->JVM(java虛擬機器 )-->OS(作業系統)-->OS呼叫寫資料的方法-->把資料寫入到檔案中
方法:
- 寫出位元組: write(int b) 方法,每次可以寫出一個位元組資料,程式碼演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件 FileOutputStream fos = new FileOutputStream("fos.txt"); // 寫出資料 fos.write(97);//寫出第1個位元組 fos.write(98);//寫出第2個位元組 fos.write(99);//寫出第3個位元組 // 關閉資源 fos.close(); } }
輸出結果:
abc
- 寫出位元組陣列: write(byte[] b) ,每次可以寫出陣列中的資料,程式碼演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件 FileOutputStream fos = new FileOutputStream("fos.txt"); // 字串轉換為位元組陣列 byte[] b = "張三李四王五".getBytes(); // 寫出位元組陣列資料 fos.write(b); // 關閉資源 fos.close(); } } 輸出結果: 張三李四王五
- 寫出指定長度位元組陣列: write(byte[] b, int off, int len) ,每次寫出從off索引開始,len個位元組,程式碼演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件 FileOutputStream fos = new FileOutputStream("fos.txt"); // 字串轉換為位元組陣列 byte[] b = "abcde".getBytes(); // 寫出從索引1開始,2個位元組。索引1是b,兩個位元組,也就是bc。 fos.write(b,1,2); // 關閉資源 fos.close(); } } 輸出結果: bc
資料追加續寫
每次程式執行,建立輸出流物件,都會清空目標檔案中的資料。那麼如果要保留目標檔案中資料,繼續新增新資料,就要使用如下方法:- public FileOutputStream(File file, boolean append) : 建立檔案輸出流以寫入由指定的 File物件表示的檔案。
- public FileOutputStream(String name, boolean append) : 建立檔案輸出流以指定的名稱寫入檔案。
程式碼使用演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件並設定資料追加續寫 FileOutputStream fos = new FileOutputStream("fos.txt",true); // 字串轉換為位元組陣列 byte[] b = "abcde".getBytes(); // 寫出位元組陣列資料 fos.write(b); // 關閉資源 fos.close(); } } 檔案操作前: bc 檔案操作後: bcabcde
寫出換行
Windows系統裡,換行符號是 \r\n 。回車符 \r 和換行符 \n : 回車符:回到一行的開頭(return) 換行符:下一行(newline)。 系統中的換行:- Windows系統裡,每行結尾是 回車+換行 ,即 \r\n ;
- Unix系統裡,每行結尾只有 換行 ,即 \n ;
- Mac系統裡,每行結尾是 回車 ,即 \r 。
程式碼使用演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件 FileOutputStream fos = new FileOutputStream("fos.txt"); // 定義位元組陣列 byte[] words = {97,98,99,100,101}; // 遍歷陣列 for (int i = 0; i < words.length; i++) { // 寫出一個位元組 fos.write(words[i]); // 寫出一個換行, 換行符號轉成陣列寫出 fos.write("\r\n".getBytes()); } // 關閉資源 fos.close(); } } 輸出結果: a b c d e
位元組輸入流【InputStream】
java.io.InputStream 抽象類是表示位元組輸入流的所有類的超類,可以讀取位元組資訊到記憶體中。它定義了位元組輸入流的基本共性功能方法。- public void close() :關閉此輸入流並釋放與此流相關聯的任何系統資源。
- public abstract int read() : 從輸入流讀取資料的下一個位元組。
- public int read(byte[] b) : 從輸入流中讀取一些位元組數,並將它們儲存到位元組陣列 b中 。
小貼士:
- close方法,當完成流的操作時,必須呼叫此方法,釋放系統資源。
- 在建立位元組輸入流物件和使用read()方法讀取資料時,會分別丟擲FileNotFoundException 和IOException異常,要注意的是,IOException異常是FileNotFoundException異常的父類,所以這裡我們直接丟擲IOException異常就可以了。
FileInputStream類
java.io.FileInputStream 類是檔案輸入流,從檔案中讀取位元組。構造方法
- FileInputStream(File file) : 通過開啟與實際檔案的連線來建立一個 FileInputStream ,該檔案由檔案系統中的 File物件 fifile命名。
- FileInputStream(String name) : 通過開啟與實際檔案的連線來建立一個 FileInputStream ,該檔案由檔案系統中的路徑名 name命名。
引數:讀取檔案的資料來源 File file:檔案 String name:檔案的路徑
注意:當你建立一個流物件時,必須傳入一個檔案路徑。該路徑下,如果沒有該檔案,會丟擲 FileNotFoundException 。構造方法程式碼演示:
public class FileInputStreamConstructor throws IOException{ public static void main(String[] args) { // 使用File物件建立流物件 File file = new File("a.txt"); FileInputStream fis = new FileInputStream(file); // 使用檔名稱建立流物件 FileInputStream fis = new FileInputStream("b.txt"); } }
作用:
- 會建立一個FileInputStream物件
- 會把FileInputstream物件指向構造方法中要讀取的檔案
讀取位元組資料
原理:(硬碟-->記憶體)
java程式-->JVM(java虛擬機器 )-->OS(作業系統)-->OS呼叫讀取資料的方法-->從檔案中讀取資料
方法:
- 讀取位元組: read():每次可以讀取一個位元組的資料,返回值int型別,讀取到檔案末尾,返回 -1 ,程式碼演示:
public class FISRead { public static void main(String[] args) throws IOException{ // 使用檔名稱建立流物件,檔案內容為abcde FileInputStream fis = new FileInputStream("read.txt"); // 讀取資料,返回一個位元組 int read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); // 讀取到末尾,返回‐1 read = fis.read(); System.out.println( read); // 關閉資源 fis.close(); } } 輸出結果: a b c d e ‐1
迴圈改進讀取方式,程式碼演示:
public class FISRead { public static void main(String[] args) throws IOException{ // 使用檔名稱建立流物件,檔案內容為abcde FileInputStream fis = new FileInputStream("read.txt"); // 定義變數,儲存資料 int b ; // 迴圈讀取 while ((b = fis.read())!=‐1) { System.out.println((char)b); } // 關閉資源 fis.close(); } } 輸出結果: a b c d e
小貼士:
- 雖然讀取了一個位元組,但是會自動提升為int型別。
- 流操作完畢後,必須釋放系統資源,呼叫close方法,千萬記得。
- 使用位元組陣列讀取: read(byte[] b) ,每次讀取b的長度個位元組到陣列中,返回讀取到的有效位元組個數,讀取到末尾時,返回 -1 ,程式碼演示:
public class FISRead { public static void main(String[] args) throws IOException{ // 使用檔名稱建立流物件,檔案內容為abcde FileInputStream fis = new FileInputStream("read.txt");// 定義變數,作為有效個數 int len ; // 定義位元組陣列,作為裝位元組資料的容器 byte[] b = new byte[2]; // 迴圈讀取 while (( len= fis.read(b))!=‐1) { // 每次讀取後,把陣列變成字串列印 System.out.println(new String(b)); } // 關閉資源 fis.close(); } } 輸出結果: ab cd ed錯誤資料 d ,是由於最後一次讀取時,只讀取一個位元組 e ,陣列中,上次讀取的資料沒有被完全替換,所以要通過 len ,獲取有效的位元組,程式碼演示:
public class FISRead { public static void main(String[] args) throws IOException{ // 使用檔名稱建立流物件,檔案內容為abcde FileInputStream fis = new FileInputStream("read.txt"); // 定義變數,作為有效個數 int len ; // 定義位元組陣列,作為裝位元組資料的容器 byte[] b = new byte[2]; // 迴圈讀取 while (( len= fis.read(b))!=‐1) { // 每次讀取後,把陣列的有效位元組部分,變成字串列印 System.out.println(new String(b,0,len));// len 每次讀取的有效位元組個數 } // 關閉資源 fis.close(); } } 輸出結果: ab cd e
小貼士:
使用陣列讀取,每次讀取多個位元組,減少了系統間的IO操作次數,從而提高了讀寫的效率,建議開發中使用。使用位元組陣列讀取資料明確兩件事情:
- 方法的引數byte[]的作用?
起到緩衝作用,儲存每次讀取到的多個位元組
陣列的長度一般定義為1024(1kb)或者1024的整數倍 - 方法的返回值int是什麼?
每次讀取的有效位元組個數
位元組流案例
說明:圖片複製,從已有的檔案中讀取位元組,將該位元組寫出到另一個檔案中案例實現:
public class Copy { public static void main(String[] args) throws IOException { // 1.建立流物件 // 1.1 指定資料來源 FileInputStream fis = new FileInputStream("D:\\test.jpg"); // 1.2 指定目的地 FileOutputStream fos = new FileOutputStream("test_copy.jpg"); // 2.讀寫資料 // 2.1 定義陣列 byte[] b = new byte[1024]; // 2.2 定義長度 int len; // 2.3 迴圈讀取 while ((len = fis.read(b))!=‐1) { // 2.4 寫出資料 fos.write(b, 0 , len); } // 3.關閉資源 fos.close(); fis.close(); } }
小貼士:
- 流的關閉原則:先開後關,後開先關。
字元流
當使用位元組流讀取文字檔案時,可能會有一個小問題。就是遇到中文字元時,可能不會顯示完整的字元,那是因為一箇中文字元可能佔用多個位元組儲存。所以Java提供一些字元流類,以字元為單位讀寫資料,專門用於處理文字檔案。字元輸出流【Writer】
java.io.Writer 抽象類是表示用於寫出字元流的所有類的超類,將指定的字元資訊寫出到目的地。它定義了位元組輸出流的基本共性功能方法。- void write(int c) 寫入單個字元。
- void write(char[] cbuf) 寫入字元陣列。
- abstract void write(char[] cbuf, int off, int len) 寫入字元陣列的某一部分,offff陣列的開始索引,len寫的字元個數。
- void write(String str) 寫入字串。
- void write(String str, int off, int len) 寫入字串的某一部分,offff字串的開始索引,len寫的字元個數。
- void flush() 重新整理該流的緩衝。
- void close() 關閉此流,但要先重新整理它。
FileWriter類
java.io.FileWriter 類是寫出字元到檔案的便利類。構造時使用系統預設的字元編碼和預設位元組緩衝區。構造方法
- FileWriter(File file) : 建立一個新的 FileWriter,給定要寫入資料的File物件。
- FileWriter(String fileName) : 建立一個新的 FileWriter,給定要寫入資料的檔案的名稱。
構造舉例,程式碼如下:
public class FileWriterConstructor { public static void main(String[] args) throws IOException { // 使用File物件建立流物件 File file = new File("a.txt"); FileWriter fw = new FileWriter(file); // 使用檔名稱建立流物件 FileWriter fw = new FileWriter("b.txt"); } }
寫出字元資料
- 寫出字元: write(int b) 方法,每次可以寫出一個字元資料,程式碼使用演示:
public class FWWrite { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件 FileWriter fw = new FileWriter("fw.txt"); // 寫出資料 fw.write(97); // 寫出第1個字元 fw.write('b'); // 寫出第2個字元 fw.write('c'); // 寫出第3個字元 fw.write(30000); // 寫出第4個字元,中文編碼表中30000對應一個漢字。 /*【注意】關閉資源時,與FileOutputStream不同。 如果不關閉,資料只是儲存到緩衝區,並未儲存到檔案。 */ fw.close(); } } 輸出結果: abc田
小貼士:
- 雖然引數為int型別四個位元組,但是隻會保留一個字元的資訊寫出。
- 如果未呼叫close方法,資料只是儲存到了緩衝區,並未寫出到檔案中。
- 寫出字元陣列 : write(char[] cbuf) 和 write(char[] cbuf, int off, int len) ,每次可以寫出字元陣列中的資料,用法類似FileOutputStream,程式碼使用演示:
public class FWWrite { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件 FileWriter fw = new FileWriter("fw.txt"); // 字串轉換為位元組陣列 char[] chars = "張三李四王五".toCharArray(); // 寫出字元陣列 fw.write(chars); // 張三李四王五 // 寫出從索引2開始,2個位元組。索引2是'李',兩個位元組,也就是'李四'。 fw.write(b,2,2); // 李四 // 關閉資源 fos.close(); } }
- 寫出字串: write(String str) 和 write(String str, int off, int len) ,每次可以寫出字串中的資料,更為方便,程式碼使用演示:
public class FWWrite { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件 FileWriter fw = new FileWriter("fw.txt"); // 字串 String msg = "張三李四王五"; // 寫出字元陣列 fw.write(msg); //張三李四王五 // 寫出從索引2開始,2個位元組。索引2是'李',兩個位元組,也就是'李四'。 fw.write(msg,2,2); // 李四 //關閉資源 fos.close(); } }
- 續寫和換行:操作類似於FileOutputStream。
public class FWWrite { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件,可以續寫資料 FileWriter fw = new FileWriter("fw.txt",true); // 寫出字串 fw.write("張三"); // 寫出換行 fw.write("\r\n"); // 寫出字串 fw.write("李四"); // 關閉資源 fw.close(); } } 輸出結果: 張三 李四
關閉和重新整理
因為內建緩衝區的原因,如果不關閉輸出流,無法寫出字元到檔案中。但是關閉的流物件,是無法繼續寫出資料的。如果既想寫出資料,又想繼續使用流,就需要 flush 方法了。- flush :重新整理緩衝區,流物件可以繼續使用。
- close :先重新整理緩衝區,然後通知系統釋放資源。流物件不可以再被使用了。
程式碼使用演示:
public class FWWrite { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件 FileWriter fw = new FileWriter("fw.txt"); // 寫出資料,通過flush fw.write('刷'); // 寫出第1個字元 fw.flush(); fw.write('新'); // 繼續寫出第2個字元,寫出成功 fw.flush(); // 寫出資料,通過close fw.write('關'); // 寫出第1個字元 fw.close(); fw.write('閉'); // 繼續寫出第2個字元,【報錯】java.io.IOException: Stream closed fw.close(); } }
小貼士:
即便是flush方法寫出了資料,操作的最後還是要呼叫close方法,釋放系統資源。字元輸入流【Reader】
java.io.Reader 抽象類是表示用於讀取字元流的所有類的超類,可以讀取字元資訊到記憶體中。它定義了字元輸入流的基本共性功能方法。- public void close() :關閉此流並釋放與此流相關聯的任何系統資源。
- public int read() : 從輸入流讀取一個字元。
- public int read(char[] cbuf) : 從輸入流中讀取一些字元,並將它們儲存到字元陣列 cbuf中 。
FileReader類
java.io.FileReader 類是讀取字元檔案的便利類。構造時使用系統預設的字元編碼和預設位元組緩衝區。小貼士:
- 字元編碼:位元組與字元的對應規則。Windows系統的中文編碼預設是GBK編碼表。idea中是UTF-8。
- 位元組緩衝區:一個位元組陣列,用來臨時儲存位元組資料。
構造方法
- FileReader(File file) : 建立一個新的 FileReader ,給定要讀取的File物件。
- FileReader(String fileName) : 建立一個新的 FileReader ,給定要讀取的檔案的名稱。
構造舉例,程式碼如下:
public class FileReaderConstructor throws IOException{ public static void main(String[] args) { // 使用File物件建立流物件 File file = new File("a.txt"); FileReader fr = new FileReader(file); // 使用檔名稱建立流物件 FileReader fr = new FileReader("b.txt"); } }
讀取字元資料
- 讀取字元: read ()方法,每次可以讀取一個字元的資料,提升為int型別,讀取到檔案末尾,返回 -1 ,迴圈讀取,程式碼使用演示:
public class FRRead { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件 ,檔案內容:張三李四王五 FileReader fr = new FileReader("read.txt"); // 定義變數,儲存資料 int b ; // 迴圈讀取 while ((b = fr.read())!=‐1) { System.out.println((char)b); } // 關閉資源 fr.close(); } } 輸出結果: 張 三 李 四 王 五
小貼士:
雖然讀取了一個字元,但是會自動提升為int型別。- 使用字元陣列讀取: read(char[] cbuf) ,每次讀取b的長度個字元到陣列中,返回讀取到的有效字元個數,讀取到末尾時,返回 -1 ,程式碼使用演示:
public class FISRead { public static void main(String[] args) throws IOException { // 使用檔名稱建立流物件 ,檔案內容:張三李四王五 FileReader fr = new FileReader("read.txt"); // 定義變數,儲存有效字元個數 int len ; // 定義字元陣列,作為裝字元資料的容器 char[] cbuf = new char[2]; // 迴圈讀取 while ((len = fr.read(cbuf))!=‐1) { System.out.println(new String(cbuf,0,len)); } // 關閉資源 fr.close(); } } 輸出結果: 張三 李四 王五
總結
這次主要寫了IO流的體系分類,以及裡面主要的位元組流和字元流中的File節點流的一些基本的寫出和讀取資料的方法實現,其他的節點流(還有處理流)我也會持續更新。除此之外,還差IO流中緩衝流效率對比也會更新。