@html
Java 的 IO 經過 java.io 包下的類和接口來支持, 在 java.io 包下主要包括輸入、 輸出兩種 10 流, 每種輸入、 輸出流又可分爲字節流和字符流兩大類。 其中字節流以字節爲單位來處理輸入、 輸出操做, 而字符流則以字符來處理輸入、 輸出操做。java
Java的標準庫java.io提供了File對象來操做文件和目錄。程序員
File 類可使用文件路徑字符串來建立 File 實例, 該文件路徑字符串既能夠是絕對路徑, 也能夠是相對路徑。 在默認狀況下, 系統老是依據用戶的工做路徑來解釋相對路徑。算法
建立了File對象後, 就能夠調用 File 對象的方法來訪問, File 類提供了不少方法來操做文件和目錄, 下面列出一些比較經常使用的方法。編程
面程序以幾個簡單方法來測試一下 File 類的功能:api
import java.io.*; public class FileTest { public static void main(String[] args) throws IOException { // 以當前路徑來建立一個File對象 File file = new File("."); // 直接獲取文件名,輸出一點 System.out.println(file.getName()); // 獲取相對路徑的父路徑可能出錯,下面代碼輸出null System.out.println(file.getParent()); // 獲取絕對路徑 System.out.println(file.getAbsoluteFile()); // 獲取上一級路徑 System.out.println(file.getAbsoluteFile().getParent()); // 在當前路徑下建立一個臨時文件 File tmpFile = File.createTempFile("aaa", ".txt", file); // 指定當JVM退出時刪除該文件 tmpFile.deleteOnExit(); // 以系統當前時間做爲新文件名來建立新文件 File newFile = new File(System.currentTimeMillis() + ""); System.out.println("newFile對象是否存在:" + newFile.exists()); // 以指定newFile對象來建立一個文件 newFile.createNewFile(); // 以newFile對象來建立一個目錄,由於newFile已經存在, // 因此下面方法返回false,即沒法建立該目錄 newFile.mkdir(); // 使用list()方法來列出當前路徑下的全部文件和路徑 String[] fileList = file.list(); System.out.println("====當前路徑下全部文件和路徑以下===="); for (String fileName : fileList) { System.out.println(fileName); } // listRoots()靜態方法列出全部的磁盤根路徑。 File[] roots = File.listRoots(); System.out.println("====系統全部根路徑以下===="); for (File root : roots) { System.out.println(root); } } }
API:java.io.File數組
在 File 類的 list()方法中能夠接收一個 FilenameFilter 參數, 經過該參數能夠只列出符合條件的文件。安全
FilenameFilter 接口裏包含了一個 accept(File dir,String name)方法, 該方法將依次對指定 File 的全部子目錄或者文件進行迭代, 若是該方法返回 true, 則 list()方法會列出該子目錄或者文件。服務器
import java.io.*; public class FilenameFilterTest { public static void main(String[] args) { File file = new File("."); // 使用Lambda表達式(目標類型爲FilenameFilter)實現文件過濾器。 // 若是文件名以.java結尾,或者文件對應一個路徑,返回true String[] nameList = file.list((dir, name) -> name.endsWith(".java") || new File(name).isDirectory()); for(String name : nameList) { System.out.println(name); } } }
按照不一樣的分類方式, 能夠將流分爲不一樣的類型。網絡
按照流的流向來分, 能夠分爲輸入流和輸出流:
此處的輸入、 輸出涉及一個方向問題, 對於如圖 1 所示的數據流向, 數據從內存到硬盤, 一般稱爲輸出流——也就是說, 這裏的輸入、 輸出都是從程序運行所在內存的角度來劃分的。
對於如圖 2 所示的數據流向, 數據從服務器經過網絡流向客戶端, 在這種狀況下, Server 端的內存負責將數據輸出到網絡裏, 所以 Server 端的程序使用輸出流; Client 端的內存負責從網絡裏讀取數據, 所以 Client 端的程序應該使用輸入流。
字節流和字符流的用法幾乎徹底同樣, 區別在於字節流和字符流所操做的數據單元不一樣操做的數據單元是 8 位的字節, 而字符流操做的數據單元是 16 位的字符。
按照流的角色來分, 能夠分爲節點流和處理流。
能夠從/向一個特定的IO設備( 如磁盤、 網絡) 讀/寫數據的流, 稱爲節點流, 節點流也被稱爲低級流( Low Level Stream)。 圖 3 顯示了節點流示意圖。
處理流則用於對一個己存在的流進行鏈接或封裝, 經過封裝後的流來實現數據讀/寫功能。 處理流也被稱爲高級流。 圖 4 顯示了處理流示意圖。
ava 把全部設備裏的有序數據抽象成流模型, 簡化了輸入/輸出處理, 理解了流的概念模型也就瞭解了Java IO。
Java 的 IO流的 40 多個類都是從以下 4 個抽象基類派生的:
經過使用處理流, Java 程序無須理會輸入/輸出節點是磁盤、 網絡仍是其餘的輸入/輸出設備, 程序只要將這些節點流包裝成處理流, 就可使用相同的輸入/輸出代碼來讀寫不一樣的輸入/輸出設備的數據。
節流和字符流放的操做方式幾乎徹底同樣, 區別只是操做的數據單元不一樣。
InputStream 和 Reader 是全部輸入流的抽象基類, 自己並不能建立實例來執行輸入, 但它們是全部輸入流的模板, 因此它們的方法是全部輸入流均可使用的方法。
在 InputStream 裏包含以下三個方法:
在 Reader 裏包含以下三個方法:
InputStream 和 Reader 都是抽象類, 自己不能建立實例, 但它們分別有一個用於讀取文件的輸入流: FilelnputStream 和 FileReader, 它們都是節點流—會直接和指定文件關聯。
下面程序爲 FilelnputStream 來讀取自身的效果實例:
import java.io.*; public class FileInputStreamTest { public static void main(String[] args) throws IOException { // 建立字節輸入流 FileInputStream fis = new FileInputStream( "FileInputStreamTest.java"); // 建立一個長度爲1024的「竹筒」 byte[] bbuf = new byte[1024]; // 用於保存實際讀取的字節數 int hasRead = 0; // 使用循環來重複「取水」過程 while ((hasRead = fis.read(bbuf)) > 0 ) { // 取出「竹筒」中水滴(字節),將字節數組轉換成字符串輸入! System.out.print(new String(bbuf , 0 , hasRead )); } // 關閉文件輸入流,放在finally塊裏更安全 fis.close(); } }
FileReader 來讀取文件自己實例:
import java.io.*; public class FileReaderTest { public static void main(String[] args) { try( // 建立字符輸入流 FileReader fr = new FileReader("FileReaderTest.java")) { // 建立一個長度爲32的「竹筒」 char[] cbuf = new char[32]; // 用於保存實際讀取的字符數 int hasRead = 0; // 使用循環來重複「取水」過程 while ((hasRead = fr.read(cbuf)) > 0 ) { // 取出「竹筒」中水滴(字符),將字符數組轉換成字符串輸入! System.out.print(new String(cbuf , 0 , hasRead)); } } catch (IOException ex) { ex.printStackTrace(); } } }
API:java.io.Reader
API:java.io.FileReader
OntputStream 和 Writer 也很是類似, 它們採用如圖 6 所示的模型來執行輸出, 兩個流都提供了以下三個方法:
由於字符流直接以字符做爲操做單位, 因此 Writer 能夠用字符串來代替字符數組, 即以 String 對象做爲參數。 Writer 裏還包含以下兩個方法:
下面程序使用 FilelnputStream 來執行輸入, 並使用 FileOutputStream 來執行輸出, 用以實現複製FileOutputStreamTest.java 文件的功能。
import java.io.*; public class FileOutputStreamTest { public static void main(String[] args) { try( // 建立字節輸入流 FileInputStream fis = new FileInputStream( "FileOutputStreamTest.java"); // 建立字節輸出流 FileOutputStream fos = new FileOutputStream("newFile.txt")) { byte[] bbuf = new byte[32]; int hasRead = 0; // 循環從輸入流中取出數據 while ((hasRead = fis.read(bbuf)) > 0 ) { // 每讀取一次,即寫入文件輸出流,讀了多少,就寫多少。 fos.write(bbuf , 0 , hasRead); } } catch (IOException ioe) { ioe.printStackTrace(); } } }
import java.io.*; public class FileWriterTest { public static void main(String[] args) { try( FileWriter fw = new FileWriter("poem.txt")) { fw.write("錦瑟 - 李商隱\r\n"); fw.write("錦瑟無故五十弦,一弦一柱思華年。\r\n"); fw.write("莊生曉夢迷蝴蝶,望帝春心託杜鵑。\r\n"); fw.write("滄海月明珠有淚,藍田日暖玉生煙。\r\n"); fw.write("此情可待成追憶,只是當時已惘然。\r\n"); } catch (IOException ioe) { ioe.printStackTrace(); } } }
API:java.io.Writer
API:java.io.FilterWriter
Java 的輸入/輸出流體系提供了近 40 個類, 這些類看上去雜亂而沒有規律, 但若是將其按功能進行分類, 則不難發現其是很是規律的。 表 1 顯示了 Java 輸入/輸出流體系中經常使用的流分類。
表 1 僅僅總結了輸入/輸出流體系中位於 java.io 包下的流, 還有一些諸如 AudioInputStream、CipherlnputStream、 DeflaterlnputStream、ZipInputStream 等具備訪問音頻文件、 加密/解密、 壓縮/解壓等功能的字節流, 它們具備特殊的功能, 位於 JDK 的其餘包下。
4 個基類使用起來有些煩瑣。 若是但願簡化編程, 能夠藉助於處理流。
下面程序使用 PrintStream 處理流來包裝 OutputStream, 使用處理流後的輸出流在輸出時將更加方便。
import java.io.*; public class PrintStreamTest { public static void main(String[] args) { try( FileOutputStream fos = new FileOutputStream("test.txt"); PrintStream ps = new PrintStream(fos)) { // 使用PrintStream執行輸出 ps.println("普通字符串"); // 直接使用PrintStream輸出對象 ps.println(new PrintStreamTest()); } catch (IOException ioe) { ioe.printStackTrace(); } } }
上面程序中先定義了一個節點輸出流 FileOutputStream, 然 後程序使用PrintStream 包裝了該節點輸出流, 最後使用 PrintStream 輸出字符串、 輸出對象……
PrintStream 的輸出功能很是強大, 前面程序中一直使用的標準輸出 System.out 的類型就是 PrintStream。
程序使用處理流, 一般只須要在建立處理流時傳入一個節點流做爲構造器參數便可, 這樣建立的處理流就是包裝了該節點流的處理流。
輸入/輸出流體系中還提供了兩個轉換流, 這兩個轉換流用於實現將字節流轉換成字符流, 其中InputStreamReader 將字節輸入流轉換成字符輸入流, OutputStreamWriter 將字節輸出流轉換成字符輸出流。
下面以獲取鍵盤輸入爲例來介紹轉換流的用法。 Java 使用 System.in 表明標準輸入, 即鍵盤輸入,但這個標準輸入流是 InputStream 類的實例, 使用不太方便, 並且鍵盤輸入內容都是文本內容, 因此可使用 InputStreamReader 將其轉換成字符輸入流, 普通的 Reader 讀取輸入內容時依然不太方便, 能夠將普通的 Reader 再次包裝成 BufferedReader, 利用 BufferedReader 的 readLine()方法能夠一次讀取一行內容。
以下程序所示:
import java.io.*; public class KeyinTest { public static void main(String[] args) { try( // 將Sytem.in對象轉換成Reader對象 InputStreamReader reader = new InputStreamReader(System.in); // 將普通Reader包裝成BufferedReader BufferedReader br = new BufferedReader(reader)) { String line = null; // 採用循環方式來一行一行的讀取 while ((line = br.readLine()) != null) { // 若是讀取的字符串爲"exit",程序退出 if (line.equals("exit")) { System.exit(1); } // 打印讀取的內容 System.out.println("輸入內容爲:" + line); } } catch (IOException ioe) { ioe.printStackTrace(); } } }
對象序列化的目標是將對象保存到磁盤中, 或容許在網絡中直接傳輸對象。 對象序列化機制容許把內存中的 Java 對象轉換成平臺無關的二進制流, 從而容許把這種二進制流持久地保存在磁盤上, 經過網絡將這種二進制流傳輸到另外一個網絡節點。 其餘程序一旦得到了這種二進制流( 不管是從磁盤中獲取的, 仍是經過網絡獲取的), 均可以將這種二進制流恢復成原來的 Java 對象。
序列化機制容許將實現序列化的 Java 對象轉換成字節序列, 這些字節序列能夠保存在磁盤上, 或經過網絡傳輸, 以備之後從新恢復成原來的對象。 序列化機制使得對象能夠脫離程序的運行而獨立存在。
對象的序列化 ( Serialize ) 指將一個 Java 對象寫入 IO流中, 與此對應的是, 對象的反序列化(Deserialize) 則指從 IO 流中恢復該 Java 對象。
若是須要讓某個對象支持序列化機制, 則必須讓它的類是可序列化的 (serializable )o 爲了讓某個
類是可序列化的, 該類必須實現以下兩個接口之一。
Java 的不少類己經實現了 Serializable, 該接口是一個標記接口, 實現該接口無須實現任何方法, 它只是代表該類的實例是可序列化的。
全部可能在網絡上傳輸的對象的類都應該是可序列化的, 不然程序將會出現異常, 好比 RMI( Remote Method Invoke, 即遠程方法調用, 是 Java EE 的基礎) 過程當中的參數和返回值; 全部須要保存到磁盤裏的對象的類都必須可序列化, 好比 Web 應用中須要保存到 HttpSession 或 ServletContext 屬性的 Java 對象。
由於序列化是 RMI 過程的參數和返回值都必須實現的機制, 而 RMI 又是 Java EE 技術的基礎——全部的分佈式應用經常須要跨平臺、 跨網絡, 因此要求全部傳遞的參數、 返回值必須實現序列化。 所以序列化機制是 Java EE 平臺的基礎。 一般建議: 程序建立的每一個 JavaBean 類都實現 Serializable。
使用 Serializable 來實現序列化, 只須要讓目標類實現 Serializable 標記接口便可, 無須實現任何方法。
一旦某個類實現了 Serializable 接口, 該類的對象就是可序列化的, 程序能夠經過以下兩個步驟來序列化該對象。
// 建立一個 ObjectlnputStream 輸入流 ObjectlnputStream ois =new ObjectlnputStream( new FilelnputStream("object.txt"));
// 從輸入流中讀取一個 Java 對象, 並將其強制類型轉換爲 Person 類 Person p (Person)ois.readObject();
下面程序定義了一個 Person 類, 這個 Person 類就是一個普通的 Java 類, 只是實現了 Serializable接口, 該接口標識該類的對象是可序列化的。
public class Person implements java.io.Serializable { private String name; private int age; // 注意此處沒有提供無參數的構造器! public Person(String name , int age) { System.out.println("有參數的構造器"); this.name = name; this.age = age; } // 省略name與age的setter和getter方法 // name的setter和getter方法 public void setName(String name) { this.name = name; } public String getName() { return this.name; } // age的setter和getter方法 public void setAge(int age) { this.age = age; } public int getAge() { return this.age; } }
下面程序使用 ObjectOutputStream 將一個 Person 對象寫入磁盤文件:
import java.io.*; public class WriteObject { public static void main(String[] args) { try( // 建立一個ObjectOutputStream輸出流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("object.txt"))) { Person per = new Person("孫悟空", 500); // 將per對象寫入輸出流 oos.writeObject(per); } catch (IOException ex) { ex.printStackTrace(); } } }
運行上面程序, 將會看到生成了一個 object.txt 文件, 該文件的內容就是Person 對象。
若是但願從二進制流中恢復 Java 對象, 則須要使用反序列化。 反序列化的步驟以下:
/ / 建立一個 ObjectlnputStream 輸入流 ObjectlnputStream ois =new ObjectlnputStream( new FilelnputStream("object.txt"));
// 從輸入流中讀取一個 Java 對象, 並將其強制類型轉換爲 Person 類 Person p (Person)ois.readObject();
下面程序從剛剛生成的 object.txt 文件中讀取 Person 對象:
import java.io.*; public class ReadObject { public static void main(String[] args) { try( // 建立一個ObjectInputStream輸入流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("object.txt"))) { // 從輸入流中讀取一個Java對象,並將其強制類型轉換爲Person類 Person p = (Person)ois.readObject(); System.out.println("名字爲:" + p.getName() + "\n年齡爲:" + p.getAge()); } catch (Exception ex) { ex.printStackTrace(); } } }
反序列化讀取的僅僅是 Java 對象的數據, 而不是 Java 類, 所以採用反序列化恢復Java 對象時, 必須提供該 Java 對象所屬類的 class 文件, 不然將會引起 ClassNotFoundException 異常。
Person 類的兩個成員變量分別是 String 類型和 int 類型, 若是某個類的成員變量的類型不是基本類型或 String 類型, 而是另外一個引用類型, 那麼這個引用類必須是可序列化的, 不然擁有該類型成員變量的類也是不可序列化的。
以下 Teacher 類持有一個 Person 類的引用, 只有 Person 類是可序列化的,Teacher 類纔是可序列化的。 若是 Person 類不可序列化, 則不管 Teacher 類是否實現 Serilizable、 Extemalizable 接口, 則 Teacher類都是不可序列化的。
public class Teacher implements java.io.Serializable { private String name; private Person student; public Teacher(String name , Person student) { this.name = name; this.student = student; } // 此處省略了name和student的setter和getter方法 …… }
Java 序列化機制採用了一種特殊的序列化算法, 其算法內容以下:
下面程序序列化了兩個 Teacher 對象, 兩個 Teacher對象都持有一個引用到同一個 Person 對象的引用, 並且程序兩次調用 writeObject()方法輸出同一Teacher 對象。
import java.io.*; public class WriteTeacher { public static void main(String[] args) { try( // 建立一個ObjectOutputStream輸出流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("teacher.txt"))) { Person per = new Person("孫悟空", 500); Teacher t1 = new Teacher("唐僧" , per); Teacher t2 = new Teacher("菩提祖師" , per); // 依次將四個對象寫入輸出流 oos.writeObject(t1); oos.writeObject(t2); oos.writeObject(per); oos.writeObject(t2); } catch (IOException ex) { ex.printStackTrace(); } } }
上面程序中的粗體字代碼 4 次調用了 writeObject()方法來輸出對象, 實際上只序列化了三個對象,並且序列的兩個 Teacher 對象的 student 引用實際是同一個 Person 對象。 下面程序讀取序列化文件中的對象便可證實這一點:
import java.io.*; public class SerializeMutable { public static void main(String[] args) { try( // 建立一個ObjectOutputStream輸入流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("mutable.txt")); // 建立一個ObjectInputStream輸入流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("mutable.txt"))) { Person per = new Person("孫悟空", 500); // 系統會per對象轉換字節序列並輸出 oos.writeObject(per); // 改變per對象的name實例變量 per.setName("豬八戒"); // 系統只是輸出序列化編號,因此改變後的name不會被序列化 oos.writeObject(per); Person p1 = (Person)ois.readObject(); //① Person p2 = (Person)ois.readObject(); //② // 下面輸出true,即反序列化後p1等於p2 System.out.println(p1 == p2); // 下面依然看到輸出"孫悟空",即改變後的實例變量沒有被序列化 System.out.println(p2.getName()); } catch (Exception ex) { ex.printStackTrace(); } } }
在一些特殊的場景下, 若是一個類裏包含的某些實例變量是敏感信息, 例如銀行帳戶信息等, 這時不但願系統將該實例變量值進行序列化; 或者某個實例變量的類型是不可序列化的, 所以不但願對該實例變量進行遞歸序列化, 以免引起 java.io.NotSerializableException 異常。
經過在實例變量前面使用 transient 關鍵字修飾, 能夠指定 Java 序列化時無須理會該實例變量。 以下 Person 類與前面的 Person 類幾乎徹底同樣, 只是它的 age 使用了 transient 關鍵字修飾。
public class Person implements java.io.Serializable { private String name; private transient int age; // 注意此處沒有提供無參數的構造器! public Person(String name , int age) { System.out.println("有參數的構造器"); this.name = name; this.age = age; } // 省略name與age的setter和getter方法 …… }
下面程序先序列化一個 Person 對象, 而後再反序列化該 Person 對象, 獲得反序列化的 Person 對象後程序輸出該對象的 age 實例變量值:
import java.io.*; public class TransientTest { public static void main(String[] args) { try( // 建立一個ObjectOutputStream輸出流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("transient.txt")); // 建立一個ObjectInputStream輸入流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("transient.txt"))) { Person per = new Person("孫悟空", 500); // 系統會per對象轉換字節序列並輸出 oos.writeObject(per); Person p = (Person)ois.readObject(); //age實例變量使用 transient 關鍵字修飾, 因此輸出 0 System.out.println(p.getAge()); } catch (Exception ex) { ex.printStackTrace(); } } }
使用 transient 關鍵字修飾實例變量雖然簡單、 方便, 但被 transient 修飾的實例變量將被徹底隔離在序列化機制以外, 這樣致使在反序列化恢復 Java 對象時沒法取得該實例變量值。 Java 還提供了一種自定義序列化機制, 經過這種自定義序列化機制可讓程序控制如何序列化各實例變量, 甚至徹底不序列化某些實例變量( 與使用 transient 關鍵字的效果相同)。
在序列化和反序列化過程當中須要特殊處理的類應該提供以下特殊簽名的方法, 這些特殊的方法用以實現自定義序列化。
下面的 Person 類提供了 writeObject()和 readObject()兩個方法, 其中 writeObject()方法在保存 Person對象時將其name 實例變量包裝成 StringBuffer, 並將其字符序列反轉後寫入;在 readObjectO方法中處理 name 的策略與此對應 先將讀取的數據強制類型轉換成 StringBuffer, 再將其反轉後賦給例變量。
import java.io.*; public class Person implements java.io.Serializable { private String name; private int age; // 注意此處沒有提供無參數的構造器! public Person(String name , int age) { System.out.println("有參數的構造器"); this.name = name; this.age = age; } // 省略name與age的setter和getter方法 …… private void writeObject(java.io.ObjectOutputStream out) throws IOException { // 將name實例變量的值反轉後寫入二進制流 out.writeObject(new StringBuffer(name).reverse()); out.writeInt(age); } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { // 將讀取的字符串反轉後賦給name實例變量 this.name = ((StringBuffer)in.readObject()).reverse() .toString(); this.age = in.readInt(); } }
對於這個 Person 類而言, 序列化、 反序列化 Person 實例並無任何區別—區別在於序列化後的對象流, 即便有 Cracker 截獲到 Person 對象流,他看到的 name 也是加密後的 name 值, 這樣就提升序列化的安全性。
還有一種更完全的自定義機制,它甚至能夠在序列化對象時將該對象替換成其餘對象。若是須要實 。現序列化某個對象時替換該對象, 則應爲序列化類提供以下特殊方法:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
此 writeReplaceO方法將由序列化機制調用, 只要該方法存在。 由於該方法能夠擁有私有( private )、受保護的 ( protected) 和 包 私 有 ( package-private) 等訪問權限, 因此其子類有可能得到該方法。 例如,下面的 Person 類提供了 writeReplace()方法, 這樣能夠在寫入 Person 對象時將該對象替換成 ArrayList。
import java.util.*; import java.io.*; public class Person implements java.io.Serializable { private String name; private int age; // 注意此處沒有提供無參數的構造器! public Person(String name , int age) { System.out.println("有參數的構造器"); this.name = name; this.age = age; } // 省略name與age的setter和getter方法 …… // 重寫writeReplace方法,程序在序列化該對象以前,先調用該方法 private Object writeReplace()throws ObjectStreamException { ArrayList<Object> list = new ArrayList<>(); list.add(name); list.add(age); return list; } }
Java 的序列化機制保證在序列化某個對象以前, 先調用該對象的writeReplaceO方法, 若是該方法返回另外一個 Java 對象, 則系統轉爲序列化另外一個對象。 以下程序表面上是序列化 Person 對象, 但實際上序列化的是 ArrayList:
import java.io.*; import java.util.*; public class ReplaceTest { public static void main(String[] args) { try( // 建立一個ObjectOutputStream輸出流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("replace.txt")); // 建立一個ObjectInputStream輸入流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("replace.txt"))) { Person per = new Person("孫悟空", 500); // 系統將per對象轉換字節序列並輸出 oos.writeObject(per); // 反序列化讀取獲得的是ArrayList ArrayList list = (ArrayList)ois.readObject(); System.out.println(list); } catch (Exception ex) { ex.printStackTrace(); } } }
與 writeReplace()方法相對的是, 序列化機制裏還有一個特殊的方法, 它能夠實現保護性複製整個對象。 這個方法就是:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
這個方法會緊接着 readObject()以後被調用, 該方法的返回值將會代替原來反序列化的對象, 而原來 readObject()反序列化的對象將會被當即丟棄。
從 JDK 1.4 開始, Java 提供了一系列改進的輸入/輸出處理的新功能, 這些功能被統稱爲新 IO ( New IO, 簡稱 NIO), 新增了許多用於處理輸入/輸出的類, 這些類都被放在 java.nio 包以及子包下, 而且對原 java.io 包中的不少類都以 NI0 爲基礎進行了改寫, 新增了知足 NI0 的功能。
新 IO 和傳統的IO有相同的目的, 都是用於進行輸入/輸出, 但新 IO 使用了不一樣的方式來處理輸入/輸出, 新 IO 採用內存映射文件的方式來處理輸入/輸出, 新 IO 將文件或文件的一段區域映射到內存中,這樣就能夠像訪問內存同樣來訪問文件了( 這種方式模擬了操做系統上的虛擬內存的概念), 經過這種方式來進行輸入/輸出比傳統的輸入/輸出要快得多。
Java 中與新 IO 相關的包以下:
從內部結構上來看, Buffer 就像一個數組, 它能夠保存多個類型相同的數據。 Buffer是一個抽象類,其最經常使用的子類是 ByteBuffer, 它能夠在底層字節數組上進行 get/set 操做。 除 ByteBuffer 以外, 對應於其餘基本數據類型( boolean除外) 都有相應的 Buffer 類: CharBuffer、 ShortBuffer、 IntBuffer、LongBuffer、FloatBuffer、 DoubleBuffer。
上面這些 Buffer類, 除 ByteBuffer 以外, 它們都採用相同或類似的方法來管理數據, 只是各自管理的數據類型不一樣而己。 這些 Buffer 類都沒有提供構造器, 經過使用以下方法來獲得一個 Buffer 對象。
但實際使用較多的是 ByteBuffer 和 CharBuffer, 其餘 Buffer 子類則較少用到。 其中 ByteBuffer 類還有一個子類: MappedByteBuffer, 它用於表示 Channel 將磁盤文件的部分或所有內容映射到內存中後獲得的結果, 一般MappedByteBuffer 對象由 Channel 的 map()方法返回。
在 Buffer 中有三個重要的概念: 容量( capacity)、 界限 ( limit ) 和 位 置( position )。
Buffer 裏還支持一個可選的標記 (mark, 類 似於傳統 IO流中的mark ), Buffer 容許直接將 position 定位到該 mark 處。 這些值知足以下關係:
mark<position<limit<capacity
當 Buffer 裝入數據結束後, 調用 Buffer 的 flip()方法, 該方法將 limit 設置爲 position 所在位置, 並將 position 設爲 0, 這就使得 Buffer 的讀寫指針又移到了開始位置。 也就是說, Buffer 調用 flip()方法以後, Buffer 爲輸出數據作好準備; 當 Buffer 輸出數據結束後, Buffer 調用 clear()方法, clear()方法不是清空 Buffer 的數據, 它僅僅將 position 置 爲 0, 將 limit 置 爲 capacity, 這 樣 爲 再 次 向 Buffer 中裝入數據作好準備。
除此以外, Buffer 還包含以下一些經常使用的方法。
除這些移動 position、 limit、 mark 的方法以外, Buffer 的全部子類還提供了兩個重要的方法: put()和 get()方法, 用於向 Buffer 中放入數據和從 Buffer 中取出數據。 當使用 put()和 get()方法放入、 取出數據時, Buffer 既支持對單個數據的訪問, 也支持對批量數據的訪問( 以數組做爲參數)。
當使用 put()和 get()來訪問 Buffer 中的數據時, 分爲相對和絕對兩種:
下面程序爲 Buffer 的一些常規操做實例:
import java.nio.*; public class BufferTest { public static void main(String[] args) { // 建立Buffer CharBuffer buff = CharBuffer.allocate(8); // ① System.out.println("capacity: " + buff.capacity()); System.out.println("limit: " + buff.limit()); System.out.println("position: " + buff.position()); // 放入元素 buff.put('a'); buff.put('b'); buff.put('c'); // ② System.out.println("加入三個元素後,position = " + buff.position()); // 調用flip()方法 buff.flip(); // ③ System.out.println("執行flip()後,limit = " + buff.limit()); System.out.println("position = " + buff.position()); // 取出第一個元素 System.out.println("第一個元素(position=0):" + buff.get()); // ④ System.out.println("取出一個元素後,position = " + buff.position()); // 調用clear方法 buff.clear(); // ⑤ System.out.println("執行clear()後,limit = " + buff.limit()); System.out.println("執行clear()後,position = " + buff.position()); System.out.println("執行clear()後,緩衝區內容並無被清除:" + "第三個元素爲:" + buff.get(2)); // ⑥ System.out.println("執行絕對讀取後,position = " + buff.position()); } }
API:java.nio.Buffer
緩衝區爲咱們裝載了數據,可是數據的寫入和讀取並不能直接進行read()和write()這樣的系統調用,而是JVM爲咱們提供了一層對系統調用的封裝。而Channel能夠用最小的開銷來訪問操做系統自己的IO服務,這就是爲何要有Channel的緣由。
Channel 相似於傳統的流對象, 但與傳統的流對象有兩個主要區別。
Java 爲 Channel 接口提供了DatagramChanneKFileChanneKPipe.SinkChanneKPipe.SourceChanneK
SelectableChannel、 ServerSocketChannel 、 SocketChannel 等實現類。
——新IO裏的 Channel 是按功能來劃分的。
全部的 Channel 都不該該經過構造器來直接建立, 而是經過傳統的節點 InputStream、 OutputStream的 getChannel()方法來返回對應的 Channel, 不一樣的節點流得到的 Channel 不同。 例如,FilelnputStream、FileOutputStream 的 getChannel()返回的是 FileChannel, 而 PipedlnputStream、 PipedOutputStream 的getChannel()返回的是 Pipe.SinkChanneK Pipe.SourceChannel。
Channel 中最經常使用的三類方法是 map()、 read()和 write(), 其中 map()方法用於將 Channel 對應的部分或所有數據映射成 ByteBuffer; 而 read()或 write()方法都有一系列重載形式, 這些方法用於從 Buffer中讀取數據或向 Buffer 中寫入數據。
map()方法的方法簽名爲: MappedByteBuffer map(FileChannel.MapMode mode, long position, longsize), 第一個參數執行映射時的模式, 分別有隻讀、 讀寫等模式; 而第二個、 第三個參數用於控制將Channel 的哪些數據映射成 ByteBuffer。
下面程序直接將 FileChannel 的所有數據映射成 ByteBuffer:
import java.io.*; import java.nio.*; import java.nio.channels.*; import java.nio.charset.*; public class FileChannelTest { public static void main(String[] args) { File f = new File("FileChannelTest.java"); try( // 建立FileInputStream,以該文件輸入流建立FileChannel FileChannel inChannel = new FileInputStream(f).getChannel(); // 以文件輸出流建立FileBuffer,用以控制輸出 FileChannel outChannel = new FileOutputStream("a.txt") .getChannel()) { // 將FileChannel裏的所有數據映射成ByteBuffer MappedByteBuffer buffer = inChannel.map(FileChannel .MapMode.READ_ONLY , 0 , f.length()); // ① // 使用GBK的字符集來建立解碼器 Charset charset = Charset.forName("GBK"); // 直接將buffer裏的數據所有輸出 outChannel.write(buffer); // ② // 再次調用buffer的clear()方法,復原limit、position的位置 buffer.clear(); // 建立解碼器(CharsetDecoder)對象 CharsetDecoder decoder = charset.newDecoder(); // 使用解碼器將ByteBuffer轉換成CharBuffer CharBuffer charBuffer = decoder.decode(buffer); // CharBuffer的toString方法能夠獲取對應的字符串 System.out.println(charBuffer); } catch (IOException ex) { ex.printStackTrace(); } } }
API:java.nio.channels.Channels
API:java.nio.channels.FileChannel
Selector(選擇器)是Java NIO中可以檢測一到多個NIO通道,並可以知曉通道是否爲諸如讀寫事件作好準備的組件。這樣,一個單獨的線程能夠管理多個channel,從而管理多個網絡鏈接。
僅用單個線程來處理多個Channels的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道。對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好。
可是,須要記住,現代的操做系統和CPU在多任務方面表現的愈來愈好,因此多線程的開銷隨着時間的推移,變得愈來愈小了。實際上,若是一個CPU有多個內核,不使用多任務多是在浪費CPU能力。無論怎麼說,關於那種設計的討論應該放在另外一篇不一樣的文章中。在這裏,只要知道使用Selector可以處理多個通道就足夠了。
下面是單線程使用一個Selector處理3個channel的示例:
//一、經過調用Selector.open()方法建立一個Selecto Selector sel = Selector.open(); //二、向Selector註冊通道 channel.configureBlocking(false); SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
與Selector一塊兒使用時,Channel必須處於非阻塞模式下。這意味着不能將FileChannel與Selector一塊兒使用,由於FileChannel不能切換到非阻塞模式,而套SocketChannel能夠。
更完整實例以下:
// 1. 建立Selector對象 Selector sel = Selector.open(); // 2. 向Selector對象綁定通道 // a. 建立可選擇通道,並配置爲非阻塞模式 ServerSocketChannel server = ServerSocketChannel.open(); server.configureBlocking(false); // b. 綁定通道到指定端口 ServerSocket socket = server.socket(); InetSocketAddress address = new InetSocketAddress(port); socket.bind(address); // c. 向Selector中註冊感興趣的事件 server.register(sel, SelectionKey.OP_ACCEPT); return sel; // 3. 處理事件 try { while(true) { // 該調用會阻塞,直到至少有一個事件就緒、準備發生 selector.select(); // 一旦上述方法返回,線程就能夠處理這些事件 Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iter = keys.iterator(); while (iter.hasNext()) { SelectionKey key = (SelectionKey) iter.next(); iter.remove(); process(key); } } } catch (IOException e) { e.printStackTrace(); }
【1】:《瘋狂Java講義》
【2】:廖雪峯的官方網站:File對象
【3】:廖雪峯的官方網站:InputStream
【4】:廖雪峯的官方網站:Reader
【5】:廖雪峯的官方網站:Writer
【6】:【一圖勝千言】java流IO超詳細思惟導圖 含xmind文件
【7】:Java.IO層次體系結構
【8】:Java NIO系列教程(一) Java NIO 概述
【9】:Java NIO系列教程(六) Selector
【10】:Java:帶你全面瞭解神祕的Java NIO
【11】:Java基礎:攻破JAVA NIO技術壁壘1
【12】:Java基礎:攻破JAVA NIO技術壁壘2