文章目錄
Java 的 IO 通過 java.io 包下的類和接口來支援, 在 java.io 包下主要包括輸入、 輸出兩種 10 流, 每種輸入、 輸出流又可分為位元組流和字元流兩大類。 其中位元組流以位元組為機關來處理輸入、 輸出操作, 而字元流則以字元來處理輸入、 輸出操作。
Java的标準庫java.io提供了File對象來操作檔案和目錄。
File 類可以使用檔案路徑字元串來建立 File 執行個體, 該檔案路徑字元串既可以是絕對路徑, 也可以是相對路徑。 在預設情況下, 系統總是依據使用者的工作路徑來解釋相對路徑。
建立了File對象後, 就可以調用 File 對象的方法來通路, File 類提供了很多方法來操作檔案和目錄, 下面列出一些比較常用的方法。
- String getName(): 傳回此 File 對象所表示的檔案名或路徑名( 如果是路徑, 則傳回最後一級子路徑名)。
- String getPath(): 傳回此 File 對象所對應的路徑名。
- File getAbsoluteFile(): 傳回此 Hie 對象的絕對路徑。
- String getAbsolutePath(): 傳回此 Hie 對象所對應的絕對路徑名。
- String getParent(): 傳回此 File 對象所對應目錄( 最後一級子目錄) 的父目錄名。
- boolean renameTo(File newName): 重命名此 File 對象所對應的檔案或目錄, 如果重命名成功,則傳回 true; 否則傳回 false。
- boolean exists(): 判斷 File 對象所對應的檔案或目錄是否存在。
- boolean canWrite(): 判斷 File 對象所對應的檔案和目錄是否可寫。
- boolean canRead(): 判斷 File 對象所對應的檔案和目錄是否可讀。
- boolean isFile(): 判斷 File 對象所對應的是否是檔案, 而不是目錄。
- boolean isDirectory(): 判斷 File 對象所對應的是否是目錄, 而不是檔案。
- boolean isAbsolute(): 判斷 Hie 對象所對應的檔案或目錄是否是絕對路徑。 該方法消除了不同平台的差異, 可以直接判斷 File 對象是否為絕對路徑。 在 UNIX/Linux/BSD 等系統上, 如果路徑名開頭是一條斜線( /), 則表明該 File 對象對應一個絕對路徑; 在 Windows 等系統上, 如果路徑開頭是盤符, 則說明它是一個絕對路徑。
- long lastModified(): 傳回檔案的最後修改時間。
- long length(): 傳回檔案内容的長度。
- boolean createNewFile(): 當此 File 對象所對應的檔案不存在時, 該方法将建立一個該File對象所指定的新檔案, 如果建立成功則傳回 true; 否則傳回 false。
- boolean delete(): 删除 Hie 對象所對應的檔案或路徑。
- static File createTempFile(String prefix,String suffix): 在預設的臨時檔案目錄中建立一個臨時的空檔案, 使用給定字首、 系統生成的随機數和給定字尾作為檔案名。 這是一個靜态方法,可以直接通過 File 類來調用。 prefix 參數必須至少是 3 位元組長。 建議字首使用一個短的、 有意義的字元串, 比如 "hjb” 或 ”mail”。 suffix 參數可以為 null, 在這種情況下, 将使用預設的字尾“.temp”。
- static File createTempFile(String prefix,String suffix,File directory): 在directory 所指定的目錄中建立一個臨時的空檔案, 使用給定字首、 系統生成的随機數和給定字尾作為檔案名。 這是一個靜态方法, 可以直接通過 File 類來調用。
- void deleteOnExit(): 注冊一個删除鈎子, 指定當 Java 虛拟機退出時, 删除 File 對象所對應的檔案和目錄。
- boolean mkdir(): 試圖建立一個 File 對象所對應的目錄, 如果建立成功, 則傳回 true; 否則傳回 false。 調用該方法時 Hie 對象必須對應一個路徑, 而不是一個檔案。
- String[] list(): 列出 File 對象的所有子檔案名和路徑名, 傳回 String 數組。
- File[] listFiles(): 列出 Hie 對象的所有子檔案和路徑, 傳回 Hie 數組。
- static File[] listRoots(): 列出系統所有的根路徑。 這是一個靜态方法, 可以直接通過 Hie 類來調用。
面程式以幾個簡單方法來測試一下 File 類的功能:
FileTest.java
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()方法會列出該子目錄或者檔案。
FilenameFilterTest.java
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 所示的資料流向, 資料從記憶體到硬碟, 通常稱為輸出流——也就是說, 這裡的輸入、 輸出都是從程式運作所在記憶體的角度來劃分的。
圖1:資料從記憶體到硬碟

對于如圖 2 所示的資料流向, 資料從伺服器通過網絡流向用戶端, 在這種情況下, Server 端的記憶體負責将資料輸出到網絡裡, 是以 Server 端的程式使用輸出流; Client 端的記憶體負責從網絡裡讀取資料, 是以 Client 端的程式應該使用輸入流。
圖2:資料從伺服器到用戶端
位元組流和字元流的用法幾乎完全一樣, 差別在于位元組流和字元流所操作的資料單元不同操作的資料單元是 8 位的位元組, 而字元流操作的資料單元是 16 位的字元。
按照流的角色來分, 可以分為節點流和處理流。
可以從/向一個特定的IO裝置( 如磁盤、 網絡) 讀/寫資料的流, 稱為節點流, 節點流也被稱為低級流( Low Level Stream)。 圖 3 顯示了節點流示意圖。
圖3:節點流示意圖
從圖 3 中可以看出, 當使用節點流進行輸入/輸出時, 程式直接連接配接到實際的資料源, 和實際的輸入/輸出節點連接配接。
處理流則用于對一個己存在的流進行連接配接或封裝, 通過封裝後的流來實作資料讀/寫功能。 處理流也被稱為進階流。 圖 4 顯示了處理流示意圖。
圖4:處理流示意圖
從圖 4 中可以看出, 當使用處理流進行輸入/輸出時, 程式并不會直接連接配接到實際的資料源, 沒有和實際的輸入/輸出節點連接配接。 使用處理流的一個明顯好處是, 隻要使用相同的處理流, 程式就可以采用完全相同的輸入/輸出代碼來通路不同的資料源, 随着處理流所包裝節點流的變化, 程式實際所通路的資料源也相應地發生變化。
ava 把所有裝置裡的有序資料抽象成流模型, 簡化了輸入/輸出處理, 了解了流的概念模型也就了解了Java IO。
Java 的 IO流的 40 多個類都是從如下 4 個抽象基類派生的:
- InputStream/Reader: 所有輸入流的基類, 前者是位元組輸入流, 後者是字元輸入流。
- OutputStream/Writer: 所有輸出流的基類, 前者是位元組輸出流, 後者是字元輸出流。
通過使用處理流, Java 程式無須理會輸入/輸出節點是磁盤、 網絡還是其他的輸入/輸出裝置, 程式隻要将這些節點流包裝成處理流, 就可以使用相同的輸入/輸出代碼來讀寫不同的輸入/輸出裝置的資料。
圖5:輸入流模型圖
圖6:輸出流模型圖
節流和字元流放的操作方式幾乎完全一樣, 差別隻是操作的資料單元不同。
InputStream 和 Reader 是所有輸入流的抽象基類, 本身并不能建立執行個體來執行輸入, 但它們是所有輸入流的模闆, 是以它們的方法是所有輸入流都可使用的方法。
在 InputStream 裡包含如下三個方法:
- int read(): 從輸入流中讀取單個位元組( 相當于從圖 5 所示的水管中取出一滴水), 傳回所讀取的位元組資料( 位元組資料可直接轉換為 int 類型)。
- int read(byte[] b): 從輸入流中最多讀取 b.length 個位元組的資料, 并将其存儲在位元組數組 b 中,傳回實際讀取的位元組數。
- int read(byte[]b,int off,int len): 從輸入流中最多讀取 len 個位元組的資料, 并将其存儲在數組 b中, 放入數組 b 中時, 并不是從數組起點幵始, 而是從 uff 位置開始, 傳回實際讀取的位元組數。
在 Reader 裡包含如下三個方法:
- int read(): 從輸入流中讀取單個字元( 相當于從圖 5 所示的水管中取出一滴水), 傳回所讀取的字元資料( 字元資料可直接轉換為 int 類型)。
- int read(char[] cbuf): 從輸入流中最多讀取 cbuf.length 個字元的資料, 并将其存儲在字元數組cbuf 中, 傳回實際讀取的字元數。
- int read(char[]cbuf,int off,int len): 從輸入流中最多讀取 len 個字元的資料, 并将其存儲在字元數組 cbuf 中, 放入數組 cbuf 中時, 并不是從數組起點開始, 而是從 off 位置開始, 傳回實際讀取的字元數。
InputStream 和 Reader 都是抽象類, 本身不能建立執行個體, 但它們分别有一個用于讀取檔案的輸入流: FilelnputStream 和 FileReader, 它們都是節點流—會直接和指定檔案關聯。
下面程式為 FilelnputStream 來讀取自身的效果執行個體:
FileInputStreamTest.java
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 來讀取檔案本身執行個體:
FileReaderTest.java
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();
}
}
}
java.io.InputStream java.io.FilterInputStream java.io.Reader java.io.FileReader
OntputStream 和 Writer 也非常相似, 它們采用如圖 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 的字元輸出到指定輸出流中。
下面程式使用 FilelnputStream 來執行輸入, 并使用 FileOutputStream 來執行輸出, 用以實作複制FileOutputStreamTest.java 檔案的功能。
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();
}
}
}
使用 Java 的 10 流執行輸出時, 不要忘記關閉輸出流, 關閉輸出流除可以保證流的物 理資源被回收之外, 可能還可以将輸出流緩沖區中的資料 flush 到實體節點裡 ( 因為在執行 close()方法之前, 自動執行輸出流的 flush()方法 )。
如果希望直接輸出字元串内容, 則使用 Writer 會有更好的效果:
FileWriterTest.java
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();
}
}
}
java.io.OutputStream java.io.FileOutputStream java.io.Writer java.io.FilterWriter
Java 的輸入/輸出流體系提供了近 40 個類, 這些類看上去雜亂而沒有規律, 但如果将其按功能進行分類, 則不難發現其是非正常律的。 表 1 顯示了 Java 輸入/輸出流體系中常用的流分類。
表1:Java 輸入/輸出流體系中常用的流分類
從表 1 中可以看出, Java 的輸入/輸出流體系之是以如此複雜, 主要是因為 Java 為了實作更好的設計, 它把 IO流按功能分成了許多類, 而每類中又分别提供了位元組流和字元流( 當然有些流無法提供位元組流, 有些流無法提供字元流), 位元組流和字元流裡又分别提供了輸入流和輸出流兩大類, 是以導緻整個輸入/輸出流體系格外複雜。
圖7:Java 輸入/輸出流體系思維導圖
通常來說, 位元組流的功能比字元流的功能強大, 因為計算機裡所有的資料都是二進制的, 而位元組流可以處理所有的二進制檔案—但問題是, 如果使用位元組流來處理文本檔案, 則需要使用合适的方式把這些位元組轉換成字元, 這就增加了程式設計的複雜度。 是以通常有一個規則: 如果進行輸入/輸出的内容是文本内容, 則應該考慮使用字元流; 如果進行輸入/輸出的内容是二進制内容, 則應該考慮使用位元組流。
圖8:Java 輸入/輸出流繼承結構
表 1 僅僅總結了輸入/輸出流體系中位于 java.io 包下的流, 還有一些諸如 AudioInputStream、CipherlnputStream、 DeflaterlnputStream、ZipInputStream 等具有通路音頻檔案、 加密/解密、 壓縮/解壓等功能的位元組流, 它們具有特殊的功能, 位于 JDK 的其他包下。
4 個基類使用起來有些煩瑣。 如果希望簡化程式設計, 可以借助于處理流。
下面程式使用 PrintStream 處理流來包裝 OutputStream, 使用處理流後的輸出流在輸出時将更加友善。
PrintStreamTest.java
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。
程式使用處理流, 通常隻需要在建立處理流時傳入一個節點流作為構造器參數即可, 這樣建立的處理流就是包裝了該節點流的處理流。
java.io.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.io.InputStreamReader java.io.InputStreamWriter
對象序列化的目标是将對象儲存到磁盤中, 或允許在網絡中直接傳輸對象。 對象序列化機制允許把記憶體中的 Java 對象轉換成平台無關的二進制流, 進而允許把這種二進制流持久地儲存在磁盤上, 通過網絡将這種二進制流傳輸到另一個網絡節點。 其他程式一旦獲得了這種二進制流( 無論是從磁盤中擷取的, 還是通過網絡擷取的), 都可以将這種二進制流恢複成原來的 Java 對象。
序列化機制允許将實作序列化的 Java 對象轉換成位元組序列, 這些位元組序列可以儲存在磁盤上, 或通過網絡傳輸, 以備以後重新恢複成原來的對象。 序列化機制使得對象可以脫離程式的運作而獨立存在。
對象的序列化 ( Serialize ) 指将一個 Java 對象寫入 IO流中, 與此對應的是, 對象的反序列化(Deserialize) 則指從 IO 流中恢複該 Java 對象。
如果需要讓某個對象支援序列化機制, 則必須讓它的類是可序列化的 (serializable )o 為了讓某個
類是可序列化的, 該類必須實作如下兩個接口之一。
- Serializable
- Extemalizable
Java 的很多類己經實作了 Serializable, 該接口是一個标記接口, 實作該接口無須實作任何方法, 它隻是表明該類的執行個體是可序列化的。
所有可能在網絡上傳輸的對象的類都應該是可序列化的, 否則程式将會出現異常, 比如 RMI( Remote Method Invoke, 即遠端方法調用, 是 Java EE 的基礎) 過程中的參數和傳回值; 所有需要儲存到磁盤裡的對象的類都必須可序列化, 比如 Web 應用中需要儲存到 HttpSession 或 ServletContext 屬性的 Java 對象。
因為序列化是 RMI 過程的參數和傳回值都必須實作的機制, 而 RMI 又是 Java EE 技術的基礎——所有的分布式應用常常需要跨平台、 跨網絡, 是以要求所有傳遞的參數、 傳回值必須實作序列化。 是以序列化機制是 Java EE 平台的基礎。 通常建議: 程式建立的每個 JavaBean 類都實作 Serializable。
使用 Serializable 來實作序列化, 隻需要讓目标類實作 Serializable 标記接口即可, 無須實作任何方法。
一旦某個類實作了 Serializable 接口, 該類的對象就是可序列化的, 程式可以通過如下兩個步驟來序列化該對象。
- 建立一個 ObjectOutputStream, 這個輸出流是一個處理流, 是以必須建立在其他節點流的基礎之上。 如下代碼所示:
// 建立一個 ObjectlnputStream 輸入流
ObjectlnputStream ois =new ObjectlnputStream(
new FilelnputStream("object.txt"));
- 調用 ObjectInputStream 對象的 readObject()方法讀取流中的對象, 該方法傳回一個 Object 類型的 Java 對象, 如果程式知道該 Java 對象的類型, 則可以将該對象強制類型轉換成其真實的類型。 如下代碼所示:
// 從輸入流中讀取一個 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 輸入流
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 關鍵字的效果相同)。
在序列化和反序列化過程中需要特殊處理的類應該提供如下特殊簽名的方法, 這些特殊的方法用以實作自定義序列化。
- private void writeObject(java.io.ObjectOutputStream out)throws IOException
- private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundExccption;
- private void readObjectNoData()throws ObjectStreamException;
- writeObject()方法負責寫入特定類的執行個體狀态, 以便相應的 readObject()方法可以恢複它。 通過重寫該方法, 程式員可以完全獲得對序列化機制的控制, 可以自主決定哪些執行個體變量需要序列化, 需要怎樣序列化。 在預設情況下, 該方法會調用 out.defaultWriteObject 來儲存 Java 對象的各執行個體變量, 進而可以實作序列化 Java 對象狀态的目的。
- readObject()方法負責從流中讀取并恢複對象執行個體變量, 通過重寫該方法, 程式員可以完全獲得對反序列化機制的控制, 可以自主決定需要反序列化哪些執行個體變量, 以及如何進行反序列化。 在預設情況下, 該方法會調用in.defaultReadObject 來恢複 Java 對象的非瞬态執行個體變量。 在通常情況下, readObject()方法與 writeObject()方法對應, 如果 writeObject()方法中對 Java 對象的執行個體變量進行了一些處理, 則應該在 readObjectO方法中對其執行個體變量進行相應的反處理, 以便正确恢複該對象。
- 當序列化流不完整時, readObjectNoData()方法可以用來正确地初始化反序列化的對象。 例如, 接收方使用的反序列化類的版本不同于發送方, 或者接收方版本擴充的類不是發送方版本擴充的類, 或者序列化流被篡改時, 系統都會調用 readObjectNoData()方法來初始化反序列化的對象。
下面的 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 将檔案或檔案的一段區域映射到記憶體中,這樣就可以像通路記憶體一樣來通路檔案了( 這種方式模拟了作業系統上的虛拟記憶體的概念), 通過這種方式來進行輸入/輸出比傳統的輸入/輸出要快得多。
圖9:NIO新特性
Java 中與新 IO 相關的包如下:
- java.nio 包: 主要包含各種與 Buffer 相關的類。
- java.nio.channels 包: 主要包含與 Channel 和 Selector 相關的類。
- java.nio.charset 包: 主要包含與字元集相關的類。
- java.nio.channels.spi 包: 主要包含與 Channel 相關的服務提供者程式設計接口。
-
java.nio.charset.spi 包: 包含與字元集相關的服務提供者程式設計接口。
圖10:NIO核心元件
Java Review(三十六、IO)File 類IO流概覽位元組流和字元流輸入/輸岀流體系對象序列化NIO
從内部結構上來看, Buffer 就像一個數組, 它可以儲存多個類型相同的資料。 Buffer是一個抽象類,其最常用的子類是 ByteBuffer, 它可以在底層位元組數組上進行 get/set 操作。 除 ByteBuffer 之外, 對應于其他基本資料類型( boolean除外) 都有相應的 Buffer 類: CharBuffer、 ShortBuffer、 IntBuffer、LongBuffer、FloatBuffer、 DoubleBuffer。
上面這些 Buffer類, 除 ByteBuffer 之外, 它們都采用相同或相似的方法來管理資料, 隻是各自管理的資料類型不同而己。 這些 Buffer 類都沒有提供構造器, 通過使用如下方法來得到一個 Buffer 對象。
- static XxxBufFer allocate(int capacity): 建立一個容量為 capacity 的 XxxBuffer 對象。
但實際使用較多的是 ByteBuffer 和 CharBuffer, 其他 Buffer 子類則較少用到。 其中 ByteBuffer 類還有一個子類: MappedByteBuffer, 它用于表示 Channel 将磁盤檔案的部分或全部内容映射到記憶體中後得到的結果, 通常MappedByteBuffer 對象由 Channel 的 map()方法傳回。
在 Buffer 中有三個重要的概念: 容量( capacity)、 界限 ( limit ) 和 位 置( position )。
- 容 量 (capacity): 緩沖區的容量 (capacity) 表 示 該 Buffer 的最大資料容量, 即最多可以存儲多少資料。 緩沖區的容量不可能為負值, 建立後不能改變。
- 界限 ( limit ): 第一個不應該被讀出或者寫入的緩沖區位置索引。 也就是說, 位于 limit 後的資料既不可被讀, 也不可被寫。
- 位置 ( position ): 用于指明下一個可以被讀出的或者寫入的緩沖區位置索引( 類似于IO流中的記錄指針)。 當使用 Buffer 從 Channel 中讀取資料時,position 的值恰好等于己經讀到了多少資料。 當剛剛建立一個 Buffer 對象時, 其position 為 0; 如 果 從 Channel 中讀取了 2 個 數 據 到該 Buffer 中, 則 position 為 2, 指向 Buffer 中 第 3 個( 第1個位置的索引為0) 位 置。
Buffer 裡還支援一個可選的标記 (mark, 類 似于傳統 IO流中的mark ), Buffer 允許直接将 position 定位到該 mark 處。 這些值滿足如下關系:
mark<position<limit<capacity
圖11:Buffer 讀入資料後的示意圖
Buffer 的主要作用就是裝入資料,然後輸出資料( 其作用類似于前面介紹的取水的“ 竹筒”), 開始時 Buffer的position 為 0, limit 為 capacity, 程式可通過 put()方法向 Buffer 中放入一些資料 ( 或者從 Channel 中擷取一些資料), 每放入一些資料, Buffer 的 position 相應地向後移動一些位置。
當 Buffer 裝入資料結束後, 調用 Buffer 的 flip()方法, 該方法将 limit 設定為 position 所在位置, 并将 position 設為 0, 這就使得 Buffer 的讀寫指針又移到了開始位置。 也就是說, Buffer 調用 flip()方法之後, Buffer 為輸出資料做好準備; 當 Buffer 輸出資料結束後, Buffer 調用 clear()方法, clear()方法不是清空 Buffer 的資料, 它僅僅将 position 置 為 0, 将 limit 置 為 capacity, 這 樣 為 再 次 向 Buffer 中裝入資料做好準備。
除此之外, Buffer 還包含如下一些常用的方法。
- int capacity(): 傳回 Buffer 的 capacity 大小。
- boolean hasRemaining(): 判斷目前位置 ( position ) 和界限 ( limit ) 之間是否還有元素可供處理。
- int limit(): 傳回 Buffer 的界限 ( limit ) 的位置。
- Buffer limit(int newLt): 重新設定界限 ( limit ) 的值, 并傳回一個具有新的 limit 的緩沖區對象。
- Buffer mark(): 設定 Buffer 的 mark 位置, 它隻能在 0 和位置 ( position ) 之間做 mark。
- int position(): 傳回 Buffer 中的 position 值。
- Buffer position(int newPs): 設定 Buffer 的 position, 并 返 回 position 被修改後的 Buffer 對象。
- int remaining(): 傳回目前位置和界限 ( limit ) 之間的元素個數。
- Buffer reset(): 将位置( position ) 轉到 mark 所在的位置。
- Buffer rewind(): 将位置( position ) 設定成 0, 取消設定的 mark。
除這些移動 position、 limit、 mark 的方法之外, Buffer 的所有子類還提供了兩個重要的方法: put()和 get()方法, 用于向 Buffer 中放入資料和從 Buffer 中取出資料。 當使用 put()和 get()方法放入、 取出資料時, Buffer 既支援對單個資料的通路, 也支援對批量資料的通路( 以數組作為參數)。
當使用 put()和 get()來通路 Buffer 中的資料時, 分為相對和絕對兩種:
- 相對( Relative ): 從 Buffer 的目前 position 處開始讀取或寫入資料, 然後将位置( position ) 的值按處理元素的個數增加。
- 絕對( Absolute): 直接根據索引向 Buffer 中讀取或寫入資料, 使用絕對方式通路 Buffer 裡的資料時, 并不會影響位置 ( position ) 的值。
下面程式為 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());
}
}
java.nio.Buffer
緩沖區為我們裝載了資料,但是資料的寫入和讀取并不能直接進行read()和write()這樣的系統調用,而是JVM為我們提供了一層對系統調用的封裝。而Channel可以用最小的開銷來通路作業系統本身的IO服務,這就是為什麼要有Channel的原因。
Channel 類似于傳統的流對象, 但與傳統的流對象有兩個主要差別。
- Channel 可以直接将指定檔案的部分或全部直接映射成 Buffer。
- 程式不能直接通路 Channel 中的資料, 包括讀取、 寫入都不行, Channel 隻能與 Buffer 進行互動。 也就是說, 如果要從 Channel 中取得資料, 必須先用 Buffer 從 Channel 中取出一些資料,然後讓程式從 Buffer 中取出這些資料; 如果要将程式中的資料寫入 Channel, —樣先讓程式将資料放入 Buffer 中, 程式再将 Buffer 裡的資料寫入 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();
}
}
}
java.nio.channels.Channels java.nio.channels.FileChannel
Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,并能夠知曉通道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的線程可以管理多個channel,進而管理多個網絡連接配接。
僅用單個線程來處理多個Channels的好處是,隻需要更少的線程來處理通道。事實上,可以隻用一個線程處理所有的通道。對于作業系統來說,線程之間上下文切換的開銷很大,而且每個線程都要占用系統的一些資源(如記憶體)。是以,使用的線程越少越好。
但是,需要記住,現代的作業系統和CPU在多任務方面表現的越來越好,是以多線程的開銷随着時間的推移,變得越來越小了。實際上,如果一個CPU有多個核心,不使用多任務可能是在浪費CPU能力。不管怎麼說,關于那種設計的讨論應該放在另一篇不同的文章中。在這裡,隻要知道使用Selector能夠處理多個通道就足夠了。
下面是單線程使用一個Selector處理3個channel的示例:
//1、通過調用Selector.open()方法建立一個Selecto
Selector sel = Selector.open();
//2、向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