Java入門系列-23-NIO(使用緩衝區和通道對檔案操作)
NIO 是什麼
java.nio全稱java non-blocking(非阻塞) IO(實際上是 new io),是指jdk1.4 及以上版本里提供的新api(New IO) ,為所有的原始型別(boolean型別除外)提供快取支援的資料容器,使用它可以提供非阻塞式的高伸縮性網路。
NIO與IO的區別
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向緩衝區(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞(Non Blocking IO) |
無 | 選擇器(Selectors) |
NIO系統的核心是:通道(Channel)和緩衝區(Buffer)
緩衝區(Buffer)
位於 java.nio 包,所有緩衝區都是 Buffer 抽象類的子類,使用陣列對資料進行緩衝。
除了 boolean 型別,Buffer 對每種基本資料型別都有針對的實現類:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
建立緩衝區通過 xxxBuffer.allocate(int capacity)方法
ByteBuffer buf1 = ByteBuffer.allocate(512); LongBuffer buf2 = LongBuffer.allocate(1024); ……
緩衝區的屬性
容量(capacity):表示緩衝區儲存資料的最大容量,不能為負數,建立後不可修改。
限制:第一個不可以讀取或寫入的資料的索引,即位於 limit 後的資料不能讀寫。不能為負數,不能大於容量。
位置(position):下一個要讀取或寫入的資料的索引,位置不能為負數,不能大於 limit
標記(mark):標記是一個索引,通過 Buffer 中的 mark() 方法指 Buffer 中一個特定的 position,之後可以通過 reset() 方法回到這個 postion。
Buffer 的常用方法
方法名稱 | 說明 |
---|---|
Buffer clear() | 清空緩衝區並返回對緩衝區的引用 |
Buffer flip() | 將緩衝區的 limit 設定為當前位置,並將當前位置重置為0 |
int capacity() | 返回 Buffer 的容量大小 |
boolean hasRemaining() | 判斷緩衝區是否還有元素 |
int limit() | 返回 限制的位置 |
Buffer limit(int n) | 將設定緩衝區界限為 n,並返回一個具有新 limit 的緩衝區物件 |
Buffer mark() | 對緩衝區設定標記 |
int position() | 返回緩衝區的當前位置 position |
Buffer position(int n) | 將設定緩衝區的當前位置為 n,並返回修改後的 Buffer 物件 |
int remaining() | 返回 position 和 limit 之間的元素個數 |
Buffer reset() | 將位置 position 轉到以前設定的 mark 所在的位置 |
Buffer rewind() | 將位置設定為 0,取消設定的 mark |
Buffer 所有子類提供了兩個操作的資料的方法:get() 方法和 put() 方法
緩衝區存取資料操作
package testnio;
import java.nio.ByteBuffer;
public class TestBuffer1 {
public static void main(String[] args) {
testuse();
}
public static void testuse() {
//1.分配一個指定大小的緩衝區
ByteBuffer buf=ByteBuffer.allocate(1024);
System.out.println("---------------allocate()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//2.利用 put() 存入資料到緩衝區中
String str="hello";
//將字串轉為 byte 陣列存入緩衝區
buf.put(str.getBytes());
System.out.println("---------------put()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//3.切換讀取資料模式
buf.flip();
System.out.println("---------------flip()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//4.利用get() 讀取緩衝區中的資料
byte[] data=new byte[buf.limit()];
System.out.println("---------------get()----------------");
buf.get(data);
System.out.println(new String(data,0,data.length));
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//5.rewind() 重複讀
buf.rewind();
System.out.println("---------------rewind()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//6.clear() 清空緩衝區,但緩衝區中的資料依然存在
buf.clear();
System.out.println("---------------clear()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
System.out.println((char)buf.get());
}
}
使用 mark()方法標記
package testnio;
import java.nio.ByteBuffer;
public class TestBuffer2 {
public static void main(String[] args) {
testmark();
}
public static void testmark() {
String str="jikedaquan.com";
//建立緩衝區
ByteBuffer buf=ByteBuffer.allocate(1024);
//存入資料
buf.put(str.getBytes());
//切換模式
buf.flip();
//臨時陣列用於接收緩衝區獲取的資料,長度與緩衝區 limit 一值
byte[] data=new byte[buf.limit()];
//獲取緩衝區的資料從0開始獲取4個,存入 data 陣列中
buf.get(data, 0, 4);
//將陣列轉為字串列印
System.out.println(new String(data,0,4));
//列印 position
System.out.println(buf.position());
//標記
buf.mark();
System.out.println("---------------再次獲取----------------");
//從索引4開始,獲取6個位元組(餘下資料)
buf.get(data, 4, 6);
System.out.println(new String(data,4,6));
System.out.println(buf.position());
//恢復到標記位置
buf.reset();
System.out.println("---------------reset()----------------");
System.out.println(buf.position());
//判斷緩衝區是是有還有剩餘資料
if (buf.hasRemaining()) {
//獲取緩衝區中可以操作的數量
System.out.println("可運算元量:"+buf.remaining());
}
}
}
mark <= position <= limit <= capacity
雖然使用了緩衝區提高了一定的IO速度,但這樣的效率仍然不是最高的。非直接緩衝區在與物理磁碟操作中需要經過核心地址空間copy操作,直接緩衝區不經過copy操作,直接操作實體記憶體對映檔案, 使用直接緩衝區將大大提高效率。
直接緩衝區進行分配和取消分配所需成本工廠高於非直接緩衝區,一般情況下,最好僅在直接緩衝區能在程式效能方面帶來明顯好處時分配它們。
直接緩衝區可以通過呼叫此類的 allocateDirect()工廠方法建立
建立直接緩衝區
package testnio;
import java.nio.ByteBuffer;
public class TestBuffer3 {
public static void main(String[] args) {
testAllocateDirect();
}
public static void testAllocateDirect() {
//建立直接緩衝區
ByteBuffer buf=ByteBuffer.allocateDirect(1024);
//是否是直接緩衝區
System.out.println(buf.isDirect());
}
}
通道(Channel)
緩衝區僅是運載資料的容器,需要對資料讀寫還需要有一條通道,這兩者是密不可分的。
Channel 介面的主要實現類:
- FileChannel:用於讀取、寫入、對映和操作檔案的通道
- DatagramChannel:通過 UDP 讀寫網路中的資料通道
- ScoketChannel:通過 TCP 讀寫網路中的資料
- ServerScoketChannel:可以監聽新進來的 TCP 連結,對每一個新進來的連線都會建立一個 SocketChannel
如何獲取通道?
1、通過支援通道的物件呼叫 getChannel() 方法
支援通道的類:
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramScoket
- Socket
- ServerScoket
使用通道和緩衝區實現檔案讀和寫
package testnio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class TestChannel {
public static void main(String[] args) {
FileInputStream fis=null;
FileOutputStream fos=null;
FileChannel inChannel=null;
FileChannel outChannel=null;
try {
//建立輸入流
fis=new FileInputStream("F:/1.jpg");
//建立輸出流
fos=new FileOutputStream("F:/2.jpg");
//獲取通道
inChannel=fis.getChannel();
outChannel=fos.getChannel();
//分配指定大小的緩衝區
ByteBuffer buf=ByteBuffer.allocate(1024);
//將通道中的資料存入快取區
while(inChannel.read(buf)!=-1) {
//切換讀取資料的模式
buf.flip();
//將讀入的緩衝區存入寫資料的管道
outChannel.write(buf);
//清空快取區(清空才能再次讀入)
buf.clear();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fis!=null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos!=null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(inChannel!=null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outChannel!=null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2、通過通道類的靜態方法 open()
package testnio;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class TestOpenAndMapped {
public static void main(String[] args) {
FileChannel inChannel=null;
FileChannel outChannel=null;
try {
//通過open建立通道
inChannel = FileChannel.open(Paths.get("F:/a.jpg"), StandardOpenOption.READ);
outChannel = FileChannel.open(Paths.get("F:/b.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
//記憶體對映檔案 直接緩衝區
MappedByteBuffer inMappedBuf=inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuf=outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接對緩衝區進行資料的讀寫操作
byte[] data=new byte[inMappedBuf.limit()];
inMappedBuf.get(data);//讀
outMappedBuf.put(data);//寫
} catch (IOException e) {
e.printStackTrace();
}finally {
if (inChannel!=null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outChannel!=null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
JDK 1.7 新增的方法 open(),引數 path 通常代表一個依賴系統的檔案路徑,通過Paths.get()獲取。
引數 StandardOpenOption 是一個列舉型別,常用值如下:
- READ :開啟讀訪問
- WRITE:開啟寫訪問
- APPEND:向後追加
- CREATE:建立新檔案,存在則覆蓋
- CREATE_NEW:建立新檔案,存在則報錯
MappedByteBuffer:直接位元組緩衝區,其內容是檔案的記憶體對映區域。
通道資料傳輸
將資料從源通道傳輸到其他 Channel 中,transferTo() 和 transferFrom()
package testnio;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class TestChannelTransfer {
public static void main(String[] args) {
FileChannel inChannel=null;
FileChannel outChannel=null;
try {
//通過open建立管道
inChannel = FileChannel.open(Paths.get("F:/a.jpg"), StandardOpenOption.READ);
outChannel = FileChannel.open(Paths.get("F:/b.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
//將inChannel中所有資料傳送到outChannel
//inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());
} catch (IOException e) {
e.printStackTrace();
}finally {
if (inChannel!=null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outChannel!=null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Channel 負責傳輸,Buffer 負責儲存