文章目錄
Java I/O流
個人博客:https://sgeekioi.github.io/2019/07/25/javaio/#more
一、 Java IO 原理
- 輸入 input:讀取外部數據(磁盤、光盤等存儲設備的數據)到程序(內存)中。
- 輸出 output:將程序(內存)數據輸出到磁盤、光盤等存儲設備中。
二、 流的分類
- 按操作數據單位不同分爲:字節流(8 bit),字符流(16 bit)
- 按數據流的流向不同分爲:輸入流,輸出流
- 按流的角色的不同分爲:節點流,處理流
[抽象基類] | 字節流 | 字符流 |
---|---|---|
輸入流 | InputStream | Reader |
輸出流 | OutputSreeam | Writer |
Java的IO流共設計40多個類,實際上非常規則,都是從以上4個抽象基類派生的。
由這四個類派生出來的子類名稱都是以其父類名作爲子類名後綴。
InputStream 和 Reader 是所有輸入流的基類
2.1 InputStream (典型實現:FileInpugStream)
- int read()
在輸入流中讀取數據的下一個字節。返回0-255範圍內的int字節值。如果因爲已經到達流末尾而沒有可用的字節,則返回-1. - int read(byte[])
從此輸入流中將最多b.length個字節的數據讀入一個byte數組中。如果因爲已經到達流末尾而沒有可用的字節,則返回值爲-1。否則以整數形式返回時機讀取的字節數 - int read(byte[],int off,int len)
將輸入流中最多len個數據字節讀入byte數組。嘗試讀取len個字節,但讀取的字節也可能小於該值。以整數形式返回實際讀取的字節數。如果因爲流位於文件末尾而沒有可用的字節,則返回值爲-1. - FileInputStream 從文件系統中的某個文件中獲得輸入字節。FileInputStream用於讀取非文本數據之類的原始字節流。要讀取字符流,需要使用FileReader。
2.2 Reader (典型實現:FileReader)
- int read()
讀取單個字符。作爲整數讀取的字符,範圍在0-65535之間(0x00-0xffff)(2個字節的Unicode碼),如果已到達流的末尾,則返回-1 - int read(char[] c)
將字符讀入數組。如果已到達流的末尾,則返回-1.否則返回本次讀取的字符數 - int read(char[] c,int off,int len)
將字符讀入數組的某一部分。存到數組cbuf中,從off處開始存儲,最多讀len個字符。如果已到達流的末尾,則返回-1。否則飯hi本次讀取的字符數。
OutputStream和Writer 是所有輸出流的基類
2.3 OutPutStream
- void write(int b)
將指定的字節寫入此輸出流。write 的常規協定是:向輸出流寫入一個字節。要寫入的字節是參數 b 的八個低位。b 的 24 個高位將被忽略。即寫入0~255範圍的。 - void write(byte[] b)
將 b.length 個字節從指定的 byte 數組寫入此輸出流。write(b)的常規協定是:應該 與調用 write(b, 0, b.length) 的效果完全相同。 - void write(byte[] b,int off,int len)
將指定 byte 數組中從偏移量 off 開始的 len 個字節寫入此輸出流。
2.4 Writer
- void write(int c)
寫入單個字符。要寫入的字符包含在給定整數值的 16 個低位中,16 高位被忽略。即 寫入0 到 65535 之間的Unicode碼。 - void write(char[] cbuf)
寫入字符數組。 - void write(char[] cbuf,int off,int len)
寫入字符數組的某一部分。從off開始,寫入len個字符 - void write(String str)
寫入字符串。 - void write(String str,int off,int len)
寫入字符串的某一部分。 - void flush()
刷新該流的緩衝,則立即將它們寫入預期目標。
程序中打開的文件IO資源不屬於內存裏的資源,垃圾回收機制無法回收該資源,所以應該顯式關閉文件IO資源
三、節點流(或文件流)
直接從數據源或目的地讀寫數據
3.1讀取文件
//1.建立一個流對象,將已存在的一個我呢見加載進流
FiileReader fr = new FileReader(new File("Test.txt"));
//2.創建有一個臨時存放數據的數組
char[] ch = new char[1024];
//3.調用流對象的讀取方法將流中的數據讀入到數組中。
fr.read(ch);
//4.關閉資源
fr.close();
FileReader fr = null;
try {
fr = new FileReader(new File("test.txt"));
char[] buf = new char[1024];
int len;
while ((len = fr.read(buf)) != -1) {
System.out.print(new String(buf, 0, len));
}
} catch (IOException e) {
System.out.println("read-Exception :" + e.getMessage());
} finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
System.out.println("close-Exception :" + e.getMessage(); }
}
}
3.2 寫入文件
//1. 創建流對象,建立數據存放文件
FileWriter fw = new FileWriter(new File("Test.txt"));
//2.調用流對象的寫入方法,將數據寫入流
fw.write("I hava a dream");
//3.關閉流資源
fw.close();
FileWriter fw = null;
try{
fw = new FileWrtier(new File("Test.txt"));
fw.write("I hava a dream");
} catch (IOException e){
e.printStackTrace();
} finally {
if (fw != null){
try{
fw.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
3.3 注意事項
- 定義文件路徑時,注意:可以用“/”或者“\”
- 在寫入一個文件時,如果使用構造器FileOutputStream(file),則目錄下有同名文件將被覆蓋
- 如果使用構造器FileOutputStream(file,true),則目錄下的同迷宮文件不會被覆蓋,在文件內容末尾追加內容
- 在讀取文件時,必須保證該文件已存在,否則報異常
- 字節流操作字節,比如:.mp3、.avi、.mp4、.jpg、.doc等
- 字符流操作字符,只能操作普通文本文件。最常見的文本文件:txt、java、c、cpp等語言的額源代碼。尤其注意doc、excel、ppt這些不是文本文件
四、處理流
不直接連接到數據源或目的地,而是“連接”在已存在的流(節點流或處理流)之上,通過對數據的處理爲程序 提供更爲強大的讀寫功能。
4.1 緩衝流(處理流之一)
- 爲了提高數據讀寫的速度,Java API提供了帶緩衝功能的流類,在使用這些流類時,會創建一個內部緩衝區數組,缺省使用 8192個字節(8kb)的緩衝區
public
class BufferedInputStream extends FilterInputStream {
private static int DEFAULT_BUFFER_SIZE = 8192;
- 緩衝流要“套接”在相應的節點流之上,根據數據操作單位可以把緩衝流分爲
- BufferedInputStream 和 BufferedOutputStream
- BufferedReader 和 BufferedWriter
- 當讀取數據時,數據按塊讀入緩衝區,其後的讀操作直接訪問緩衝區
- 當使用BufferedInputStream讀取字節文件時,BufferedInputStream會一次性從文件讀取8192(8kb),存在緩衝區中,直到緩衝區裝滿了,才重新從文件中讀取下一個8192個字節數組。
- 向流中寫入字節時,不會直接寫道文件,先寫道緩衝區中直到緩衝區寫滿,BufferedOutputStream纔會把緩衝區中的數據一次性寫到文件裏。使用方法**flush()**可以強制將緩衝區的內容全部寫入輸入流
- 關閉流的順序和打開流的順序相反。只要關閉最外層流即可,關閉最外城流也會相應關閉內層節點流
- flush()方法的使用:手動將buffer中內容寫入文件
- 如果是帶緩衝區的流對象close()方法,不但會關閉流,還會在關閉流之前刷新緩衝區,關閉不能再寫出
BufferedReader br = null;
BufferedWriter bw = null;
try {
// 創建緩衝流對象:它是處理流,是對節點流的包裝
br = new BufferedReader(new FileReader("d:\\IOTest\\source.txt"));
bw = new BufferedWriter(new FileWriter("d:\\IOTest\\d est.txt"));
String str;
while ((str = br.readLine()) != null) { // 一次讀取字符文本文件的一行字符
bw.write(str); // 一次寫入一行字符串
bw.newLine(); // 寫入行分隔符
}
bw.flush(); // 刷新緩衝區
} catch (IOException e) {
e.printStackTrace();
} finally {
// 關閉IO流對象
try {
if(bw != null) {
bw.close(); // 關閉過濾流時,會自動關閉它所包裝的底層節點流
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
4.2 轉換流(處理流之二)
轉換流提供了字節流和字符流之間的轉換
Java API提供了兩個轉換流:
- InputStreamReader:將InputStream轉換爲Reader
實現將字節的的輸入流按指定字符集轉換爲字符的輸入流。
- 需要和InputStream“套接”
- 構造器
- public InputStreamReader(InputStream in)
- public InputStreamReader(InputStream in,String charsetName)
- OutputStreamWriter:將Writer轉換爲OutputStream
實現將字符的輸出流按指定字符集轉換爲字節的輸出流
- 需要和OutputStream“套接”
- 構造器
- public OutputStreamWriter(OutputStream out)
- public OutputStreamWriter(OutputStream out,String charsetName)
- 字節流中的數據都是字符時,轉成字符流操作更高效。
- 很多時候我們使用轉換流來處理文件亂碼問題。實現編碼和節碼的功能。
4.3 打印流(標準流之三)
實現將基本數據類型的數據格式轉換爲字符串
打印流:PrintStream和PrintWriter
- 提供了一系列重載的print()和pringln()方法,用於多種數據類型的輸出
- PrintStream和PrintWriter的輸出不會拋出IOException異常
- PrintStream和PrintWriter有自動flush功能
- PrintStream打印的所有字符都使用平臺的默認字符編碼轉換爲字節。在需要寫入字符而不是寫入字節的情況下,應使用PrintWriter類。
- System.out返回的是PrintStream的實例
4.4 對象流
ObjectInputStream和ObjectOutputStream
-
用於存儲和讀取基本數據類型數據或對象的處理流。它的強大之處就是可以把Java中的對象寫入到數據源中,也能把對象從數據源中還原回來。
-
序列化:用ObjectOutputStream類保存基本類型數據或對象的機制
-
反序列化:用ObjectInputStream類讀取基本數據類型或對象的機制
-
ObjectOutStream和ObjectInputStream不能序列化static和transient修飾的成員變量
4.5對象的序列化
- 對象的序列化機制允許把內存中的Java對象轉化稱平臺無關的二進制流,從而把允許這種二進制流持久的保存在磁盤上,或通過網絡將這中二進制流傳輸到另一個網絡節點。當其他程序獲取了這種二進制流,就可以恢復成原來的Java對象
- 序列化的好處在於可將任何實現了Serializable接口的對象轉化爲字節數據,使其在保存和傳輸時可被還原
- 序列化時RMI(Remote Method Invoke-遠程方法調用)過程的參數和返回值都必須實現的機制,而RMI是JavaEE的基礎。因此序列化機制是JavaEE平臺的基礎
- 如果需要讓兩個對象支持序列化機制,則必須讓對象所屬的類及其屬性是可序列化的,爲了讓某個類是可序列化的,則該類必須實現如下兩個接口之一
- Serializable
- Externlizable
- 凡是實現Serializable接口的類都有一個表示序列化版本標識符的靜態變量:
- private static final long serialVersionUID;
- serialVersionUID 用來表明類的不同版本間的兼容性。簡言之,其目的是以序列化對象進行版本控制,有關各版本反序列化時是否兼容。
- 如果類沒有顯示定義這個靜態常量,它的值是Java運行時環境根據類的內部細節自動生成的。**若類的實例變量做了修改,serialVersionUID可能發生變化。**故建議,顯示聲明。
- 簡單來說,Java的序列化機制是通過在運行是判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認爲是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常(InvalidcastException)
4.5.1 使用對象流序列化對象
- 若某個類實現了Serializable接口,該類的對象就是可序列化的:
- 創建一個ObjectOututStream
- 調用ObjectoutputStream對象的writeObject(對象)方法輸出可序列化對象
- 注意寫一次,操作一次flush()
- 反序列化
- 創建一個ObjectInputStream
- 調用readObject()方法讀取流中的對象
如果某個類的屬性不是基本數據類型或String類型,而是另一個引用類型,那麼這個引用類型必須是可序列化的,否則擁有該類型的Field的類也不能序列化
序列化:將對象寫入到磁盤或者進行網絡傳輸。要求對象必須實現序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.txt"));
Persion p = new Per("張三",18,"北京",new Pet());
oos.writeObject(p);
oos.flush();
oos.close();
反序列化:將磁盤中的對象數據源讀出。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.txt");
Persion p1 = (Persion)ois.readObject();
System.out.println(p1.toString());
ois.close();
面試題
**談談你對java.io.Serializable接口的理解,我們知道它用於序列化,是空方法接口,還有其他人是嗎?
- 實現了Serializable接口的對象,可將它們轉換成一系列字節,並可在以後完全恢復回原來的樣子。這一過程亦可通過網絡進行。這意味着序列化機制能自動補償操作系統間的差異。換句話說,可以在Windows機器上創建一個對象,對其序列化 ,然後通過網絡發給一臺Unix機器,然後在那裏準確無誤地重新裝配。不必關心數據在不同機器上如何表示,也不必關心字節地順序或者其他任何細節。
- 由於大部分作爲參數地類如String、Integer等都實現了Serializable的接口,也可以利用多態的性質,作爲參數使接口更靈活。
4.5.2隨機存取文件流 RadnomAccessFile類
我們可以用RandomAccessFile這個類,來實現一個多線程斷點下載的功能,用過下載工具的朋友們都知道,下載前都會建立兩個臨時文件,一個是與被下載文件大小相同的空文件,另一個是記錄文件指針的位置文件,每次暫停的時候,都會保存上一次的指針,然後斷點下載的時候,會繼續從上次的地方下載,從而是按斷點下載或上傳的功能。
- 讀取文件內容
RandomAccessFile raf = new RandomAccessFile(“test.txt”, “rw”);
raf.seek(5);
byte [] b = new byte[1024];
int off = 0;
int len = 5;
raf.read(b, off, len);
String str = new String(b, 0, len); System.out.println(str);
raf.close();
- 寫入文件內容
RandomAccessFile raf = new RandomAccessFile("test.txt", "rw"); raf.seek(5);
//先讀出來 String temp = raf.readLine();
raf.seek(5); raf.write("xyz".getBytes());
raf.write(temp.getBytes());
raf.close();