IO 概括
# 一、概覽
Java 的 I/O 大概可以分成以下幾類:
- 磁盤操作:File
- 字節(jié)操作:InputStream 和 OutputStream
- 字符操作:Reader 和 Writer
- 對(duì)象操作:Serializable
- 網(wǎng)絡(luò)操作:Socket
- 新的輸入/輸出:NIO
# 二、磁盤操作
File 類可以用于表示文件和目錄的信息,但是它不表示文件的內(nèi)容。
遞歸地列出一個(gè)目錄下所有文件:
```java
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);
}
}
```
# 三、字節(jié)操作
## 實(shí)現(xiàn)文件復(fù)制
```java
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];
// read() 最多讀取 buffer.length 個(gè)字節(jié)
// 返回的是實(shí)際讀取的個(gè)數(shù)
// 返回 -1 的時(shí)候表示讀到 eof,即文件尾
while (in.read(buffer, 0, buffer.length) != -1) {
out.write(buffer);
}
in.close();
out.close();
}
```
## 裝飾者模式
Java I/O 使用了裝飾者模式來實(shí)現(xiàn)。以 InputStream 為例,
- InputStream 是抽象組件;
- FileInputStream 是 InputStream 的子類,屬于具體組件,提供了字節(jié)流的輸入操作;
- FilterInputStream 屬于抽象裝飾者,裝飾者用于裝飾組件,為組件提供額外的功能。例如 BufferedInputStream 為 FileInputStream 提供緩存的功能。
<div align="center"> <img src="../pics//DP-Decorator-java.io.png" width="500"/> </div><br>
實(shí)例化一個(gè)具有緩存功能的字節(jié)流對(duì)象時(shí),只需要在 FileInputStream 對(duì)象上再套一層 BufferedInputStream 對(duì)象即可。
```java
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
```
DataInputStream 裝飾者提供了對(duì)更多數(shù)據(jù)類型進(jìn)行輸入的操作,比如 int、double 等基本類型。
# 四、字符操作
## 編碼與解碼
編碼就是把字符轉(zhuǎn)換為字節(jié),而解碼是把字節(jié)重新組合成字符。
如果編碼和解碼過程使用不同的編碼方式那么就出現(xiàn)了亂碼。
- GBK 編碼中,中文字符占 2 個(gè)字節(jié),英文字符占 1 個(gè)字節(jié);
- UTF-8 編碼中,中文字符占 3 個(gè)字節(jié),英文字符占 1 個(gè)字節(jié);
- UTF-16be 編碼中,中文字符和英文字符都占 2 個(gè)字節(jié)。
UTF-16be 中的 be 指的是 Big Endian,也就是大端。相應(yīng)地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
Java 使用雙字節(jié)編碼 UTF-16be,這不是指 Java 只支持這一種編碼方式,而是說 char 這種類型使用 UTF-16be 進(jìn)行編碼。char 類型占 16 位,也就是兩個(gè)字節(jié),Java 使用這種雙字節(jié)編碼是為了讓一個(gè)中文或者一個(gè)英文都能使用一個(gè) char 來存儲(chǔ)。
## String 的編碼方式
String 可以看成一個(gè)字符序列,可以指定一個(gè)編碼方式將它編碼為字節(jié)序列,也可以指定一個(gè)編碼方式將一個(gè)字節(jié)序列解碼為 String。
```java
String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);
```
在調(diào)用無參數(shù) getBytes() 方法時(shí),默認(rèn)的編碼方式不是 UTF-16be。雙字節(jié)編碼的好處是可以使用一個(gè) char 存儲(chǔ)中文和英文,而將 String 轉(zhuǎn)為 bytes[] 字節(jié)數(shù)組就不再需要這個(gè)好處,因此也就不再需要雙字節(jié)編碼。getBytes() 的默認(rèn)編碼方式與平臺(tái)有關(guān),一般為 UTF-8。
```java
byte[] bytes = str1.getBytes();
```
## Reader 與 Writer
不管是磁盤還是網(wǎng)絡(luò)傳輸,最小的存儲(chǔ)單元都是字節(jié),而不是字符。但是在程序中操作的通常是字符形式的數(shù)據(jù),因此需要提供對(duì)字符進(jìn)行操作的方法。
- InputStreamReader 實(shí)現(xiàn)從字節(jié)流解碼成字符流;
- OutputStreamWriter 實(shí)現(xiàn)字符流編碼成為字節(jié)流。
## 實(shí)現(xiàn)逐行輸出文本文件的內(nèi)容
```java
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 組合了一個(gè) Reader 對(duì)象
// 在調(diào)用 BufferedReader 的 close() 方法時(shí)會(huì)去調(diào)用 Reader 的 close() 方法
// 因此只要一個(gè) close() 調(diào)用即可
bufferedReader.close();
}
```
# 五、對(duì)象操作
## 序列化
序列化就是將一個(gè)對(duì)象轉(zhuǎn)換成字節(jié)序列,方便存儲(chǔ)和傳輸。
- 序列化:ObjectOutputStream.writeObject()
- 反序列化:ObjectInputStream.readObject()
不會(huì)對(duì)靜態(tài)變量進(jìn)行序列化,因?yàn)樾蛄谢皇潜4鎸?duì)象的狀態(tài),靜態(tài)變量屬于類的狀態(tài)。
## Serializable
序列化的類需要實(shí)現(xiàn) Serializable 接口,它只是一個(gè)標(biāo)準(zhǔn),沒有任何方法需要實(shí)現(xiàn),但是如果不去實(shí)現(xiàn)它的話而進(jìn)行序列化,會(huì)拋出異常。
```java
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
transient 關(guān)鍵字可以使一些屬性不會(huì)被序列化。
ArrayList 中存儲(chǔ)數(shù)據(jù)的數(shù)組 elementData 是用 transient 修飾的,因?yàn)檫@個(gè)數(shù)組是動(dòng)態(tài)擴(kuò)展的,并不是所有的空間都被使用,因此就不需要所有的內(nèi)容都被序列化。通過重寫序列化和反序列化方法,使得可以只序列化數(shù)組中有內(nèi)容的那部分?jǐn)?shù)據(jù)。
```java
private transient Object[] elementData;
```
?
# 七、NIO
- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
- [Java NIO 淺析](https://tech.meituan.com/nio.html)
- [IBM: NIO 入門](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
新的輸入/輸出 (NIO) 庫是在 JDK 1.4 中引入的,彌補(bǔ)了原來的 I/O 的不足,提供了高速的、面向塊的 I/O。
## 流與塊
I/O 與 NIO 最重要的區(qū)別是數(shù)據(jù)打包和傳輸?shù)姆绞?#xff0c;I/O 以流的方式處理數(shù)據(jù),而 NIO 以塊的方式處理數(shù)據(jù)。
面向流的 I/O 一次處理一個(gè)字節(jié)數(shù)據(jù):一個(gè)輸入流產(chǎn)生一個(gè)字節(jié)數(shù)據(jù),一個(gè)輸出流消費(fèi)一個(gè)字節(jié)數(shù)據(jù)。為流式數(shù)據(jù)創(chuàng)建過濾器非常容易,鏈接幾個(gè)過濾器,以便每個(gè)過濾器只負(fù)責(zé)復(fù)雜處理機(jī)制的一部分。不利的一面是,面向流的 I/O 通常相當(dāng)慢。
面向塊的 I/O 一次處理一個(gè)數(shù)據(jù)塊,按塊處理數(shù)據(jù)比按流處理數(shù)據(jù)要快得多。但是面向塊的 I/O 缺少一些面向流的 I/O 所具有的優(yōu)雅性和簡(jiǎn)單性。
I/O 包和 NIO 已經(jīng)很好地集成了,java.io.\* 已經(jīng)以 NIO 為基礎(chǔ)重新實(shí)現(xiàn)了,所以現(xiàn)在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些類包含以塊的形式讀寫數(shù)據(jù)的方法,這使得即使在面向流的系統(tǒng)中,處理速度也會(huì)更快。
## 通道與緩沖區(qū)
### 1. 通道
通道 Channel 是對(duì)原 I/O 包中的流的模擬,可以通過它讀取和寫入數(shù)據(jù)。
通道與流的不同之處在于,流只能在一個(gè)方向上移動(dòng)(一個(gè)流必須是 InputStream 或者 OutputStream 的子類),而通道是雙向的,可以用于讀、寫或者同時(shí)用于讀寫。
通道包括以下類型:
- FileChannel:從文件中讀寫數(shù)據(jù);
- DatagramChannel:通過 UDP 讀寫網(wǎng)絡(luò)中數(shù)據(jù);
- SocketChannel:通過 TCP 讀寫網(wǎng)絡(luò)中數(shù)據(jù);
- ServerSocketChannel:可以監(jiān)聽新進(jìn)來的 TCP 連接,對(duì)每一個(gè)新進(jìn)來的連接都會(huì)創(chuàng)建一個(gè) SocketChannel。
### 2. 緩沖區(qū)
發(fā)送給一個(gè)通道的所有數(shù)據(jù)都必須首先放到緩沖區(qū)中,同樣地,從通道中讀取的任何數(shù)據(jù)都要先讀到緩沖區(qū)中。也就是說,不會(huì)直接對(duì)通道進(jìn)行讀寫數(shù)據(jù),而是要先經(jīng)過緩沖區(qū)。
緩沖區(qū)實(shí)質(zhì)上是一個(gè)數(shù)組,但它不僅僅是一個(gè)數(shù)組。緩沖區(qū)提供了對(duì)數(shù)據(jù)的結(jié)構(gòu)化訪問,而且還可以跟蹤系統(tǒng)的讀/寫進(jìn)程。
緩沖區(qū)包括以下類型:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
## 緩沖區(qū)狀態(tài)變量
- capacity:最大容量;
- position:當(dāng)前已經(jīng)讀寫的字節(jié)數(shù);
- limit:還可以讀寫的字節(jié)數(shù)。
狀態(tài)變量的改變過程舉例:
① 新建一個(gè)大小為 8 個(gè)字節(jié)的緩沖區(qū),此時(shí) position 為 0,而 limit = capacity = 8。capacity 變量不會(huì)改變,下面的討論會(huì)忽略它。
<div align="center"> <img src="../pics//1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
② 從輸入通道中讀取 5 個(gè)字節(jié)數(shù)據(jù)寫入緩沖區(qū)中,此時(shí) position 移動(dòng)設(shè)置為 5,limit 保持不變。
<div align="center"> <img src="../pics//80804f52-8815-4096-b506-48eef3eed5c6.png"/> </div><br>
③ 在將緩沖區(qū)的數(shù)據(jù)寫到輸出通道之前,需要先調(diào)用 flip() 方法,這個(gè)方法將 limit 設(shè)置為當(dāng)前 position,并將 position 設(shè)置為 0。
<div align="center"> <img src="../pics//952e06bd-5a65-4cab-82e4-dd1536462f38.png"/> </div><br>
④ 從緩沖區(qū)中取 4 個(gè)字節(jié)到輸出緩沖中,此時(shí) position 設(shè)為 4。
<div align="center"> <img src="../pics//b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png"/> </div><br>
⑤ 最后需要調(diào)用 clear() 方法來清空緩沖區(qū),此時(shí) position 和 limit 都被設(shè)置為最初位置。
<div align="center"> <img src="../pics//67bf5487-c45d-49b6-b9c0-a058d8c68902.png"/> </div><br>
## 文件 NIO 實(shí)例
以下展示了使用 NIO 快速復(fù)制文件的實(shí)例:
```java
public static void fastCopy(String src, String dist) throws IOException {
/* 獲得源文件的輸入字節(jié)流 */
FileInputStream fin = new FileInputStream(src);
/* 獲取輸入字節(jié)流的文件通道 */
FileChannel fcin = fin.getChannel();
/* 獲取目標(biāo)文件的輸出字節(jié)流 */
FileOutputStream fout = new FileOutputStream(dist);
/* 獲取輸出字節(jié)流的文件通道 */
FileChannel fcout = fout.getChannel();
/* 為緩沖區(qū)分配 1024 個(gè)字節(jié) */
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
/* 從輸入通道中讀取數(shù)據(jù)到緩沖區(qū)中 */
int r = fcin.read(buffer);
/* read() 返回 -1 表示 EOF */
if (r == -1) {
break;
}
/* 切換讀寫 */
buffer.flip();
/* 把緩沖區(qū)的內(nèi)容寫入輸出文件中 */
fcout.write(buffer);
/* 清空緩沖區(qū) */
buffer.clear();
}
}
```
## 選擇器
NIO 常常被叫做非阻塞 IO,主要是因?yàn)?NIO 在網(wǎng)絡(luò)通信中的非阻塞特性被廣泛使用。
NIO 實(shí)現(xiàn)了 IO 多路復(fù)用中的 Reactor 模型,一個(gè)線程 Thread 使用一個(gè)選擇器 Selector 通過輪詢的方式去監(jiān)聽多個(gè)通道 Channel 上的事件,從而讓一個(gè)線程就可以處理多個(gè)事件。
通過配置監(jiān)聽的通道 Channel 為非阻塞,那么當(dāng) Channel 上的 IO 事件還未到達(dá)時(shí),就不會(huì)進(jìn)入阻塞狀態(tài)一直等待,而是繼續(xù)輪詢其它 Channel,找到 IO 事件已經(jīng)到達(dá)的 Channel 執(zhí)行。
因?yàn)閯?chuàng)建和切換線程的開銷很大,因此使用一個(gè)線程來處理多個(gè)事件而不是一個(gè)線程處理一個(gè)事件,對(duì)于 IO 密集型的應(yīng)用具有很好地性能。
應(yīng)該注意的是,只有套接字 Channel 才能配置為非阻塞,而 FileChannel 不能,為 FileChannel 配置非阻塞也沒有意義。
<div align="center"> <img src="../pics//4d930e22-f493-49ae-8dff-ea21cd6895dc.png"/> </div><br>
### 1. 創(chuàng)建選擇器
```java
Selector selector = Selector.open();
```
### 2. 將通道注冊(cè)到選擇器上
```java
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
```
通道必須配置為非阻塞模式,否則使用選擇器就沒有任何意義了,因?yàn)槿绻ǖ涝谀硞€(gè)事件上被阻塞,那么服務(wù)器就不能響應(yīng)其它事件,必須等待這個(gè)事件處理完畢才能去處理其它事件,顯然這和選擇器的作用背道而馳。
在將通道注冊(cè)到選擇器上時(shí),還需要指定要注冊(cè)的具體事件,主要有以下幾類:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
它們?cè)?SelectionKey 的定義如下:
```java
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;
```
可以看出每個(gè)事件可以被當(dāng)成一個(gè)位域,從而組成事件集整數(shù)。例如:
```java
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
```
### 3. 監(jiān)聽事件
```java
int num = selector.select();
```
使用 select() 來監(jiān)聽到達(dá)的事件,它會(huì)一直阻塞直到有至少一個(gè)事件到達(dá)。
### 4. 獲取到達(dá)的事件
```java
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. 事件循環(huán)
因?yàn)橐淮?select() 調(diào)用不能處理完所有的事件,并且服務(wù)器端有可能需要一直監(jiān)聽事件,因此服務(wù)器端處理事件的代碼一般會(huì)放在一個(gè)死循環(huán)內(nèi)。
```java
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();
}
}
```
## 套接字 NIO 實(shí)例
```java
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();
// 服務(wù)器會(huì)為每個(gè)新連接創(chuàng)建一個(gè) SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);
// 這個(gè)新連接主要用于從客戶端讀取數(shù)據(jù)
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();
}
}
```
```java
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();
}
}
```
## 內(nèi)存映射文件
內(nèi)存映射文件 I/O 是一種讀和寫文件數(shù)據(jù)的方法,它可以比常規(guī)的基于流或者基于通道的 I/O 快得多。
向內(nèi)存映射文件寫入可能是危險(xiǎn)的,只是改變數(shù)組的單個(gè)元素這樣的簡(jiǎn)單操作,就可能會(huì)直接修改磁盤上的文件。修改數(shù)據(jù)與將數(shù)據(jù)保存到磁盤是沒有分開的。
下面代碼行將文件的前 1024 個(gè)字節(jié)映射到內(nèi)存中,map() 方法返回一個(gè) MappedByteBuffer,它是 ByteBuffer 的子類。因此,可以像使用其他任何 ByteBuffer 一樣使用新映射的緩沖區(qū),操作系統(tǒng)會(huì)在需要時(shí)負(fù)責(zé)執(zhí)行映射。
```java
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
```
## 對(duì)比
NIO 與普通 I/O 的區(qū)別主要有以下兩點(diǎn):
- NIO 是非阻塞的;
- NIO 面向塊,I/O 面向流。
轉(zhuǎn)載于:https://www.cnblogs.com/kakaisgood/p/9579734.html
總結(jié)
- 上一篇: Python——使用matplotlib
- 下一篇: go-cqhttp + noneBot群