ByteBuffer in-depth understanding

The ByteBuffer class is a buffer class that is often used in Java NIO. It can be used for efficient IO operations. However, if the understanding of common methods is wrong, unexpected bugs will occur.

 

Common methods of the ByteBuffer class

Let's take a look at a basic program first

 
publicvoid test()throwsIOException{ByteBuffer buff =ByteBuffer.allocate(128);FileChannel fin =null;FileChannel fout =null;try{
            fin =newFileInputStream("filein").getChannel();
            fout =newFileOutputStream("fileout").getChannel();while(fin.read(buff)!=-1){
                buff.flip();
                fout.write(buff);
                buff.clear();}}catch(FileNotFoundException e){}finally{try{if(fin !=null){
                    fin.close();}if(fout !=null){
                    fout.close();}}catch(IOException e){throw e;}}}

In the test method, a memory space is first allocated through the ByteBuffer.allocate() method. As a cache, the allocate method automatically clears the cache, and then opens an input file pipe fin and an output file pipe fout, and starts from fin in the loop. The read data is stored in the buff buffer, and then the content in the buff buffer is written to fout. Of course, this is not efficient for scenarios like reading from a file and then writing. 
It can be seen that after reading the data from fin, the ByteBuffer.flip() method must be called first. If the data is written to the output file, the ByteBuffer.clear() method must be called. Why do you do this?

ByteBuffer can be used as a buffer because it is a continuous space in memory. Four indexes are defined inside the ByteBuffer object, namely mark, position, limit, capacity, among which

  • mark is used to mark the current position

  • position represents the current readable and writable pointer. If a byte is written to the ByteBuffer object, the byte will be written to the address pointed to by position. If a byte is read from the ByteBuffer, then the byte will be read. Read this byte out of the address pointed to by position. After reading and writing is completed, position is incremented by 1.

  • The limit is the boundary that can be read and written. When the position reaches the limit, it means that the content in the ByteBuffer has been read, or the ByteBuffer is full.

  • capacity is the capacity of this ByteBuffer. The call in the above program ByteBuffer.allocate(128)means that a ByteBuffer object with a capacity of bytes is created.

After understanding these four variables, let's take a look at the previous program. The reason why the ByteBuffer.flip() method is called is because after writing data to the ByteBuffer, the position is the position of the last byte of the data just read in the buffer. The flip method sets the limit value to the position value and the position to 0 , so that when the get*() method is called to fetch data from the ByteBuffer, the valid data in the ByteBuffer can be obtained. The code of the flip method in the JDK is as follows:

publicfinalBuffer flip(){
    limit = position;
    position =0;
    mark =-1;returnthis;}

When called four.write(buff), the data in the buff buffer is written to the output pipe. At this time, the ByteBuffer.clear() method is called to prepare for reading data from the pipe next time, but calling the clear method does not change the data in the buffer. To clear it, set the values ​​of the three variables of position, mark and limit. The code of the clear method in JDK is as follows:

publicfinalBuffer clear(){
    position =0;
    limit = capacity;
    mark =-1;returnthis;}

The naming of this method gives the impression that the data is emptied, but in fact it is not. It does not clear the data in the buffer, but resets the three index values ​​in the object. If it is not emptied, assuming this The data in the ByteBuffer is full this time, and the data read next time is not enough to fill the buffer, then there will be the data that has been processed last time, so when judging whether there is any available data in the buffer, use ByteBuffer .hasRemaining() method, in JDK, the code of this method is as follows:

 
publicfinalboolean hasRemaining(){return position < limit;}

In this method, the values ​​of position and limit are compared to determine whether there is still data available.

In the ByteBuffer class, another method is compact. For ByteBuffer, the implementation of the compact method of its subclass HeapByteBuffer is as follows:

publicByteBuffer compact(){System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
    position(remaining());
    limit(capacity());returnthis;}

If the position() method returns the position value in the current buffer, the remaining() method returns the length of the interval between limit and position. The code of the remaining() method in JDK is as follows

 
publicfinalint remaining(){return limit - position;}

所以compact()方法中第一条语句作用是将数组hb当前position所指向的位置开始复制长度为limit-position的数据到hb数组的开始出,其中使用到了ix()函数,这个函数是将参数值加上一个offset值,offset即一个偏移值,在这样的比如一个这样的场景对于一个很大的缓冲区,将其分成两段,第一段的起始位置是p1,长度是q1,第二段起始位置是p2,长度是q2,那么可以分别将这两段包装成一个HeapByteBuffer对象,然后这两个HeapByteBuffer对象(ByteBuffer的子类,默认实现)的offset属性分别设置为p1和p2,这样就可以通过在内部使用ix()函数来简化ByteBuffer对外提供的接口,在使用者看来,与默认的ByteBuffer并没有区别。

在compact函数中,接着将当前的缓冲区的position索引置为limit-position,limit索引置为缓冲区的容量,这样调用compact方法中就可以将缓冲区的有效数据全部移到缓冲区的首部,而position指向下一个可写位置。

比如刚刚创建一个ByteBuffer对象buff时,position=0,limit=capacity,那么此时调用buff.hasRemaining()则会返回true,这样来判断缓冲区中是否有数据是不行的,因为此时缓冲区中的存储的全部是0,但是调用一次compact()方法就可以将position置为limit值,这样再通过buff.hasRemaining()就会返回false,可以与后面的逻辑一起处理了。

ByteBuffer还有一个名为mark的方法,该方法设置mark索引为position的值,JDK中的代码如下:

publicfinalBuffer mark(){
    mark = position;returnthis;}

与其功能相反的方法为reset方法,即将position的值设置为mark,JDK中的代码如下:

 
publicfinalBuffer reset(){int m = mark;if(m <0)thrownewInvalidMarkException();
    position = m;returnthis;}

此外还有一个名为rewind的方法,这个方法将position索引置为0,mark索引置为-1,JDK中的代码如下:

publicfinalBuffer rewind(){
    position =0;
    mark =-1;returnthis;}

通过这些方法,就可以很方便的操作一个缓冲区,关键是要理解这些方法具体的作用,以及对三个索引值的影响(capacity是不变的)。

ByteBuffer继承自Buffer类,上面的方法四个索引值都定义在Buffer类中,操作索引值的方法也都定义在Buffer类中。

总结

通过对ByteBuffer中的四个索引值操作方法的分析,加深了对ByteBuffer的理解。理解ByteBuffer和其他几种Buffer的关键是要理解在使用中各个方法是如何操作索引值的,特别要注意的是clear方法并没有清除缓冲区的内容。

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326392621&siteId=291194637