Java NIO之缓冲区Buffer分析

目录

Buffer介绍

ByteBuffer介绍

ByteBuffer案例

总结


Buffer介绍

1.Buffer简介

      缓冲区(Buffer)是中NIO中基础的内容,存在于包java.nio下面.一个Buffer对象可以看做是一个存储数据的容器,数据被存储到这里后可以进行检索。缓冲区工作与通道关联,我们不与通道直接进行交互,而是通过缓冲区将数据传送到通道里,或者从通道获取数据放到缓冲区。Buffer类是一个顶层的抽象类,里面定义了缓冲区操作的基本方法。Buffer的子类包含了java基本类型中除布尔类型以外的其他基本数据类型对应缓冲区。需要注意的是数据传输的基本单位是字节,所以唯一与通道交互的缓冲区是ByteBuffer,存储的是原始的字节数据,而其他的缓冲区提供了用java基本数据类型来查看ByteBuffer中字节数据的视图,比如调用asCharBuffer(),就是将字节转换成字符查看ByteBuffer里面的数据,实际存储数据的真正地方依旧是ByteBuffer缓冲区。

      ByteBuffer是原始字节存储的地方,如要将字节进行正确转换成字符数据,需要将字节数据先进行特定编码转成字符输入到字符缓冲区,或者在输出的时候将数据按指定字符集进行解码处理,否则会出现乱码的情况。

java.nio包下面包含java基本类型中除boolean类型的缓冲区有ByteBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,CharBuffer

DoubleBuffer,此外还有MappedByteBuffer(继承自ByteBuffer),还有不能直接访问的HeapByteBuffer,DirectByteBuffer。其中HeapByteBuffer是调用ByteBuffer中allocate()时底层的实现,分配空间是受JVM管控的堆内存。而DirectByteBuffer是ByteBuffer.allocateDirect()方法时底层实现,分配的空间是虚拟机之外的内存。Buffer的子类子类如下:

                 

基本数据类型  缓冲区
byte ByteBuffer
short ShortBuffer
int IntBuffer
long LongBuffer
boolean -
char  CharBuffer
float FloatBuffer
double DoubleBuffer
- MappedByteBuffer
- HeapByteBuffer(底层实现,无法直接访问,JVM堆内存)
- DirectByteBuffer(底层实现,无法直接访问,JVM外的内存)

2.内部变量

private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
long address;
  • mark----调用mark()方法将会当前位置postion保存,-1表示的是当前没有标记。
  • position----下一个要写入或者读取元素的索引位置,position值不会为负数,并且不会大于limit的值。
  • limit----第一个不能写入或者读取的元素的索引位置,limit值不会为负数,并且不会大于capacity的值。
  • capacity----缓冲区的容量,capacity值不会为负数,并且不能被改变。

所以Buffer变量内部变量满足:0 <= mark<=position <= limit <= capacity.

2.内部方法

public final int capacity() {}
public final int position() {}
public final Buffer position(int newPosition) {}
public final int limit() {}
public final Buffer limit(int newLimit) {}
public final Buffer mark() {}
public final Buffer reset() {}
public final Buffer clear() {}
public final Buffer flip() {}
public final Buffer rewind() {}
public final int remaining() {}
public final boolean hasRemaining() {}
public abstract boolean isReadOnly()
public abstract boolean hasArray();
public abstract Object array();
public abstract int arrayOffset();
public abstract boolean isDirect()
  • capacity()----返回的是缓冲区容量的大小。
  • position()----返回position的值大小。
  • position(int newPosition)----设置缓冲区中position的值。
  • limit()----返回此缓冲区的限制值limit.
  • limit(int newLimit)----设置此缓冲区的限制值Limit,并返回缓冲区对象。
  • mark()----标记缓冲区中位置。
  • reset()----将缓冲区位置position设置为标记的位置。
  • clear()----清空缓冲区,调用此方法将position设置为0,limit的值设置为缓冲区容量值,标记将被丢弃。
  • flip()----调用此方法将limit设置当前position的值,position设置为0,有标记情况下,标记将被丢弃。
  • rewind()----调用此方法将position值设置为0,标记将被丢弃.,limit值不会变化。
  • remaining()----返回当前位置与限制limit之间的值,即limit-position的值。
  • hasRemaing()---布尔类型,当前位置与限制值之间是否有元素。
  • isReadOnly()----抽象方法,返回此缓冲区是否只读。
  • hasArray()----抽象方法,是否能用一个可访问的数组来备份缓冲区。
  • array()----抽象方法,用一个可访问的数组来备份缓冲区。
  • arrayOffset()----抽象方法,缓冲区中的第一个元素到备份数组中的偏移量。
  • isDirect()----判断此缓冲区是否为直接缓冲区(调用allocateDirect()分配的是直接缓冲区,即不受JVM管控的堆外内存)。

ByteBuffer介绍

a.创建缓冲区方法

public static ByteBuffer allocateDirect(int capacity){}
public static ByteBuffer allocate(int capacity) {}
public static ByteBuffer wrap(byte[] array,int offset, int length){}
public static ByteBuffer wrap(byte[] array) {}
public final boolean hasArray() {}
public final byte[] array() {}
public final int arrayOffset() {}

allocateDirect()与allocate()相比较,分配的是直接缓冲区,直接缓冲区的创建是由操作系统方面的代码分配的,是JVM堆外的内存,而allocate()分配的是JVM堆内存,两者各有利弊。直接分别缓冲区脱离了JVM的束缚,是IO操作方面的最佳选择,但创建和销毁直接缓冲区花销更大,非直接缓冲区分配的是JVM堆内存,创建非直接缓冲区向通道传输数据底层可能会先创建一个临时直接缓冲区,然后将非直接缓冲区数据放到临时直接缓冲区,使用临时缓冲区与IO进行交互,这会创建大量对象,但垃圾对象能得到及时回收。

wrap()方法创建可视数组作为缓冲区的备份存储器,对于数组更改还是缓冲区更改,都会直接影响到对方。对于wrap()方法需要注意的是:

byte[] byteArray = new byte[100];

ByteBuffer buffer = ByteBuffer.wrap(byteArray,10,50);

创建的是一个position为10,limit为10+50=60,容量大小为100的字节缓冲区,而不是字节数组byteArray的子集(容易混淆的地方)

hasArray()返回的是缓冲区是否有一个可存取的备份数组。

array()返回的是这个缓冲区所使用的数组存储空间的引用。

arrayOffset()返回缓冲区数据在数组存储开始位置的偏移量。

b.存取方法。

public abstract byte get()
public abstract byte get(int index)
public abstract ByteBuffer put(byte b)
public abstract ByteBuffer put(int index, byte b)

get()和put()不带索引的方法,存取的相对位置的元素。带了索引index的get()和put()方法,读取的是指定索引位置的元素或者将元素存储到指定位置。

c.批量存取方法

public ByteBuffer get(byte[] dst, int offset, int length) {}
public ByteBuffer get(byte[] dst){}
public ByteBuffer put(byte[] src, int offset, int length) {}
public final ByteBuffer put(byte[] src) {}
public ByteBuffer put(ByteBuffer src){}

缓冲区的目的就是为了一次可以存取整块数据,批量数据存取方法get()或者put(),实现了数据从缓冲区或者到缓冲区的高效存取。get()方法实现将缓冲区数据批量复制放到字节数组或者字节数组的子集中,其中offset是字节数组中偏移量,length是数据长度。如果没有指定长度情况下,必须要填满整个数组。

byte[] bytes = new byte[101];

ByteBuffer buffer = ByteBuffer.allocate(100);

buffer.get(bytes,10,92); 将缓冲区复制数据到字节数组bytes中,从bytes的索引10开始,填充长度为92个,超过了bytes的长度,将会抛出IndexOutOfBoundsException

buffer.get(bytes);从缓冲区复制数据到字节数组bytes中,没有设置长度,即要填充101个字节,但是缓冲只有100个字节,所以将会抛出异常BufferUnderflowException

d.比较方法

public boolean equals(Object ob)
public int compareTo(ByteBuffer that)

其中equals()方法比较的是两个缓冲区剩余内容是否一样,相等充要条件:第一点是两个对象必须都是Buffer类型且同一个数据类型的Buffer;其次Buffer容量可以不一致,只要剩余元素数目相等;第三点是get()函数返回的剩余元素的序列一致。

compareTo()方法容许以词典顺序比较两个缓冲区中的剩余元素序列,不容许不同对象之间的比较,如果传递了对象类型错误,将会抛出 ClassCastException异常

e.转换视图的方法

public abstract xxxx getXxxx()
public abstract xxxx getXxxx(int index)
public abstract ByteBuffer putXxxx(xxxx value)
public abstract ByteBuffer putXxxx(int index, xxxx value)
public abstract XxxxBuffer asXxxxBuffer()

ByteBuffer中有将缓冲区字节数据映射成除了布尔类型数据外其他原始数据类型的方法。并且提供将字节映射为指定数据类型的getXxxx()方法和将java原始数据类型存储到字节缓冲区的putXxxx()方法,其中Xxxx可以是除布尔类型以外的基本数据类型short,int ,long ,char ,float,double。其中asXxxxBuffer()提供了将字节缓冲区ByteBuffer转换成其他数据类型的视图来查看字节数据,但是要注意的是实际存储数据的地方依旧是ByteBuffer。例如:double数据类型的方法。

public abstract double getDouble()
public abstract double getDouble(int index)
public abstract ByteBuffer putDouble(double value)
public abstract ByteBuffer putDouble(int index, double value)
public abstract DoubleBuffer asDoubleBuffer()

f.复制缓冲区

public abstract ByteBuffer slice()
public abstract ByteBuffer duplicate()
public abstract ByteBuffer asReadOnlyBuffer()

slice()创建的原始缓冲区当前位置开始的新缓冲区,并且与原始缓冲区共享一段数据元素子序列,其中只读属性和基本属性包含position,limit,mark属性与原始缓冲区是一致的。

duplicate()创建的是与原始缓冲区相似的缓冲区,新缓冲区会继承原始缓冲区只读属性,两个缓冲区共享容量和数据元素,但是有各自的position,limit,mark属性,并且一个缓冲区的元素改变也会反映在另外一个缓冲区上。

asReadOnlyBuffer()创建的是与duplicate()操作方法类似的缓冲区,但只是只读视图的缓冲区。

g.compact()/rewind()/flip()/mark()/reset()等

调用compact()方法将缓冲区数据进行压缩,具体是丢弃释放的元素,保存未释放的元素。也就是将position位置之前的元素全部丢弃,然后将position开始之后元素全部向前移动。缓冲区当前位置将会被置为缓冲区中最后一个“存活”元素后插入数据的位置。

其中rewind().flip(),mark(),reset()方法完全继承自父类Buffer。

新创建的ByteBuffer缓冲区,position=0,limit与capacity的大小相等。

                                                           

使用put()方法进行填充后。

                                                             

就缓冲区上面目前状态,分别调用flip()rewind()方法,flip()与rewind()方法都会将缓冲区中position置为0。但两者的区别是调用flip()方法会将将limit设置为position,然后将position置为0;rewind()方法不会改变limit的值。

                                                           

调用compact()方法,会将position之前的数据丢弃。后面的剩余未读取数据会整体向前移动,而后面的数据不会变化,position会设置为缓冲区中最后一个数据插入的位置,limit值会被值为capacity。例如:下图中"HE"已经被读取,将被丢弃,后面的数据整体向前移动,可以看到"LLO"复制到了原先缓冲区中“HEL”位置,但是后面的两个数据“LO”并没有变化,position设置为原缓冲区最后一个数据“O”的后面插入数据的位置,即缓冲区“O”后面的数据“L”位置。

                                                               

标记方法调用mark()方法,会记录position的位置,当调用reset()方法时会将position重置为标记位置。

                                                               

h.存储字节顺序

 public final ByteOrder order() {}
 public final ByteBuffer order(ByteOrder bo) {}

字节顺序的存储是由计算机硬件决定的。字节存储到缓冲区中,存储量大于一个字节情况,就会出现字节顺序问题,比如short(2个字节),double(8个字节)等超过一个字节的数据。字节存储顺序有两种:大端(big endian)和小端(little-endian)。大端就是将高位字节存储到低位地址,小端就是高位字节存储到高位地址。内存地址是低位到高位,而数据是高位字节到低位字节。如:0x4F6B,大端存储就是4F6B,小端存储就是6B4F。多数默认的存储数据的顺序是大端。

ByteBuffer案例

public class BufferDemo {
  public static void main(String[] args) {
    testViewBuffer();
    testOtherMethod();
    testByteOrder();
  }

   //字节存储顺序的测试 
  private static void testByteOrder() {
    ByteBuffer buffer = ByteBuffer.allocate(6);
    buffer.asCharBuffer().put("abc");
    System.out.println(Arrays.toString(buffer.array()));
    buffer.rewind();
    buffer.order(ByteOrder.BIG_ENDIAN);
    buffer.asCharBuffer().put("abc");
    System.out.println(Arrays.toString(buffer.array()));
    buffer.rewind();
    buffer.order(ByteOrder.LITTLE_ENDIAN);
    buffer.asCharBuffer().put("abc");
    System.out.println(Arrays.toString(buffer.array()));
  }  

  private static void testOtherMethod() {
    ByteBuffer buffer = ByteBuffer.allocate(100);
    buffer.put("helloworld".getBytes());
    
    //调用flip()方法后,position=0,limit=10。
    //flip()方法从写入模式切换到读取模式
    buffer.flip();
    System.out.println(buffer); 
    //ByteBuffer中元素通过char视图浏览,需要解码。
    String encoding = System.getProperty("file.encoding");
    System.out.println(Charset.forName(encoding).decode(buffer));

    buffer.rewind();
    System.out.println(buffer);
    //测试mark(),reset()方法。
    System.out.println((char)buffer.get());
    buffer.mark();
    System.out.println("mark()-------"+(char)buffer.get());
    int i=0;
    while(i++<4) {
      System.out.print((char)buffer.get());
    }
    System.out.println();
    buffer.reset();
    System.out.println("reset()-------"+(char)buffer.get());
    System.out.println("读取元素"+(char)buffer.get());
    //关于compact()方法并非数据整体前移。
    //而是整体移动的时候,还是会保留部分数据
    buffer.compact();
    System.out.println(buffer);
    while(buffer.hasRemaining()) {
      System.out.print((char)buffer.get());
    }
    System.out.println();
    
    buffer.rewind();
    buffer.asCharBuffer().put("a");
    System.out.println(buffer.getChar());
    
    buffer.asShortBuffer().put((short)11247);
    System.out.println(buffer.getShort());
  }
  
  //测试其他视图浏览ByteBuffer里面的元素
  private static void testViewBuffer() {
    ByteBuffer buffer = ByteBuffer.wrap(new byte[] {0,0,0,0,0,0,0,'a'});
    buffer.rewind();
    System.out.println("----ByteBuffer----");
    while(buffer.hasRemaining()) {
      System.out.print(buffer.position()+"-->"+buffer.get()+", ");
    }
    System.out.println();
    
    //char占两个字节
    buffer.rewind();
    CharBuffer charBuffer = buffer.asCharBuffer();
    System.out.println("----CharBuffer----");
    while(charBuffer.hasRemaining()) {
      System.out.print(charBuffer.position()+"-->"+charBuffer.get()+", ");
    }
    System.out.println();
    
    //short占用两个字节
    buffer.rewind();
    ShortBuffer shortBuffer = buffer.asShortBuffer();
    System.out.println("----ShortBuffer----");
    while(shortBuffer.hasRemaining()) {
      System.out.print(shortBuffer.position()+"-->"+shortBuffer.get()+", ");
    }
    System.out.println();
    
    //int占用四个字节
    buffer.rewind();
    IntBuffer intBuffer = buffer.asIntBuffer();
    System.out.println("----IntBuffer----");
    while(intBuffer.hasRemaining()) {
      System.out.print(intBuffer.position()+"-->"+intBuffer.get()+", ");
    }
    System.out.println();
    
    //long占用8个字节
    buffer.rewind();
    LongBuffer longBuffer = buffer.asLongBuffer();
    System.out.println("----LongBuffer----");
    while(longBuffer.hasRemaining()) {
      System.out.print(longBuffer.position()+"-->"+longBuffer.get()+", ");
    }
    System.out.println();
    
    //float占用两个字节
    buffer.rewind();
    FloatBuffer floatBuffer = buffer.asFloatBuffer();
    System.out.println("----FloatBuffer----");
    while(floatBuffer.hasRemaining()) {
      System.out.print(floatBuffer.position()+"-->"+floatBuffer.get()+", ");
    }
    System.out.println();
    
    //double占用8个字节
    buffer.rewind();
    DoubleBuffer doubleBuffer = buffer.asDoubleBuffer();
    System.out.println("----DoubleBuffer----");
    while(doubleBuffer.hasRemaining()) {
      System.out.print(doubleBuffer.position()+"-->"+doubleBuffer.get()+", ");
    }
    System.out.println();
  }
}

运行结果:

testViewBuffer()方法运行结果:

----ByteBuffer----
0-->0, 1-->0, 2-->0, 3-->0, 4-->0, 5-->0, 6-->0, 7-->97, 
----CharBuffer----
0-->, 1-->, 2-->, 3-->a,
----ShortBuffer----
0-->0, 1-->0, 2-->0, 3-->97, 
----IntBuffer----
0-->0, 1-->97, 
----LongBuffer----
0-->97, 
----FloatBuffer----
0-->0.0, 1-->1.36E-43, 
----DoubleBuffer----
0-->4.8E-322

testOtherMethod()方法运行结果:

java.nio.HeapByteBuffer[pos=0 lim=10 cap=100]
helloworld
java.nio.HeapByteBuffer[pos=0 lim=10 cap=100]
h
mark()-------e
llow
reset()-------e
读取数据l
java.nio.HeapByteBuffer[pos=7 lim=100 cap=100]
rld
a
11247

testByteOrder()方法运行结果:

[0, 97, 0, 98, 0, 99]         
[0, 97, 0, 98, 0, 99]
[97, 0, 98, 0, 99, 0]

总结

1.rewind()与flip()都是将缓冲区的position置为0,不同点是rewind()不会改变limit的值,flip()方法会先设置limit为position,然后将position值置为0。

2.从通道中获取的数据原始数据是字节,存储到ByteBuffer缓冲区,用其他数据类型视图查看字节数据。例如调用asCharBuffer(),也只是通过char视图浏览ByteBuffer缓冲区中数据,实际存储数据的地方依旧是ByteBuffer。

3.ByteBuffer.wrap(byte[] array,int offset, int length)创建的缓冲区并非是array的子集,而是设置了position=offset,

limit=offset+llength,缓冲区的容量capacity大小是array.length。这是比较容易混淆的。

4.关于compact()压缩方法,压缩数据除了丢弃已读取数据,将未读取数据整体向前移动,还会保留后面部分数据。此外还会设置position和limit的值。

参考文献:《Java编程思想》《Java NIO》

猜你喜欢

转载自blog.csdn.net/lili13897741554/article/details/82734656
今日推荐