Buffer 是java NIO中三個核心概念之一 緩存, 在java的實現體系中Buffer作為頂級抽象類存在
簡單說,Buffer在做什么?
我們知道,在java IO中體系中, 因為InputStream和OutputStream是抽象類,而java又不可以多重繼承,於是任何一個流要么只讀,要么只寫.而無法完成同時讀寫的工作
於是: Buffer來了
NIO中,對數據的讀寫,都是在Buffer中完成的,也就是說,同一個buffer我們可以先讀后寫, 它底層維護着一個數組,這個數組被三個重要的屬性控制,有機的工作結合,使buffer可讀可寫;
此外,Buffer是線程不安全的,並發訪問需要同步
三個重要屬性:
- capacity: 容量
- Buffer中元素的個數
- 永遠不能為負數
- 永遠不會變化
- limit: 限制
- 實際上它是Buffer所維護的那個數組中的一個下標
- limit是第一個不能被讀,或者第一個不能被寫的元素的index
- limit永遠不會是負數
- 永遠不會超過capacity
- Position: 定位
- 指數組中下一個將要被讀或者將要被寫的元素的索引
圖解,Buffer是如何維護的數組
一開始: 我們初始化它的大小為6 初始狀態,Capacity和Limit都在最后一個不能被讀或者不能被寫的位置上
接着我們讀入兩個數據.position跳轉到下一個將被讀的index
接下來,准備寫把buffer中的數據寫出去兩個, 於是我們需要反轉數值
buffer.flip();
反轉的邏輯:
limit=position;
position=0;
於是從0寫 ,寫到哪個位置? 寫到limi前
寫完畢后:如下圖:
現在可以看到,pisition == limit
如果再想讀入新的數據,同樣需要反轉數據flip()
但是此時,limit仍然是剛剛position的所在的最后的位置
Buffer架構體系:
模擬Buffer實現一個相同的繼承體系,進一步了解成員變量在他們之間是怎么維護的
首先,和Buufer等級一樣的頂級父類
public abstract class ParentSupper {
private int position;
private int capacity;
ParentSupper(int position,int capacity){
this.position=position;
this.capacity=capacity;
System.out.println("ParentSupper 的構造方法執行了... ");
}
final int nextPutIndex(){
if (position>=capacity){
throw new RuntimeException("索引異常");
}
return position++;
}
int i=0;
final int nextGetIndex(){
if (i >=position){
throw new RuntimeException("索引異常");
}
return i++;
}
}
接着是它的實現類, 和IntBuffer 等級一樣
public abstract class Parent extends ParentSupper {
// 抽象類中的成員變量,必須放在構造方法中
final int[] arr1;
int tag;
// todo 執行父類的構造方法
Parent(int a, int[] arr1) {
// 調用父類的構造函數
super(0,a);
this.arr1 = arr1;
this.tag = a;
}
// 構造器
public static Parent allocate(int capacity) {
return new Child(capacity);
}
// 抽象的方法
public abstract int get();
public abstract void put(int number);
}
作為抽象類的它,有自己的抽象方法, get put, 同時它里面維護着 核心數組, final不可變類型的, 抽象類中的變量不能單獨存在,必須依附構造函數,於是我們添加它的構造函數
再就是它的實現類:
class Child extends Parent {
public Child(int capacity) {
super(capacity,new int[capacity]);
}
// todo 重寫父類的構造方法
@Override
public int get() {
return arr1[nextGetIndex()];
}
@Override
public void put(int num) {
arr1[nextPutIndex()]=num;
}
}
注意,Child的類上並沒有public 修飾,意味着他只是可以包內訪問
下面測試:
public class text {
public static void main(String[] args) {
// 初始化
Parent allocate = Parent.allocate(9);
for (int i=0;i<9;i++){
allocate.put(i);
}
for (int i=0;i<=8;i++){
System.out.println( allocate.get());
}
}
}
運行流程是怎樣的呢?
當我們使用 Parent.allocate(10);
創建對象時,底層確實 new Child(int i), 同時把傳遞給Child, 而在Child()相應的構造函數中,接着調用的是spuer()方法,同時把10 傳遞給super也就是Parent,同時實例化了Parent的 數組, 在Supper的構造方法中,把0,和傳遞進來的10 傳遞給了SuperParent, 讓他維護兩個值
思考, 各個部分之間的作用
- Buffer : 維護着當前數據的 position limit 和 capaciy值,這是數組的下標值,但是Buffer卻沒有數組
- Buffer的直接實現類,如IntBuffer , 依然是抽象類, 它有數組作為緩存, 數組的維護它交給了它的父類Buffer, 針對數組的初始化,讀寫,他交給了自己的實現類
數組的維護工作是一樣的,所以抽象成Buffer
- HeapIntBuffer: 它繼承了IntBuffer, 同時把它的父類的數組進行了實例化,並且重寫了父類的get put方法
不同類型的數據,具體的讀寫是有區別的,所有抽象成不同的子類
在回去看,allocate(); 顯然我們得到的是最外層的子類對象,這也就意味着他是最強的那個對象,它擁有父類的數據,並且這個數據的讀寫由它的爺爺替自己把關,這就是Buffer的設計模式
Buffer中常見的API
Buffer的所有子類全部定義了兩套關於get()
和 put()
的方法
- Relative: 相對的讀寫, 相對的讀寫都是從當前的position的位置開始,每次的get/put都會是position的位置發生改變
- Avsolute: 絕對的讀寫 , 用戶指定索引, 從指定的索引里面get,或者直接往指定的索引里面put
clear()
將buffer置為初始狀態的值,實際上就是讓新讀入buffer中的值,覆蓋掉原來的值
position=0;
limit=capacity;
flip()
反轉buffer
limit = position;
positon=0;
isReadOnly()
判斷是否是只讀的buffer
分片Buffer
//限制前后 准備切片
byteBuffer.position(2);
byteBuffer.limit(6);
// 切片
ByteBuffer slice = byteBuffer.slice();
新得到的buffer和原buffer共享內存空間
類型化的Buffer
ByteBuffer allocate = ByteBuffer.allocate(123);
allocate.putInt(1);
allocate.putChar('你');
allocate.putDouble(123.123123);
allocate.putShort((short) 2);
allocate.putLong(3L);
allocate.flip();
System.out.println(allocate.getInt());
System.out.println(allocate.getChar());
System.out.println(allocate.getDouble());
System.out.println(allocate.getShort());
System.out.println(allocate.getLong());
類型化 的put和get 但是呢!!! 怎么存進去的 就得怎么取出來 , 否者會出現亂碼
NIO拷貝文件
// 獲取輸入流
FileInputStream fileInputStream = new FileInputStream("123.txt");
FileOutputStream fileOutputStream = new FileOutputStream("output123.txt");
// 獲取一個通道,關聯上數據
FileChannel channel = fileInputStream.getChannel();
FileChannel outputStreamChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
while (true) {
// todo 每次讀滿一次緩存后, 重新初始化buffer
byteBuffer.clear();
// 將數據讀入 緩存
int read = channel.read(byteBuffer);
System.out.println("read == "+read);
if (read == -1) {
break;
}
// 反轉 limit=position position=0
byteBuffer.flip();
// 通過channel 往輸出流中寫入緩存的數據
outputStreamChannel.write(byteBuffer);
}
// 釋放資源
fileInputStream.close();
fileInputStream.close();
System.out.println("結束...");
}
每次循環一開始,都要重置buffer,否則第一輪讀取結束之后, limit==position
從而channel里面新的數據讀不進去,而 limit==position
也不會引發異常,隨后的flip()
將buffer反轉,position為0, 使得當前buffer中的數據被循環寫到文件中,成為死循環