【NIO】Buffer:基本原理及高階使用
緩衝區實際上是一個容器物件,更直接的說,其實就是一個陣列,在 NIO 庫中,所有資料都是用緩衝區處理的。
讀/寫 ==> Buffer。即使用者的直接操作都是面向緩衝區。在讀取資料時,它是直接讀到緩衝區中的; 在寫入資料時,它也是寫入到緩衝區中的;任何時候訪問 NIO 中的資料,都是將它放到緩衝區中。
PS:在面向流I/O 系統中,所有資料都是直接寫入或者直接將資料讀取到 Stream 物件中。
在NIO 中,所有的緩衝區型別都繼承於抽象類 Buffer,最常用的就是 ByteBuffer,對於 Java 中的基本型別,基本都有一個具體 Buffer 型別與之相對應,它們之間的繼承關係如下圖所示:
下面來看一個使用 Buffer 的簡單示例,其中包含了最基本的 API:
public class IntBufferDemo {
public static void main(String[] args) {
// 1.分配新的 int 緩衝區,引數為緩衝區容量(capacity)
// 新緩衝區的當前位置將為零,其界限(limit)將為其容量。
// 它將具有一個底層實現陣列,其陣列偏移量(position)將為零。
IntBuffer buffer = IntBuffer.allocate(8);
for (int i = 0; i < buffer.capacity(); ++i) {
int j = 2 * (i + 1);
// 2.將給定整數寫入此緩衝區的當前位置,當前位置遞增
// 注:因為底層是陣列,所以還可以 put(index,value)
buffer.put(j);
}
// 3.重設此緩衝區,將限制設定為當前位置,然後將當前位置設定為 0
buffer.flip();
// 檢視在當前位置和限制位置之間是否有元素
// 注:這裡也可以 buffer.remaining() > 0
while (buffer.hasRemaining()) {
// 4.讀取此緩衝區當前位置的整數,然後當前位置遞增
// 注:因為底層是陣列,所以還可以 get(index)
int j = buffer.get();
System.out.print(j + " ");
}
}
}
1.Buffer 基本原理
在談到緩衝區時,我們說緩衝區物件本質上是一個數組,但它其實是一個特殊的陣列,緩衝區物件內建了一些機制,
能夠跟蹤和記錄緩衝區的狀態變化情況,如果我們使用 get()方法從緩衝區獲取資料或者使用 put()方法把資料寫入緩衝
區,都會引起緩衝區狀態的變化。
在緩衝區中,最重要的屬性有下面三個,它們一起合作完成對緩衝區內部狀態的變化跟蹤:
- position:遊標,指定下一個將要被寫入或者讀取的元素索引,它的值由 get()/put()方法自動更新,在新建立一個 Buffer 物件時,position 被初始化為0。
- limit:界限,指定還有多少資料需要取出(從緩衝區寫入通道時),或者還有多少空間可以放入資料(從通道讀入緩衝區時)。
- capacity:容量,指定了可以儲存在緩衝區中的最大資料容量,實際上,它指定了底層陣列的大小,或者至少是指定了准許我們使用的底層陣列的容量。
以上三個屬性值之間有一些相對大小的關係:0 <=position <= limit <=capacity。
如果我們建立一個新的容量大小為 10 的 ByteBuffer 物件,在初始化的時候,position 設定為0,limit 和 capacity 被設定為 10,在以後使用 ByteBuffer 物件過程中,capacity 的值不會再發生變化,而其它兩個將會隨著使用而變化。
下面我們用程式碼來演示一遍,準備一個txt文件,存放的 C 盤,輸入以下內容:
Zhangsan
下面我們用一段程式碼來驗證 position、limit和 capacity 這幾個值的變化過程,程式碼如下
public class BufferDemo {
public static void main(String[] args) throws Exception {
FileInputStream fin = new FileInputStream("C://test.txt");
// 1.建立檔案操作管道
// 注:BIO中沒有這一步
FileChannel fc = fin.getChannel();
// 2.建立緩衝區(Buffer)
// 初始化容量為10,就是建立一個大小為10的byte陣列
ByteBuffer buffer = ByteBuffer.allocate(10);
output("初始化", buffer);
// 3.將管道中的資料讀到Buffer中
fc.read(buffer);
output("呼叫read()", buffer);
// 4.準備操作之前,先鎖定操作範圍
buffer.flip();
output("呼叫flip()", buffer);
// 5.讀取Buffer中的資料
// 注:這裡是逐位元組讀取
while (buffer.remaining() > 0) {
byte b = buffer.get();
}
output("呼叫get()", buffer);
// 6.clear可以理解為解鎖
buffer.clear();
output("呼叫clear()", buffer);
fin.close();
}
private static void output(String step, ByteBuffer buffer) {
System.out.println(step + " : ");
// capacity,容量
System.out.print("capacity: " + buffer.capacity() + ", ");
// position,遊標,記錄要操作資料位置
System.out.print("position: " + buffer.position() + ", ");
// limit,界限,資料操作範圍在position-limit之間
System.out.print("limit: " + buffer.limit());
System.out.println("\n");
}
}
輸出結果:
1)初始化,limit=capacitty,position=0
2) read(),讀取資料進Buffer,position=資料大小
3)filp(),鎖定,將limit移動到position,將position置0
4)get(),逐位讀取Buffer中的資料,positon向limit移動,且移動範圍不超過limit
5)clear(),解鎖,position=0,limit=capacity
2.Buffer 高階使用
在文章開篇我們演示了的 Buffer 的基本使用,下面我們來看看關於 Buffer 的一些高階用法…
2.1 緩衝區分配
在前面的幾個例子中,我們已經看到了,在建立一個緩衝區物件時,會呼叫靜態方法allocate()來指定緩衝區的容量,其實呼叫allocate()相當於建立了一個指定大小的陣列,並把它包裝為緩衝區物件。
或者我們也可以直接將一個現有的陣列,包裝為緩衝區物件,如下示例程式碼所示:
public Buffer wrap() {
// 方式一:分配指定大小緩衝區
ByteBuffer buffer1 = ByteBuffer.allocate(10);
// 方式二:手動包裝一個現有陣列
byte[] arr = new byte[10];
ByteBuffer buffer2 = ByteBuffer.wrap(arr);
return buffer2;
}
2.2 緩衝區分片
在NIO中,除了可以分配或者包裝一個緩衝區物件外,還可以根據現有的緩衝區物件來建立一個子緩衝區,即在現有緩衝區上切出一片來作為一個新的緩衝區,但現有的緩衝區與建立的子緩衝區在底層陣列層面上是資料共享的。也就是說,子緩衝區相當於是現有緩衝區的一個檢視視窗。
呼叫slice()方法可以建立一個子緩衝區,讓我們通過例子來看一下:
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); i++) {
// 注:這裡要將int強轉為byte
buffer.put((byte)i);
}
// 建立[3, 7)的分片
// 注:建立分片的方式是[position,limit),
// 可以通過buffer.capacity()/limit()手動設定
buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();
// 改變分片中的資料 ----> 實際上是改變的原陣列
for (int i = 0; i < slice.capacity(); i++) {
byte b = slice.get(i);
b *= 10;
slice.put(i, b);
}
// 注:因為修改了p,l,所以在最後讀去原陣列時要將position與limit還原
buffer.position(0);
buffer.limit(buffer.capacity());
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
}
2.3 只讀緩衝區
只讀緩衝區非常簡單,可以讀取它們,但是不能向它們寫入資料。
可以通過呼叫緩衝區的 asReadOnlyBuffer()方法,將任何常規緩衝區轉換為只讀緩衝區,這個方法返回一個與原緩衝區完全相同的緩衝區,並與原緩衝區共享資料,只不過它是隻讀的。
PS:如果原緩衝區的內容發生了變化,只讀緩衝區的內容也隨之發生變化
public class ReadOnlyBuffer {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte)i);
}
// 通過原Buffer建立只讀Buffer
ByteBuffer readOnly = buffer.asReadOnlyBuffer();
// 修改原Buffer
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.get(i);
b *= 10;
buffer.put(i, b);
}
readOnly.position(0);
readOnly.limit(buffer.capacity());
// 只讀Buffer隨之改變
while (buffer.hasRemaining()) {
System.out.println(readOnly.get());
}
}
}
只讀緩衝區對於保護資料很有用。在將緩衝區傳遞給某個物件的方法時,無法知道這個方法是否會修改緩衝區中的資料。如果嘗試修改只讀緩衝區的內容,則會報ReadOnlyBufferException異常。
建立一個只讀的緩衝區可以保證該緩衝區不會被修改。只可以把常規緩衝區轉換為只讀緩衝區,而不能將只讀的緩衝區轉換為可寫的緩衝區。
2.4 直接緩衝區
直接緩衝區是為加快I/O 速度,使用一種特殊方式為其分配記憶體的緩衝區,JDK文件中的描述為:給定一個直接位元組緩衝區,Java虛擬機器將盡最大努力直接對它執行本機I/O操作。
也就是說,它會在每一次呼叫底層作業系統的本機I/O 操作之前(或之後),嘗試避免將緩衝區的內容拷貝到一箇中間緩衝區(JVM的)或者從一箇中間緩衝區中拷貝資料。
要分配直接緩衝區,需要呼叫allocateDirect()方法,而不是allocate()方法,使用方式與普通緩衝區並無區別,如下面的拷貝檔案示例:
public class DirectBuffer {
static public void main( String args[] ) throws Exception {
// 首先我們從磁碟上讀取剛才我們寫出的檔案內容
String infile = "C://test.txt";
FileInputStream fin = new FileInputStream( infile );
FileChannel fcin = fin.getChannel();
// 把剛剛讀取的內容寫入到一個新的檔案中
String outfile = String.format("C://testcopy.txt");
FileOutputStream fout = new FileOutputStream( outfile );
FileChannel fcout = fout.getChannel();
// 使用 allocateDirect,而不是 allocate
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
buffer.clear();
int r = fcin.read(buffer);
if (r==-1) {
break;
}
buffer.flip();
fcout.write(buffer);
}
}
}
2.5 記憶體對映
記憶體對映是一種讀和寫檔案資料的方法,它可以比常規的基於流或者基於通道的I/O 快的多。記憶體對映檔案I/O 是通過使檔案中的資料出現為記憶體陣列的內容來完成的,這其初聽起來似乎不過就是將整個檔案讀到記憶體中,但是事實上並不是這樣。一般來說,只有檔案中實際讀取或者寫入的部分才會對映到記憶體中。如下面的示例程式碼:
public class MappedBuffer {
static private final int start = 0;
static private final int size = 1024;
static public void main( String args[] ) throws Exception {
RandomAccessFile raf = new RandomAccessFile( "C://test.txt", "rw" );
FileChannel fc = raf.getChannel();
// 把緩衝區跟檔案系統進行一個對映關聯
// 只要操作緩衝區裡面的內容,檔案內容也會跟著改變
MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,start, size );
mbb.put( 0, (byte)97 );
mbb.put( 1023, (byte)122 );
raf.close();
}
}