Java NIO是什么
Java NIO( New IO) 是從Java 1.4版本開始引入的一個新的IO API,可以替代標準的Java IO API。NIO與原來的IO有同樣的作用和目的,但是使用的方式完全不同, NIO支持面向緩沖區的、基于通道的IO操作。 NIO將以更加高效的方式進行文件的讀寫操作。
Java NIO 與 IO 的主要區別
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向緩沖區(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
(無) | 選擇器(Selectors) |
緩沖區( Buffer)
一個用于特定基本數據類型的容器。由 java.nio 包定義的,所有緩沖區都是 Buffer 抽象類的子類。Buffer 主要用于與 NIO 通道進行交互,數據是從通道讀入緩沖區,從緩沖區寫入通道中的。Buffer 就像一個數組,可以保存多個相同類型的數據。根據數據類型不同(boolean 除外) ,有以下 Buffer 常用子類:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
上述 Buffer 類 他們都采用相似的方法進行管理數據,只是各自管理的數據類型不同而已。
緩沖區的基本屬性
- 容量 (capacity) :表示 Buffer 最大數據容量,緩沖區容量不能為負,并且創建后不能更改.
- 限制 (limit):第一個不應該讀取或寫入的數據的索引,即位于 limit 后的數據不可讀寫。緩沖區的限制不能為負,并且不能大于其容量。
- 位置 (position):下一個要讀取或寫入的數據的索引。緩沖區的位置不能為負,并且不能大于其限制.
- 標記 (mark)與重置 (reset):標記是一個索引,通過 Buffer 中的 mark() 方法指定 Buffer 中一個特定的 position,之后可以通過調用 reset()方法恢復到這個 position.
標記、 位置、 限制、 容量遵守以下不變式: 0 <= mark <= position <= limit <= capacity
緩沖區的數據操作
Buffer 所有子類提供了兩個用于數據操作的方法: get()與 put() 方法
獲取 Buffer 中的數據
- get() :讀取單個字節
- get(byte[] dst):批量讀取多個字節到 dst 中
- get(int index):讀取指定索引位置的字節(不會移動 position)
放入數據到 Buffer 中
- put(byte b):將給定單個字節寫入緩沖區的當前位置
- put(byte[] src):將 src 中的字節寫入緩沖區的當前位置
- put(int index, byte b):將指定字節寫入緩沖區的索引位置(不會移動 position)
public class TestBuffer {
@Test
public void test1() {
//分配指定大小的緩沖區
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println(buffer.position());//0 返回緩沖區的當前位置 position
System.out.println(buffer.limit());//1024 返回 Buffer 的界限(limit) 的位置
System.out.println(buffer.capacity());//1024 返回Buffer的capacity 大小
//將數據存入緩沖區
buffer.put("abcde".getBytes());
System.out.println(buffer.position());//5
System.out.println(buffer.limit());//1024
System.out.println(buffer.capacity());//1024
//切換到讀取數據的模式
buffer.flip();
System.out.println(buffer.position());//0
System.out.println(buffer.limit());//5
System.out.println(buffer.capacity());//1024
//讀取緩沖區的數據
byte[] bs = new byte[buffer.limit()];
buffer.get(bs);
System.out.println(new String(bs, 0, bs.length));//abcde
System.out.println(buffer.position());//5
System.out.println(buffer.limit());//5
System.out.println(buffer.capacity());//1024
//回到讀模式,可重復讀數據
buffer.rewind();//將位置設為為 0, 取消設置的 mark
System.out.println(buffer.position());//0
System.out.println(buffer.limit());//5
System.out.println(buffer.capacity());//1024
//清空緩沖區,回到最初狀態。但是緩沖區的數據還在,處于被遺忘狀態。
buffer.clear();//清空緩沖區并返回對緩沖區的引用
System.out.println(buffer.position());//0
System.out.println(buffer.limit());//1024
System.out.println(buffer.capacity());//1024
}
@Test
public void test2() {
String str = "abcde";
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(str.getBytes());
System.out.println(buffer.position());//5
System.out.println(buffer.limit());//1024
System.out.println(buffer.capacity());//1024
buffer.flip();
byte[] bs = new byte[buffer.limit()];
buffer.get(bs, 0, 2);
System.out.println(new String(bs, 0, 2));//ab
System.out.println(buffer.position());//2
System.out.println(buffer.limit());//5
System.out.println(buffer.capacity());//1024
//標記position的位置
buffer.mark();
buffer.get(bs, 2, 2);
System.out.println(new String(bs, 2, 2));//cd
System.out.println(buffer.position());//4
//重置position的位置到標記的地方
buffer.reset();
System.out.println(buffer.position());//2
//緩沖區中是否還有課操作的字節
if (buffer.hasRemaining()) {
//剩余可操作的字節數
System.out.println(buffer.remaining());//3 返回 position 和 limit 之間的元素個數
}
}
@Test
public void test3() {
//創建直接緩沖區
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
//判斷是否為直接緩沖區
System.out.println(buffer.isDirect());//true
}
}
直接與非直接緩沖區
- 字節緩沖區要么是直接的,要么是非直接的。如果為直接字節緩沖區,則 Java 虛擬機會盡最大努力直接在此緩沖區上執行本機 I/O操作。也就是說,在每次調用基礎操作系統的一個本機 I/O 操作之前(或之后),虛擬機都會盡量避免將緩沖區的內容復制到中間緩沖區中(或從中間緩沖區中復制內容)。
- 直接字節緩沖區可以通過調用此類的 allocateDirect() 工廠方法來創建。此方法返回的緩沖區進行分配和取消分配所需成本通常高于非直接緩沖區。直接緩沖區的內容可以駐留在常規的垃圾回收堆之外,因此,它們對應用程序的內存需求量造成的影響可能并不明顯。所以,建議將直接緩沖區主要分配給那些易受基礎系統的本機 I/O 操作影響的大型、持久的緩沖區。一般情況下,最好僅在直接緩沖區能在程序性能方面帶來明顯好處時分配它們。
- 直接字節緩沖區還可以通過 FileChannel 的 map() 方法將文件區域直接映射到內存中來創建。該方法返回MappedByteBuffer。Java 平臺的實現有助于通過 JNI從本機代碼創建直接字節緩沖區。如果以上這些緩沖區中的某個緩沖區實例指的是不可訪問的內存區域,則試圖訪問該區域不會更改該緩沖區的內容,并且將會在訪問期間或稍后的某個時間導致拋出不確定的異常。
- 字節緩沖區是直接緩沖區還是非直接緩沖區可通過調用其 isDirect() 方法來確定。提供此方法是為了能夠在性能關鍵型代碼中執行顯式緩沖區管理。
通道Channel
通道表示打開到 IO 設備(例如:文件、套接字)的連接。若需要使用 NIO 系統,需要獲取用于連接 IO 設備的通道以及用于容納數據的緩沖區。然后操作緩沖區,對數據進行處理。Channel 負責傳輸, Buffer 負責存儲。通道是由 java.nio.channels 包定義的。 Channel 表示 IO 源與目標打開的連接。Channel 類似于傳統的“流”。只不過 Channel本身不能直接訪問數據, Channel 只能與Buffer 進行交互。
Java 為 Channel 接口提供的最主要實現類
- FileChannel:用于讀取、寫入、映射和操作文件的通道。
- DatagramChannel:通過 UDP 讀寫網絡中的數據通道。
- SocketChannel:通過 TCP 讀寫網絡中的數據。
- ServerSocketChannel:可以監聽新進來的 TCP 連接,對每一個新進來的連接都會創建一個 SocketChannel。
獲取通道
獲取通道的一種方式是對支持通道的對象調用getChannel() 方法。支持通道的類如下:
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramSocket
- Socket
- ServerSocket
獲取通道的其他方式是使用 Files 類的靜態方法 newByteChannel() 獲取字節通道。或者通過通道的靜態方法 open() 打開并返回指定通道。
通道的數據傳輸
//通道用于源節點與目標節點之間的連接,在NIO中負責緩沖區中數據的傳輸。
//Channel本身不存儲數據,需要配合緩沖區進行數據傳輸
/**
* 通道的主要實現類
* java.nio.channels.Channel接口
* |--FileChannel
* |--SocketChannel
* |--ServerSocketChannel
* |--DatagramChannel
*
* 獲取通道
* 1.Java針對支持通道的類提供了getChannel()方法
* 本地IO:
* FileInputStream/FileOutputStream
* RandomAccessFile
*
* 網絡IO:
* Socket
* ServerSocket
* DatagramSocket
*
* 2.在JDK1.7中NIO.2針對各個通道提供了靜態方法open()
* 3.在JDK1.7中NIO.2的Files工具類的newByteChannel()
*/
public class TestChannel {
//利用通道完成文件的復制(非直接緩沖區)
@Test
public void test1() {
//26088
//6479
//6587
//6464
long start = System.currentTimeMillis();
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
fileInputStream = new FileInputStream("E:\\BaiduYunDownload\\juc.zip");
fileOutputStream = new FileOutputStream("F:\\juc_copy2.zip");
//獲取通道
inChannel = fileInputStream.getChannel();
outChannel = fileOutputStream.getChannel();
//分配指定大小的緩沖區
ByteBuffer buffer = ByteBuffer.allocate(1024);
//將通道中的數據存入緩沖區
while (inChannel.read(buffer) != -1) {
//切換成讀取數據模式
buffer.flip();
//將緩沖區中的數據寫入通道中
outChannel.write(buffer);
//清空緩沖區
buffer.clear();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
//關閉通道 省略if判斷
outChannel.close();
inChannel.close();
//關閉流
fileOutputStream.close();
fileInputStream.close();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
@Test
public void test2() throws IOException {
//1190
//5144
//5932
//1316
//1126
//998
//1242
long start = System.currentTimeMillis();
//使用直接緩沖區完成文件的復制(內存映射文件)
//只有ByteBuffer支持直接緩沖區
FileChannel inChannel = FileChannel.open(Paths.get("E:\\BaiduYunDownload\\", "juc.zip"), StandardOpenOption.READ);
//StandardOpenOption.CREATE_NEW若存在則報錯
//StandardOpenOption.CREATE若存在則覆蓋
FileChannel outChannel = FileChannel.open(Paths.get("F:/", "juc-3.zip"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//內存映射文件。緩沖區在物理內存中。
MappedByteBuffer inMappedByteBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedByteBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接對緩沖區進行數據讀寫操作
byte[] dst = new byte[inMappedByteBuffer.limit()];
inMappedByteBuffer.get(dst);
outMappedByteBuffer.put(dst);
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
@Test
public void test3() throws IOException {
//將數據從源通道傳輸到其他 Channel 中
//729
//684
//601
//706
//625
long start = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("E:\\BaiduYunDownload\\", "juc.zip"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("F:/", "juc-4.zip"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//通道之間的數據傳輸
//inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
@Test
public void test4() throws IOException {
RandomAccessFile randomAccessFile1 = new RandomAccessFile("E:/a.txt", "rw");
//獲取通道
FileChannel fileChannel1 = randomAccessFile1.getChannel();
//分配指定大小的緩沖區
ByteBuffer buffer1 = ByteBuffer.allocate(100);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
//分散讀取,從 Channel 中讀取的數據“分散” 到多個 Buffer 中
ByteBuffer[] buffers = {buffer1, buffer2};
fileChannel1.read(buffers);
for (int i = 0; i < buffers.length; i++) {
buffers[i].flip();
}
System.out.println(new String(buffers[0].array(), 0, buffers[0].limit()));
System.out.println("-------------------");
System.out.println(new String(buffers[1].array(), 0, buffers[1].limit()));
//聚集寫入,將多個 Buffer 中的數據“聚集”到 Channel
RandomAccessFile randomAccessFile2 = new RandomAccessFile("E:/b.txt", "rw");
FileChannel fileChannel2 = randomAccessFile2.getChannel();
fileChannel2.write(buffers);
}
@Test
public void test5() {
/**
* 編碼:字符串->字節數組
* 解碼:字節數組->字符串
*/
//獲取所有支持的字符集
SortedMap<String, Charset> availableCharsets = Charset.availableCharsets();
Set<Entry<String, Charset>> set = availableCharsets.entrySet();
for (Entry<String, Charset> entry: set) {
System.out.println(entry.getKey() + "--" + entry.getValue());
}
}
@Test
public void test6() throws CharacterCodingException {
Charset charset = Charset.forName("GBK");
//編碼器
CharsetEncoder charsetEncoder = charset.newEncoder();
//解碼器
CharsetDecoder charsetDecoder = charset.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(1024);
charBuffer.put("你好");
charBuffer.flip();
//編碼
ByteBuffer byteBuffer = charsetEncoder.encode(charBuffer);
for (int i = 0; i < 4; i++) {
//字節數組
System.out.println(byteBuffer.get());
}
//切換到讀模式
byteBuffer.flip();
//解碼
CharBuffer charBuffer2 = charsetDecoder.decode(byteBuffer);
System.out.println(charBuffer2.toString());
}
}