一、概述
我们在之前的文章中介绍输入输出流的时候提到过,输入流InputStream的read方法从输入流中读取数据的时候,如果数据源中没有数据,那么这个方法会阻塞。输出流OutputStream的write方法在写入数据时同样也会阻塞,也就是之前介绍的输入、输出流都是阻塞式的。不仅如此,传统的输入、输出流都是通过字节的移动来处理的,也就是说面向流的输入输出每次只能处理一个字节,因此面向流的输入输出体系通常效率不高。
Java从1.4版本开始,提供了一系列改进输入输出流的类,这些类都存放在Java.nio包下,新IO和传统IO有相同的目的,都是用于处理输入输出,但是新的IO使用了不同的方式来处理输入输出,通过使用内存的方式来加快数据处理速度,并且提供了更加丰富的API以供使用者操作。
二、概念模型
Buffer使用内存映射的方式来处理输入输出,Buffer将文件或者文件的一部分映射到内存中,这样就可以像访问内存一样访问文件了,通过这种方式访问文件要快很多,所以传统的输入输出是面向流的处理,那么新的输入输出则是面向"块"的处理。
Buffer可以理解成一个容器,发送到channel或者从channel中读取数据都需要先经过buffer进行处理,此处的buffer类似于一个缓冲器,既可以多次访问,每次访问获取一点数据,也可以一次映射某"块"数据加以处理。
从内部结构上来看,buffer就是一个数组,可以保存相同类型的一组数据,它有三个比较重要的概念:容量(capacity)、界限(limit)、位置(position)。
容量:缓冲区的容量表示buffer可以存储的最多数据量,缓冲区的容量不可能为负值,并且创建后不可修改;
界限:可以被读取或者可被写入的最大位置;
位置:用于标志下一个可以被读取或者写入的位置索引;
当初始化一个buffer时,capacity为buffer边界最大值,limit为capacity,position为0,当写入一段数据之后,capacity不变,limit不变,position为写入数据最大值;再次写入数据,capacity不变,limit不变,position为两次数据和的最大值。当要读取数据时,设置buffer状态为读之后,capacity不变,limit为两次写入数据最大位置,position为0;读取一部分数据之后,capacity不变,limit不变,position为读取数据最大值位置。
buffer读写数据模型如下图所示:
三、buffer提供的API
1、java.nio.Buffer API
名称 |
返回值 |
功能 |
Buffer(int mark, int pos, int lim, int cap) |
无 |
构造方法,包私有,用户不能使用,一般由子类调用,并设置相关参数 |
position(int newPosition) |
Buffer |
重新设置position位置,如果Mark大于原position位置,则Mark置-1 |
limit(int newLimit) |
Buffer |
重新设置limit位置,如果position大于limit位置,则设置position为limit,如果Mark大于limit,则设置Mark为-1 |
mark() |
Buffer |
设置Mark的位置为position |
reset() |
Buffer |
重置position位置为Mark位置 |
clear() |
Buffer |
重置buffer,position置0,limit置为capacity,Mark置-1 |
flip() |
Buffer |
切换buffer为读模式,limit置为position位置,position置0,Mark置为-1 |
rewind() |
Buffer |
重新读取buffer中的数据,position置0,limit不变,Mark置-1 |
remaining() |
int |
buffer可读取或者可写入的元素个数,大小为limit - position |
isDirect() |
boolean |
判断该buffer是否为直接内存 |
2、java.nio.CharBuffer API
名称 |
返回值 |
功能 |
CharBuffer(int mark, int pos, int lim, int cap, char[] hb, int offset) |
无 |
构造方法,包私有方法,不能由用户调用,只能由子类构造 |
allocate(int capacity) |
CharBuffer |
创建固定大小的buffer |
wrap(char[] array, int offset, int length) |
CharBuffer |
创建buffer |
read(CharBuffer target) |
int |
读取buffer内容到新buffer中 |
slice() |
CharBuffer |
用于创建一个共享了原始缓冲区子序列的新缓冲区。新缓冲区的position值是0,而其limit和capacity的值都等于原始缓冲区的limit和position的差值。slice()方法将新缓冲区数组的offset值设置为原始缓冲区的position值,然而,在新缓冲区上调用array()方法还是会返回整个数组。 |
duplicate() |
CharBuffer |
用于创建一个与原始缓冲区共享内容的新缓冲区。新缓冲区的position,limit,mark和capacity都初始化为原始缓冲区的索引值,然而,它们的这些值是相互独立的。 |
asReadOnlyBuffer() |
CharBuffer |
创建只读缓冲区 |
get(char[] dst, int offset, int length) |
CharBuffer |
从offset开始,获取length长度的数据到char数组中 |
put(char[] src, int offset, int length) |
CharBuffer |
从offset开始,在char数组中获取length长度数据到buffer中 |
compact() |
CharBuffer |
丢弃已经释放的数据,保留未释放数据,使缓冲区为重新填充内容做准备 |
四、buffer继承体系
Buffer类的继承体系如下图所示:
Buffer是最底层抽象类,定义了buffer的基本功能,capacity、position、limit及Mark的定义和操作,它的直接子类定义了buffer的存储类型及实际存储地址,每个基本数据类型对应一个buffer子类,例如CharBuffer定义了存储地址为char[],存储类型为char,还定义了char数组的创建、获取、读取等操作。
buffer的应用类按照内存划分可以分为两类,堆内存类和非堆内存类,以Heap开始的类为堆内存类,例如HeapCharBuffer,非Heap开头的类称为非堆内存类也叫作直接内存存储类,例如DirecByteBuffer,如名称一致,它的数据存储在堆外。
五、应用
1、缓冲区使用步骤
使用缓冲区一般需要遵循以下几个步骤
1)创建缓冲区
2)写入数据到缓冲区
3)调用flip方法将缓冲区转换为读状态
4)从缓冲区读取数据
下面是一个使用Buffer的例子;
public class BufferTest {
public static void main(String[] args) {
//init
CharBuffer buffer = CharBuffer.allocate(6);
//write
buffer.put('a');
buffer.put('b');
buffer.put('c');
//transfer status prepare to read
buffer.flip();
System.out.println(buffer);
//read
char firstChar = buffer.get();
System.out.println(firstChar);
//clear
buffer.clear();
}
}
5)释放或重置缓冲区
2、创建缓冲区
创建缓冲区主要有两种方式,调用allocate方法或者调用wrap方法,我们一CharBuffer为例说明:
1)调用allocate方法实际上会返回一个HeapCharBuffer类,这个方法会把数据存储在堆中,源码如下:
public static CharBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapCharBuffer(capacity, capacity);
}
HeapCharBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new char[cap], 0);
}
CharBuffer(int mark, int pos, int lim, int cap, // package-private
char[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
Buffer(int mark, int pos, int lim, int cap) { // package-private
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
CharBuffer不仅可以分配堆空间,也可以分配直接内存,例如调用allocateDirect方法可以分配直接内存,其内部是通过 unsafe.allocateMemoryfangfa实现直接内存分配的,如果使用此方法,则hasArray方法会返回false.
2)调用wrap方法实际上也会返回HeapCharBuffer类,这里的内存空间大小是以char数组定义的,它的重载方法可以缓冲区初始化的时候设置position和limit位置。如下代码会创建一个capacity=10、limit=8,position=3的Buffer。
char[] myArray = new char[10];
CharBuffer charbuffer = CharBuffer.wrap (myArray, 3, 5);
3、复制缓冲区
缓存区复制有三种方式,调用duplicate方法、调用asReadOnlyBuffer方法、调用slice方法。
调用duplicate方法会创建缓冲区的一个拷贝,不是深拷贝,是浅拷贝,也就是会创建原缓冲区的一个引用,缓冲区的capacity、limit、position各自独立,但是数据共享,修改一个缓冲区的元素会影响另一个缓冲区,但是读取位置不会影响,如下代码所示:
public class BufferTest {
public static void main(String[] args) {
//init
CharBuffer buffer1 = CharBuffer.allocate(6);
CharBuffer buffer2 = buffer1.duplicate();
//write
buffer1.put('a');
buffer1.put('b');
buffer1.put('c');
//transfer status prepare to read
buffer1.flip();
System.out.println("buffer1.out:" + buffer1);
System.out.println("buffer2.out:" + buffer2);
//read
char firstChar = buffer1.get();
System.out.println("buffer1.get.data:" + firstChar);
System.out.println("buffer1.out:" + buffer1);
System.out.println("buffer2.out:" + buffer2);
buffer1.clear();
}
}
执行结果如下图所示:
调用asReadOnlyBuffer方法会产生一个只读缓冲区,与duplicate方法一致,唯一的区别是这个缓冲区是只读的,如果进行写入操作,会抛出异常。代码如下图所示:
public class BufferTest {
public static void main(String[] args) {
// init
CharBuffer buffer1 = CharBuffer.allocate(6);
CharBuffer buffer2 = buffer1.asReadOnlyBuffer();
// write
buffer1.put('a');
buffer1.put('b');
buffer1.put('c');
// transfer status prepare to read
buffer1.flip();
System.out.println("buffer1.out:" + buffer1);
System.out.println("buffer2.out:" + buffer2);
// read
buffer1.put('d');
buffer2.put('e');
System.out.println("buffer1.out:" + buffer1);
System.out.println("buffer2.out:" + buffer2);
buffer1.clear();
buffer2.clear();
}
}
执行结果如下图所示:
调用slice方法相当于对原缓冲区进行了分割,该方法创建一个新的缓冲区,新建缓冲区会议原缓冲区的position为起始点,以limit为结束,使用剩余元素数量作为新缓冲区的容量,该缓冲区与原始缓冲区共享数据。
4、读写缓冲区
写入数据到缓冲区有两种方式,从Channel写数据到buffer中,或者调用buffer的put方法写入数据。
读取缓冲区的数据也有两种方式,使用channel直接读取buffer中的数据,或者调用buffer的个头方法。
5、缓冲区比较
当满足下列条件时,两个buffer相等:
1)有相同的类型;
2)buffer中剩余元素个数相同;
3)所有数据从开始到结束依次相同;
equals源码如下:
public boolean equals(Object ob) {
if (this == ob)
return true;
if (!(ob instanceof CharBuffer))
return false;
CharBuffer that = (CharBuffer)ob;
if (this.remaining() != that.remaining())
return false;
int p = this.position();
for (int i = this.limit() - 1, j = that.limit() - 1; i >= p; i--, j--)
if (!equals(this.get(i), that.get(j)))
return false;
return true;
}