1. 程式人生 > >小師妹學JavaIO之:Buffer和Buff

小師妹學JavaIO之:Buffer和Buff

[toc] # 簡介 小師妹在學習NIO的路上越走越遠,唯一能夠幫到她的就是在她需要的時候給她以全力的支援。什麼都不說了,今天介紹的是NIO的基礎Buffer。老鐵給我上個Buff。 # Buffer是什麼 小師妹:F師兄,這個Buffer是我們縱橫王者峽谷中那句:老鐵給我加個Buff的意思嗎? 當然不是了,此Buffer非彼Buff,Buffer是NIO的基礎,沒有Buffer就沒有NIO,沒有Buffer就沒有今天的java。 因為NIO是按Block來讀取資料的,這個一個Block就可以看做是一個Buffer。我們在Buffer中儲存要讀取的資料和要寫入的資料,通過Buffer來提高讀取和寫入的效率。 更多精彩內容且看: * [區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新](http://www.flydean.com/blockchain/) * [Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新](http://www.flydean.com/learn-spring-boot/) * [Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新](http://www.flydean.com/spring5/) * [java程式設計師從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程](https://blog.csdn.net/superfjj/article/details/105482751) > 更多內容請訪問[www.flydean.com](www.flydean.com) 還記得java物件的底層儲存單位是什麼嗎? 小師妹:這個我知道,java物件的底層儲存單位是位元組Byte。 對,我們看下Buffer的繼承圖: ![](https://img-blog.csdnimg.cn/20200514142719108.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) Buffer是一個介面,它下面有諸多實現,包括最基本的ByteBuffer和其他的基本型別封裝的其他Buffer。 小師妹:F師兄,有ByteBuffer不就夠了嗎?還要其他的型別Buffer做什麼? 小師妹,山珍再好,也有吃膩的時候,偶爾也要換個蘿蔔白菜啥的,你以為乾隆下江南都幹了些啥? ByteBuffer雖然好用,但是它畢竟是最小的單位,在它之上我們還有Char,int,Double,Short等等基礎型別,為了簡單起見,我們也給他們都搞一套Buffer。 # Buffer進階 小師妹:F師兄,既然Buffer是這些基礎型別的集合,為什麼不直接用結合來表示呢?給他們封裝成一個物件,好像有點多餘。 我們既然在面向物件的世界,從表面來看自然是使用Object比較合乎情理,從底層的本質上看,這些封裝的Buffer包含了一些額外的元資料資訊,並且還提供了一些意想不到的功能。 ![](https://img-blog.csdnimg.cn/20200519142644525.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 上圖列出了Buffer中的幾個關鍵的概念,分別是Capacity,Limit,Position和Mark。Buffer底層的本質是陣列,我們以ByteBuffer為例,它的底層是: ~~~java final byte[] hb; ~~~ * Capacity表示的是該Buffer能夠承載元素的最大數目,這個是在Buffer建立初期就設定的,不可以被改變。 * Limit表示的Buffer中可以被訪問的元素個數,也就是說Buffer中存活的元素個數。 * Position表示的是下一個可以被訪問元素的index,可以通過put和get方法進行自動更新。 * Mark表示的是歷史index,當我們呼叫mark方法的時候,會把設定Mark為當前的position,通過呼叫reset方法把Mark的值恢復到position中。 # 建立Buffer 小師妹:F師兄呀,這麼多Buffer建立起來是不是很麻煩?有沒有什麼快捷的使用辦法? 一般來說建立Buffer有兩種方法,一種叫做allocate,一種叫做wrap。 ~~~java public void createBuffer(){ IntBuffer intBuffer= IntBuffer.allocate(10); log.info("{}",intBuffer); log.info("{}",intBuffer.hasArray()); int[] intArray=new int[10]; IntBuffer intBuffer2= IntBuffer.wrap(intArray); log.info("{}",intBuffer2); IntBuffer intBuffer3= IntBuffer.wrap(intArray,2,5); log.info("{}",intBuffer3); intBuffer3.clear(); log.info("{}",intBuffer3); log.info("{}",intBuffer3.hasArray()); } ~~~ allocate可以為Buffer分配一個空間,wrap同樣為Buffer分配一個空間,不同的是這個空間背後的陣列是自定義的,wrap還支援三個引數的方法,後面兩個引數分別是offset和length。 ~~~java INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10] INFO com.flydean.BufferUsage - true INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10] INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=2 lim=7 cap=10] INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10] INFO com.flydean.BufferUsage - true ~~~ hasArray用來判斷該Buffer的底層是不是陣列實現的,可以看到,不管是wrap還是allocate,其底層都是陣列。 > 需要注意的一點,最後,我們呼叫了clear方法,clear方法呼叫之後,我們發現Buffer的position和limit都被重置了。這說明wrap的三個引數方法設定的只是初始值,可以被重置。 # Direct VS non-Direct 小師妹:F師兄,你說了兩種建立Buffer的方法,但是兩種Buffer的後臺都是陣列,難道還有非陣列的Buffer嗎? 自然是有的,但是隻有ByteBuffer有。ByteBuffer有一個allocateDirect方法,可以分配Direct Buffer。 小師妹:Direct和非Direct有什麼區別呢? ![](https://img-blog.csdnimg.cn/20200513225239404.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) Direct Buffer就是說,不需要在使用者空間再複製拷貝一份資料,直接在虛擬地址對映空間中進行操作。這叫Direct。這樣做的好處就是快。缺點就是在分配和銷燬的時候會佔用更多的資源,並且因為Direct Buffer不在使用者空間之內,所以也不受垃圾回收機制的管轄。 所以通常來說只有在資料量比較大,生命週期比較長的資料來使用Direct Buffer。 看下程式碼: ~~~java public void createByteBuffer() throws IOException { ByteBuffer byteBuffer= ByteBuffer.allocateDirect(10); log.info("{}",byteBuffer); log.info("{}",byteBuffer.hasArray()); log.info("{}",byteBuffer.isDirect()); try (RandomAccessFile aFile = new RandomAccessFile("src/main/resources/www.flydean.com", "r"); FileChannel inChannel = aFile.getChannel()) { MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size()); log.info("{}",buffer); log.info("{}",buffer.hasArray()); log.info("{}",buffer.isDirect()); } } ~~~ 除了allocateDirect,使用FileChannel的map方法也可以得到一個Direct的MappedByteBuffer。 上面的例子輸出結果: ~~~java INFO com.flydean.BufferUsage - java.nio.DirectByteBuffer[pos=0 lim=10 cap=10] INFO com.flydean.BufferUsage - false INFO com.flydean.BufferUsage - true INFO com.flydean.BufferUsage - java.nio.DirectByteBufferR[pos=0 lim=0 cap=0] INFO com.flydean.BufferUsage - false INFO com.flydean.BufferUsage - true ~~~ # Buffer的日常操作 小師妹:F師兄,看起來Buffer確實有那麼一點複雜,那麼Buffer都有哪些操作呢? Buffer的操作有很多,下面我們一一來講解。 ## 向Buffer寫資料 向Buffer寫資料可以呼叫Buffer的put方法: ~~~java public void putBuffer(){ IntBuffer intBuffer= IntBuffer.allocate(10); intBuffer.put(1).put(2).put(3); log.info("{}",intBuffer.array()); intBuffer.put(0,4); log.info("{}",intBuffer.array()); } ~~~ 因為put方法返回的還是一個IntBuffer類,所以Buffer的put方法可以像Stream那樣連寫。 同時,我們還可以指定put在什麼位置。上面的程式碼輸出: ~~~java INFO com.flydean.BufferUsage - [1, 2, 3, 0, 0, 0, 0, 0, 0, 0] INFO com.flydean.BufferUsage - [4, 2, 3, 0, 0, 0, 0, 0, 0, 0] ~~~ ## 從Buffer讀資料 讀資料使用get方法,但是在get方法之前我們需要呼叫flip方法。 flip方法是做什麼用的呢?上面講到Buffer有個position和limit欄位,position會隨著get或者put的方法自動指向後面一個元素,而limit表示的是該Buffer中有多少可用元素。 如果我們要讀取Buffer的值則會從positon開始到limit結束: ~~~java public void getBuffer(){ IntBuffer intBuffer= IntBuffer.allocate(10); intBuffer.put(1).put(2).put(3); intBuffer.flip(); while (intBuffer.hasRemaining()) { log.info("{}",intBuffer.get()); } intBuffer.clear(); } ~~~ 可以通過hasRemaining來判斷是否還有下一個元素。通過呼叫clear來清除Buffer,以供下次使用。 ## rewind Buffer rewind和flip很類似,不同之處在於rewind不會改變limit的值,只會將position重置為0。 ~~~java public void rewindBuffer(){ IntBuffer intBuffer= IntBuffer.allocate(10); intBuffer.put(1).put(2).put(3); log.info("{}",intBuffer); intBuffer.rewind(); log.info("{}",intBuffer); } ~~~ 上面的結果輸出: ~~~java INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10] INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10] ~~~ ## Compact Buffer Buffer還有一個compact方法,顧名思義compact就是壓縮的意思,就是把Buffer從當前position到limit的值賦值到position為0的位置: ~~~java public void useCompact(){ IntBuffer intBuffer= IntBuffer.allocate(10); intBuffer.put(1).put(2).put(3); intBuffer.flip(); log.info("{}",intBuffer); intBuffer.get(); intBuffer.compact(); log.info("{}",intBuffer); log.info("{}",intBuffer.array()); } ~~~ 上面程式碼輸出: ~~~java INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=3 cap=10] INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=2 lim=10 cap=10] INFO com.flydean.BufferUsage - [2, 3, 3, 0, 0, 0, 0, 0, 0, 0] ~~~ ## duplicate Buffer 最後我們講一下複製Buffer,有三種方法,duplicate,asReadOnlyBuffer,和slice。 duplicate就是拷貝原Buffer的position,limit和mark,它和原Buffer是共享原始資料的。所以修改了duplicate之後的Buffer也會同時修改原Buffer。 如果用asReadOnlyBuffer就不允許拷貝之後的Buffer進行修改。 slice也是readOnly的,不過它拷貝的是從原Buffer的position到limit-position之間的部分。 ~~~java public void duplicateBuffer(){ IntBuffer intBuffer= IntBuffer.allocate(10); intBuffer.put(1).put(2).put(3); log.info("{}",intBuffer); IntBuffer duplicateBuffer=intBuffer.duplicate(); log.info("{}",duplicateBuffer); IntBuffer readOnlyBuffer=intBuffer.asReadOnlyBuffer(); log.info("{}",readOnlyBuffer); IntBuffer sliceBuffer=intBuffer.slice(); log.info("{}",sliceBuffer); } ~~~ 輸出結果: ~~~java INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10] INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10] INFO com.flydean.BufferUsage - java.nio.HeapIntBufferR[pos=3 lim=10 cap=10] INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=7 cap=7] ~~~ # 總結 今天給小師妹介紹了Buffer的原理和基本操作。 本文的例子[https://github.com/ddean2009/learn-java-io-nio](https://github.com/ddean2009/learn-java-io-nio) > 本文作者:flydean程式那些事 > > 本文連結:[http://www.flydean.com/java-io-nio-buffer/](http://www.flydean.com/java-io-nio-buffer/) > > 本文來源:flydean的部落格 > > 歡迎關注我的公眾號:程式那些事,更多精彩等著您!