備戰-Java IO
備戰-Java IO
君如載酒須盡醉,醉來不復思天涯。
簡介:備戰-Java IO。
一、概述
Java 的 I/O 大概可以分成以下幾類:
- 磁盤操作:File
- 位元組操作:InputStream 和 OutputStream
- 字符操作:Reader 和 Writer
- 對象操作:Serializable
- 網絡操作:Socket
- 新的輸入/輸出:NIO
二、磁盤操作
File 類可以用於表示文件和目錄的信息,但是它不表示文件的內容。
遞歸地列出一個目錄下所有文件:


1 public class QrController {
2 public static void main(String[] args) {
3 File file = new File("/TJT/code");
4 listAllFile(file);
5 }
6
7 public static void listAllFile(File dir){
8 if (!dir.exists() || dir == null){
9 return;
10 }
11 if (dir.isFile()){
12 System.out.println(dir.getName());
13 return;
14 }
15 for (File file : dir.listFiles()){
16 listAllFile(file);
17 }
18 }
19 }
View Code
從 Java7 開始,可以使用 Paths 和 Files 代替 File。
三、位元組操作
實現文件複製


1 public static void copyFile(String src, String dist) throws IOException {
2 FileInputStream in = new FileInputStream(src);
3 FileOutputStream out = new FileOutputStream(dist);
4
5 byte[] buffer = new byte[20 * 1024];
6 int cnt;
7
8 // read() 最多讀取 buffer.length 個位元組
9 // 返回的是實際讀取的個數
10 // 返回 -1 的時候表示讀到 eof,即文件尾
11 while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
12 out.write(buffer, 0, cnt);
13 }
14
15 in.close();
16 out.close();
17 }
View Code
裝飾者模式
Java I/O 使用了裝飾者模式來實現。以 InputStream 為例,
- InputStream 是抽象組件;
- FileInputStream 是 InputStream 的子類,屬於具體組件,提供了位元組流的輸入操作;
- FilterInputStream 屬於抽象裝飾者,裝飾者用於裝飾組件,為組件提供額外的功能。例如 BufferedInputStream 為 FileInputStream 提供緩存的功能。
實例化一個具有緩存功能的位元組流對象時,只需要在 FileInputStream 對象上再套一層 BufferedInputStream 對象即可。
1 FileInputStream fileInputStream = new FileInputStream(filePath);
2 BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
DataInputStream 裝飾者提供了對更多數據類型進行輸入的操作,比如 int、double 等基本類型。
四、字符操作
編碼與解碼
編碼就是把字符轉換為位元組,而解碼是把位元組重新組合成字符。
如果編碼和解碼過程使用不同的編碼方式那麼就出現了亂碼。
- GBK 編碼中,中文字符占 2 個位元組,英文字符占 1 個位元組;
- UTF-8 編碼中,中文字符占 3 個位元組,英文字符占 1 個位元組;
- UTF-16be 編碼中,中文字符和英文字符都占 2 個位元組。
UTF-16be 中的 be 指的是 Big Endian,也就是大端。相應地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
Java 的內存編碼使用雙位元組編碼 UTF-16be,這不是指 Java 只支持這一種編碼方式,而是說 char 這種類型使用 UTF-16be 進行編碼。char 類型占 16 位,也就是兩個位元組,Java 使用這種雙位元組編碼是為了讓一個中文或者一個英文都能使用一個 char 來存儲。
String 可以看成一個字符序列,可以指定一個編碼方式將它編碼為位元組序列,也可以指定一個編碼方式將一個位元組序列解碼為 String。
1 public static void main(String[] args) throws UnsupportedEncodingException {
2 String str1 = "深圳";
3 byte[] bytes = str1.getBytes("UTF-8");
4 System.out.println(bytes); // [B@4bec1f0c
5 String str2 = new String(bytes, "UTF-8");
6 System.out.println(str2); // 深圳
7 }
在調用無參數 getBytes() 方法時,默認的編碼方式不是 UTF-16be。雙位元組編碼的好處是可以使用一個 char 存儲中文和英文,而將 String 轉為 bytes[] 位元組數組就不再需要這個好處,因此也就不再需要雙位元組編碼。getBytes() 的默認編碼方式與平台有關,一般為 UTF-8。
byte[] bytes = str1.getBytes();
Reader 與 Writer
不管是磁盤還是網絡傳輸,最小的存儲單元都是位元組,而不是字符。但是在程序中操作的通常是字符形式的數據,因此需要提供對字符進行操作的方法。
- InputStreamReader 實現從位元組流解碼成字符流;
- OutputStreamWriter 實現字符流編碼成為位元組流。
實現逐行輸出文本文件的內容


1 public static void readFileContent(String filePath) throws IOException {
2
3 FileReader fileReader = new FileReader(filePath);
4 BufferedReader bufferedReader = new BufferedReader(fileReader);
5
6 String line;
7 while ((line = bufferedReader.readLine()) != null) {
8 System.out.println(line);
9 }
10
11 // 裝飾者模式使得 BufferedReader 組合了一個 Reader 對象
12 // 在調用 BufferedReader 的 close() 方法時會去調用 Reader 的 close() 方法
13 // 因此只要一個 close() 調用即可
14 bufferedReader.close();
15 }
View Code
五、對象操作
序列化
序列化就是將一個對象轉換成位元組序列,方便存儲和傳輸。
- 序列化:ObjectOutputStream.writeObject()
- 反序列化:ObjectInputStream.readObject()
不會對靜態變量進行序列化,因為序列化只是保存對象的狀態,靜態變量屬於類的狀態。
Serializable
序列化的類需要實現 Serializable 接口,它只是一個標準,沒有任何方法需要實現,但是如果不去實現它的話而進行序列化,會拋出異常。


1 public class QrController {
2 public static void main(String[] args) throws IOException, ClassNotFoundException {
3
4 A a1 = new A(123, "abc");
5 String objectFile = "/TJT/Code/Test.class";
6
7 ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
8 objectOutputStream.writeObject(a1);
9 objectOutputStream.close();
10
11 ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
12 A a2 = (A) objectInputStream.readObject();
13 objectInputStream.close();
14 System.out.println(a2);
15 }
16
17 private static class A implements Serializable {
18
19 private int x;
20 private String y;
21
22 A(int x, String y) {
23 this.x = x;
24 this.y = y;
25 }
26
27 @Override
28 public String toString() {
29 return "x = " + x + " " + "y = " + y;
30 }
31 }
32 }
View Code
transient
transient 關鍵字可以使一些屬性不會被序列化。
transient 關鍵字語法鏈接://www.cnblogs.com/taojietaoge/p/10260145.html
ArrayList 中存儲數據的數組 elementData 是用 transient 修飾的,因為這個數組是動態擴展的,並不是所有的空間都被使用,因此就不需要所有的內容都被序列化。通過重寫序列化和反序列化方法,使得可以只序列化數組中有內容的那部分數據。
private transient Object[] elementData;
六、網絡操作
Java 中的網絡支持:
- InetAddress:用於表示網絡上的硬件資源,即 IP 地址;
- URL:統一資源定位符;
- Sockets:使用 TCP 協議實現網絡通信;
- Datagram:使用 UDP 協議實現網絡通信。
InetAddress
沒有公有的構造函數,只能通過靜態方法來創建實例。
1 InetAddress.getByName(String host);
2 // InetAddress ipAddress = InetAddress.getByName(ip);// 指定獲取到的IP地址
3 InetAddress.getByAddress(byte[] address);
URL
可以直接從 URL 中讀取位元組流數據。


1 import java.io.BufferedReader;
2 import java.io.IOException;
3 import java.io.InputStream;
4 import java.io.InputStreamReader;
5 import java.net.URL;
6
7
8 public class User {
9 public static void main(String[] args) throws IOException {
10
11 URL url = new URL("//www.baidu.com");
12
13 /* 位元組流 */
14 InputStream is = url.openStream();
15
16 /* 字符流 */
17 InputStreamReader isr = new InputStreamReader(is, "utf-8");
18
19 /* 提供緩存功能 */
20 BufferedReader br = new BufferedReader(isr);
21
22 String line;
23 while ((line = br.readLine()) != null) {
24 System.out.println(line);
25 }
26
27 br.close();
28 }
29
30 }
View Code
Sockets
- ServerSocket:服務器端類
- Socket:客戶端類
- 服務器和客戶端通過 InputStream 和 OutputStream 進行輸入輸出。
Datagram
- DatagramSocket:通信類
- DatagramPacket:數據包類
七、NIO
新的輸入/輸出 (NIO) 庫是在 JDK 1.4 中引入的,彌補了原來的 I/O 的不足,提供了高速的、面向塊的 I/O;非阻塞IO,作為原始IO的補充,為了應對高性能高並發的應用場景。
流與塊
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 的子類),而通道是雙向的,可以用於讀、寫或者同時用於讀寫。
通道包括以下類型:
- FileChannel:從文件中讀寫數據;
- DatagramChannel:通過 UDP 讀寫網絡中數據;
- SocketChannel:通過 TCP 讀寫網絡中數據;
- ServerSocketChannel:可以監聽新進來的 TCP 連接,對每一個新進來的連接都會創建一個 SocketChannel。
2. 緩衝區
發送給一個通道的所有數據都必須首先放到緩衝區中,同樣地,從通道中讀取的任何數據都要先讀到緩衝區中。也就是說,不會直接對通道進行讀寫數據,而是要先經過緩衝區。
緩衝區實質上是一個數組,但它不僅僅是一個數組。緩衝區提供了對數據的結構化訪問,而且還可以跟蹤系統的讀/寫進程。
緩衝區包括以下類型:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
文件 NIO 實例
以下展示了使用 NIO 快速複製文件的實例:


1 public static void fastCopy(String src, String dist) throws IOException {
2
3 /* 獲得源文件的輸入位元組流 */
4 FileInputStream fin = new FileInputStream(src);
5
6 /* 獲取輸入位元組流的文件通道 */
7 FileChannel fcin = fin.getChannel();
8
9 /* 獲取目標文件的輸出位元組流 */
10 FileOutputStream fout = new FileOutputStream(dist);
11
12 /* 獲取輸出位元組流的文件通道 */
13 FileChannel fcout = fout.getChannel();
14
15 /* 為緩衝區分配 1024 個位元組 */
16 ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
17
18 while (true) {
19
20 /* 從輸入通道中讀取數據到緩衝區中 */
21 int r = fcin.read(buffer);
22
23 /* read() 返回 -1 表示 EOF */
24 if (r == -1) {
25 break;
26 }
27
28 /* 切換讀寫 */
29 buffer.flip();
30
31 /* 把緩衝區的內容寫入輸出文件中 */
32 fcout.write(buffer);
33
34 /* 清空緩衝區 */
35 buffer.clear();
36 }
37 }
View Code
選擇器
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. 將通道註冊到選擇器上
1 ServerSocketChannel ssChannel = ServerSocketChannel.open();
2 ssChannel.configureBlocking(false); // 將通道設置為非阻塞
3 ssChannel.register(selector, SelectionKey.OP_ACCEPT);
通道必須配置為非阻塞模式,否則使用選擇器就沒有任何意義了,因為如果通道在某個事件上被阻塞,那麼服務器就不能響應其它事件,必須等待這個事件處理完畢才能去處理其它事件,顯然這和選擇器的作用背道而馳。
在將通道註冊到選擇器上時,還需要指定要註冊的具體事件,主要有以下幾類:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
它們在 SelectionKey 的定義如下:
1 public static final int OP_READ = 1 << 0;
2 public static final int OP_WRITE = 1 << 2;
3 public static final int OP_CONNECT = 1 << 3;
4 public static final int OP_ACCEPT = 1 << 4;
可以看出每個事件可以被當成一個位域,從而組成事件集整數。例如:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
3. 監聽事件
int num = selector.select();
使用 select() 來監聽到達的事件,它會一直阻塞直到有至少一個事件到達。
4. 獲取到達的事件


1 Set<SelectionKey> keys = selector.selectedKeys();
2 Iterator<SelectionKey> keyIterator = keys.iterator();
3 while (keyIterator.hasNext()) {
4 SelectionKey key = keyIterator.next();
5 if (key.isAcceptable()) {
6 // do what you want to do
7 } else if (key.isReadable()) {
8 // do what u want to do
9 }
10 keyIterator.remove();
11 }
View Code
5. 事件循環
因為一次 select() 調用不能處理完所有的事件,並且服務器端有可能需要一直監聽事件,因此服務器端處理事件的代碼一般會放在一個死循環內。


1 while (true) {
2 int num = selector.select();
3 Set<SelectionKey> keys = selector.selectedKeys();
4 Iterator<SelectionKey> keyIterator = keys.iterator();
5 while (keyIterator.hasNext()) {
6 SelectionKey key = keyIterator.next();
7 if (key.isAcceptable()) {
8 // ...
9 } else if (key.isReadable()) {
10 // ...
11 }
12 keyIterator.remove();
13 }
14 }
View Code
套接字 NIO 實例


1 public class NIOServer {
2
3 public static void main(String[] args) throws IOException {
4
5 Selector selector = Selector.open();
6
7 ServerSocketChannel ssChannel = ServerSocketChannel.open();
8 ssChannel.configureBlocking(false);
9 ssChannel.register(selector, SelectionKey.OP_ACCEPT);
10
11 ServerSocket serverSocket = ssChannel.socket();
12 InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
13 serverSocket.bind(address);
14
15 while (true) {
16
17 selector.select();
18 Set<SelectionKey> keys = selector.selectedKeys();
19 Iterator<SelectionKey> keyIterator = keys.iterator();
20
21 while (keyIterator.hasNext()) {
22
23 SelectionKey key = keyIterator.next();
24
25 if (key.isAcceptable()) {
26
27 ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
28
29 // 服務器會為每個新連接創建一個 SocketChannel
30 SocketChannel sChannel = ssChannel1.accept();
31 sChannel.configureBlocking(false);
32
33 // 這個新連接主要用於從客戶端讀取數據
34 sChannel.register(selector, SelectionKey.OP_READ);
35
36 } else if (key.isReadable()) {
37
38 SocketChannel sChannel = (SocketChannel) key.channel();
39 System.out.println(readDataFromSocketChannel(sChannel));
40 sChannel.close();
41 }
42
43 keyIterator.remove();
44 }
45 }
46 }
47
48 private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
49
50 ByteBuffer buffer = ByteBuffer.allocate(1024);
51 StringBuilder data = new StringBuilder();
52
53 while (true) {
54
55 buffer.clear();
56 int n = sChannel.read(buffer);
57 if (n == -1) {
58 break;
59 }
60 buffer.flip();
61 int limit = buffer.limit();
62 char[] dst = new char[limit];
63 for (int i = 0; i < limit; i++) {
64 dst[i] = (char) buffer.get(i);
65 }
66 data.append(dst);
67 buffer.clear();
68 }
69 return data.toString();
70 }
71 }
View Code
1 public class NIOClient {
2 public static void main(String[] args) throws IOException {
3 Socket socket = new Socket("127.0.0.1", 8888);
4 OutputStream out = socket.getOutputStream();
5 String s = "do what you want to do";
6 out.write(s.getBytes());
7 out.close();
8 }
9 }
內存映射文件
內存映射文件 I/O 是一種讀和寫文件數據的方法,它可以比常規的基於流或者基於通道的 I/O 快得多。
向內存映射文件寫入可能是危險的,只是改變數組的單個元素這樣的簡單操作,就可能會直接修改磁盤上的文件。修改數據與將數據保存到磁盤是沒有分開的。
下面代碼行將文件的前 1024 個位元組映射到內存中,map() 方法返回一個 MappedByteBuffer,它是 ByteBuffer 的子類。因此,可以像使用其他任何 ByteBuffer 一樣使用新映射的緩衝區,操作系統會在需要時負責執行映射。
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
對比
NIO 與普通 I/O 的區別主要有以下兩點:
- NIO 是非阻塞的;
- NIO 面向塊,I/O 面向流。
君如載酒須盡醉
醉來不復思天涯