Java 的 I/O 大概能夠分紅如下幾類:java
File 類能夠用於表示文件和目錄的信息,可是它不表示文件的內容。數組
遞歸地列出一個目錄下全部文件:緩存
public static void listAllFiles(File dir) { if (dir == null || !dir.exists()) { return; } if (dir.isFile()) { System.out.println(dir.getName()); return; } for (File file : dir.listFiles()) { listAllFiles(file); } }
從 Java7 開始,可使用 Paths 和 Files 代替 File。服務器
public static void copyFile(String src, String dist) throws IOException { FileInputStream in = new FileInputStream(src); FileOutputStream out = new FileOutputStream(dist); byte[] buffer = new byte[20 * 1024]; int cnt; // read() 最多讀取 buffer.length 個字節 // 返回的是實際讀取的個數 // 返回 -1 的時候表示讀到 eof,即文件尾 while ((cnt = in.read(buffer, 0, buffer.length)) != -1) { out.write(buffer, 0, cnt); } in.close(); out.close(); }
Java I/O 使用了裝飾者模式來實現。以 InputStream 爲例,網絡
實例化一個具備緩存功能的字節流對象時,只須要在 FileInputStream 對象上再套一層 BufferedInputStream 對象便可。app
FileInputStream fileInputStream = new FileInputStream(filePath); BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
DataInputStream 裝飾者提供了對更多數據類型進行輸入的操做,好比 int、double 等基本類型。socket
編碼就是把字符轉換爲字節,而解碼是把字節從新組合成字符。ide
若是編碼和解碼過程使用不一樣的編碼方式那麼就出現了亂碼。函數
UTF-16be 中的 be 指的是 Big Endian,也就是大端。相應地也有 UTF-16le,le 指的是 Little Endian,也就是小端。性能
Java 的內存編碼使用雙字節編碼 UTF-16be,這不是指 Java 只支持這一種編碼方式,而是說 char 這種類型使用 UTF-16be 進行編碼。char 類型佔 16 位,也就是兩個字節,Java 使用這種雙字節編碼是爲了讓一箇中文或者一個英文都能使用一個 char 來存儲。
String 能夠當作一個字符序列,能夠指定一個編碼方式將它編碼爲字節序列,也能夠指定一個編碼方式將一個字節序列解碼爲 String。
String str1 = "中文"; byte[] bytes = str1.getBytes("UTF-8"); String str2 = new String(bytes, "UTF-8"); System.out.println(str2);
在調用無參數 getBytes() 方法時,默認的編碼方式不是 UTF-16be。雙字節編碼的好處是可使用一個 char 存儲中文和英文,而將 String 轉爲 bytes[] 字節數組就再也不須要這個好處,所以也就再也不須要雙字節編碼。getBytes() 的默認編碼方式與平臺有關,通常爲 UTF-8。
byte[] bytes = str1.getBytes();
不論是磁盤仍是網絡傳輸,最小的存儲單元都是字節,而不是字符。可是在程序中操做的一般是字符形式的數據,所以須要提供對字符進行操做的方法。
public static void readFileContent(String filePath) throws IOException { FileReader fileReader = new FileReader(filePath); BufferedReader bufferedReader = new BufferedReader(fileReader); String line; while ((line = bufferedReader.readLine()) != null) { System.out.println(line); } // 裝飾者模式使得 BufferedReader 組合了一個 Reader 對象 // 在調用 BufferedReader 的 close() 方法時會去調用 Reader 的 close() 方法 // 所以只要一個 close() 調用便可 bufferedReader.close(); }
序列化就是將一個對象轉換成字節序列,方便存儲和傳輸。
不會對靜態變量進行序列化,由於序列化只是保存對象的狀態,靜態變量屬於類的狀態。
序列化的類須要實現 Serializable 接口,它只是一個標準,沒有任何方法須要實現,可是若是不去實現它的話而進行序列化,會拋出異常。
public static void main(String[] args) throws IOException, ClassNotFoundException { A a1 = new A(123, "abc"); String objectFile = "file/a1"; ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile)); objectOutputStream.writeObject(a1); objectOutputStream.close(); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile)); A a2 = (A) objectInputStream.readObject(); objectInputStream.close(); System.out.println(a2); } private static class A implements Serializable { private int x; private String y; A(int x, String y) { this.x = x; this.y = y; } @Override public String toString() { return "x = " + x + " " + "y = " + y; } }
transient 關鍵字可使一些屬性不會被序列化。
ArrayList 中存儲數據的數組 elementData 是用 transient 修飾的,由於這個數組是動態擴展的,並非全部的空間都被使用,所以就不須要全部的內容都被序列化。經過重寫序列化和反序列化方法,使得能夠只序列化數組中有內容的那部分數據。
private transient Object[] elementData;
Java 中的網絡支持:
沒有公有的構造函數,只能經過靜態方法來建立實例。
InetAddress.getByName(String host); InetAddress.getByAddress(byte[] address);
能夠直接從 URL 中讀取字節流數據。
public static void main(String[] args) throws IOException { URL url = new URL("http://www.baidu.com"); /* 字節流 */ InputStream is = url.openStream(); /* 字符流 */ InputStreamReader isr = new InputStreamReader(is, "utf-8"); /* 提供緩存功能 */ BufferedReader br = new BufferedReader(isr); String line; while ((line = br.readLine()) != null) { System.out.println(line); } br.close(); }
新的輸入/輸出 (NIO) 庫是在 JDK 1.4 中引入的,彌補了原來的 I/O 的不足,提供了高速的、面向塊的 I/O。
I/O 與 NIO 最重要的區別是數據打包和傳輸的方式,I/O 以流的方式處理數據,而 NIO 以塊的方式處理數據。
面向流的 I/O 一次處理一個字節數據:一個輸入流產生一個字節數據,一個輸出流消費一個字節數據。爲流式數據建立過濾器很是容易,連接幾個過濾器,以便每一個過濾器只負責複雜處理機制的一部分。不利的一面是,面向流的 I/O 一般至關慢。
面向塊的 I/O 一次處理一個數據塊,按塊處理數據比按流處理數據要快得多。可是面向塊的 I/O 缺乏一些面向流的 I/O 所具備的優雅性和簡單性。
I/O 包和 NIO 已經很好地集成了,java.io.* 已經以 NIO 爲基礎從新實現了,因此如今它能夠利用 NIO 的一些特性。例如,java.io.* 包中的一些類包含以塊的形式讀寫數據的方法,這使得即便在面向流的系統中,處理速度也會更快。
1. 通道
通道 Channel 是對原 I/O 包中的流的模擬,能夠經過它讀取和寫入數據。
通道與流的不一樣之處在於,流只能在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類),而通道是雙向的,能夠用於讀、寫或者同時用於讀寫。
通道包括如下類型:
2. 緩衝區
發送給一個通道的全部數據都必須首先放到緩衝區中,一樣地,從通道中讀取的任何數據都要先讀到緩衝區中。也就是說,不會直接對通道進行讀寫數據,而是要先通過緩衝區。
緩衝區實質上是一個數組,但它不只僅是一個數組。緩衝區提供了對數據的結構化訪問,並且還能夠跟蹤系統的讀/寫進程。
緩衝區包括如下類型:
狀態變量的改變過程舉例:
新建一個大小爲 8 個字節的緩衝區,此時 position 爲 0,而 limit = capacity = 8。capacity 變量不會改變,下面的討論會忽略它。
從輸入通道中讀取 5 個字節數據寫入緩衝區中,此時 position 爲 5,limit 保持不變。
在將緩衝區的數據寫到輸出通道以前,須要先調用 flip() 方法,這個方法將 limit 設置爲當前 position,並將 position 設置爲 0。
從緩衝區中取 4 個字節到輸出緩衝中,此時 position 設爲 4。
最後須要調用 clear() 方法來清空緩衝區,此時 position 和 limit 都被設置爲最初位置。
如下展現了使用 NIO 快速複製文件的實例:
public static void fastCopy(String src, String dist) throws IOException { /* 得到源文件的輸入字節流 */ FileInputStream fin = new FileInputStream(src); /* 獲取輸入字節流的文件通道 */ FileChannel fcin = fin.getChannel(); /* 獲取目標文件的輸出字節流 */ FileOutputStream fout = new FileOutputStream(dist); /* 獲取輸出字節流的文件通道 */ FileChannel fcout = fout.getChannel(); /* 爲緩衝區分配 1024 個字節 */ ByteBuffer buffer = ByteBuffer.allocateDirect(1024); while (true) { /* 從輸入通道中讀取數據到緩衝區中 */ int r = fcin.read(buffer); /* read() 返回 -1 表示 EOF */ if (r == -1) { break; } /* 切換讀寫 */ buffer.flip(); /* 把緩衝區的內容寫入輸出文件中 */ fcout.write(buffer); /* 清空緩衝區 */ buffer.clear(); } }
NIO 經常被叫作非阻塞 IO,主要是由於 NIO 在網絡通訊中的非阻塞特性被普遍使用。
NIO 實現了 IO 多路複用中的 Reactor 模型,一個線程 Thread 使用一個選擇器 Selector 經過輪詢的方式去監聽多個通道 Channel 上的事件,從而讓一個線程就能夠處理多個事件。
經過配置監聽的通道 Channel 爲非阻塞,那麼當 Channel 上的 IO 事件還未到達時,就不會進入阻塞狀態一直等待,而是繼續輪詢其它 Channel,找到 IO 事件已經到達的 Channel 執行。
由於建立和切換線程的開銷很大,所以使用一個線程來處理多個事件而不是一個線程處理一個事件,對於 IO 密集型的應用具備很好地性能。
應該注意的是,只有套接字 Channel 才能配置爲非阻塞,而 FileChannel 不能,爲 FileChannel 配置非阻塞也沒有意義。
1. 建立選擇器
Selector selector = Selector.open();
2. 將通道註冊到選擇器上
ServerSocketChannel ssChannel = ServerSocketChannel.open(); ssChannel.configureBlocking(false); ssChannel.register(selector, SelectionKey.OP_ACCEPT);
通道必須配置爲非阻塞模式,不然使用選擇器就沒有任何意義了,由於若是通道在某個事件上被阻塞,那麼服務器就不能響應其它事件,必須等待這個事件處理完畢才能去處理其它事件,顯然這和選擇器的做用背道而馳。
在將通道註冊到選擇器上時,還須要指定要註冊的具體事件,主要有如下幾類:
它們在 SelectionKey 的定義以下:
public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4;
能夠看出每一個事件能夠被當成一個位域,從而組成事件集整數。例如:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
3. 監聽事件
int num = selector.select();
使用 select() 來監聽到達的事件,它會一直阻塞直到有至少一個事件到達。
4. 獲取到達的事件
Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = keys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { // ... } else if (key.isReadable()) { // ... } keyIterator.remove(); }
5. 事件循環
由於一次 select() 調用不能處理完全部的事件,而且服務器端有可能須要一直監聽事件,所以服務器端處理事件的代碼通常會放在一個死循環內。
while (true) { int num = selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = keys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { // ... } else if (key.isReadable()) { // ... } keyIterator.remove(); } }
public class NIOServer { public static void main(String[] args) throws IOException { Selector selector = Selector.open(); ServerSocketChannel ssChannel = ServerSocketChannel.open(); ssChannel.configureBlocking(false); ssChannel.register(selector, SelectionKey.OP_ACCEPT); ServerSocket serverSocket = ssChannel.socket(); InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888); serverSocket.bind(address); while (true) { selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = keys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel(); // 服務器會爲每一個新鏈接建立一個 SocketChannel SocketChannel sChannel = ssChannel1.accept(); sChannel.configureBlocking(false); // 這個新鏈接主要用於從客戶端讀取數據 sChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel sChannel = (SocketChannel) key.channel(); System.out.println(readDataFromSocketChannel(sChannel)); sChannel.close(); } keyIterator.remove(); } } } private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); StringBuilder data = new StringBuilder(); while (true) { buffer.clear(); int n = sChannel.read(buffer); if (n == -1) { break; } buffer.flip(); int limit = buffer.limit(); char[] dst = new char[limit]; for (int i = 0; i < limit; i++) { dst[i] = (char) buffer.get(i); } data.append(dst); buffer.clear(); } return data.toString(); } } public class NIOClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 8888); OutputStream out = socket.getOutputStream(); String s = "hello world"; out.write(s.getBytes()); out.close(); } }
內存映射文件 I/O 是一種讀和寫文件數據的方法,它能夠比常規的基於流或者基於通道的 I/O 快得多。
向內存映射文件寫入多是危險的,只是改變數組的單個元素這樣的簡單操做,就可能會直接修改磁盤上的文件。修改數據與將數據保存到磁盤是沒有分開的。
下面代碼行將文件的前 1024 個字節映射到內存中,map() 方法返回一個 MappedByteBuffer,它是 ByteBuffer 的子類。所以,能夠像使用其餘任何 ByteBuffer 同樣使用新映射的緩衝區,操做系統會在須要時負責執行映射。
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
NIO 與普通 I/O 的區別主要有如下兩點: