写在前面
作为NIO核心组件之一,Buffer
起着数据容器的作用.那么它和我们传统上使用java.io开发有什么不同呢?
在java.nio的包描述里面,有这样一段话:
The central abstractions of the NIO APIs are:
Buffers, which are containers for data;
Charsets and their associated decoders and encoders,
which translate between bytes and Unicode characters;
Channels of various types, which represent connections
to entities capable of performing I/O operations; and
Selectors and selection keys, which together with
selectable channels define a multiplexed, non-blocking
I/O facility.
1 | public void (){ |
在java.io
中,最核心的一个概念是流(Stream),在java中,io编程被称为面向流的编程.流是信息的载体.
java.nio 是面向块的编程
ByteBuffer
概念
ByteBuffer
对象是固定数量的数据的容器,和List
下的ArrayList
类似,内核都是一个数组,与ArrayList
不同的是,ByteBuffer
内部是一个final byte[] hb
.
操纵ByteBuffer
,本质上就是在操纵数组,所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息:
- 标记(
mark
): 一个备忘位置. - 位置(
position
): 下一个要被读或者写的元素的索引.由get
或者put
自动更新. - 上界(
limit
): 缓冲区现存元素的计数 - 容量(
capacity
): 缓冲区的最大容量,在这里,是指的数组的长度.在构造ByteBuffer
的必须指定,且永远不能被修改.
四个属性的关系为:0<= mark <= position <= limit <= capacity
默认情况下,在创建一个ByteBuffer
的时候,limit=capacity
,mark=-1
,position=0
.
直接缓冲区和非直接缓冲区
ByteBuffer有三个子类(这里不讨论HeapByteBufferR
和DirectByteBufferR
)
三个子类中,DirectByteBuffer
和HeapByteBuffer
属于包可见的子类,不对外提供服务,MappedByteBuffer
是和文件相关的缓冲区.C/C++中存在一个名词,叫做共享内存,java的共享内存,可以由MappedByteBuffer
实现.
ByteBuffer
是抽象类,不能直接new
,但是其提供了两个工厂方法allocate
和allocateDirect
.
先看一下allocateDirect
:
1 | public static ByteBuffer allocateDirect(int capacity) { |
再来allocate
1 | public static ByteBuffer allocate(int capacity) { |
现在已经明确了,DirectByteBuffer
直接通过Unsafe
来分配内存,相当于直接从物理内存中分配内存.由DirectByteBuffer
分配的内存,称之为直接缓冲区.
而HeapByteBuffer
在其构造函数中,new
了一个byte[cap]
,而且我们都知道,new
出来的对象,是分配在堆上的,而堆在虚拟机内.所以,由HeapByteBuffer
分配的内存,称之为非直接缓冲区.
何时使用直接缓冲区? jdk官方文档上描述的很清楚:
1
2
3
4
5
6
7
此方法(allocateDirect)返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区. |
使用ByteBuffer
先来看一下ByteBuffer
中操作数据的两个方法get
和put
,其重载方法如下:
1 |
|
上述几个方法,分成两类,绝对操作和相对操作.
相对操作和绝对操作
绝对操作:public abstract byte get(int index)
和public abstract ByteBuffer put(int index, byte b)
.只要参数0<=index<limit
,可以无视position
,对ByteBuffer
进行任意存取操作.
之所以限制index
的范围:0<=index<limit
1 | final int checkIndex(int i) { package-private |
对于相对操作,我曾经遇到过一个坑:
1 | public static void main(String[] args) { |
根据ASCII表,第81个字符是Q.猜测一下,这段代码,会打印什么?如果你猜成了”QQQQQQ”,不怪你,都是学习嘛.如果你认为打印为空,那结果就是为空.
这就涉及到相对操作的问题了.
先来看一下,当调用result.put(bytes, 0, bytes.length)
时,会发生什么?
1 | //根据上下文语境,这里的length为66 |
当执行buffer.get(tag,0,6)
时
1 | // 根据上下文语境,这里的length为6 |
由解析得出,此时tag
数组中的元素,是ByteBuffer中第66到72下标的元素,而我们put进去的长度为66,所以我们得到的是无效值.
接着,就可以引申出相对操作和绝对操作的概念:
以limit
为参照系的操作,称为绝对操作,绝对操作有两个get(int index)
和put(int index)
,之所以以limit
作为参照系,是因为参数的值不能大于limit
,否则就抛IndexOutOfBoundsException
异常.
以position
为参照系的操作,称为相对操作,相对操作会改变position
的值,无论是get
还是put
.而且,无论是get
还是put
,都是以当前position
为基点,向后增大.
现在已经明确了相对操作和绝对操作的概念,现在想要让上面的程序打印”QQQQQQ”,怎办呢?
只需要让position
重置为0,不就行了嘛.
于是,在返回之前,可以调用buffer.position(0)
.
除此之外,ByteBuffer还提供了其他有用的工具.
rewind
buffer.get()
无论是put()
还是get()
,都会造成position
的改变