上一篇讲了基本的缓冲区概念,以及NIO的缓冲区属性以及部分操作。
现在让我们在看看缓冲区的一些细节。
1、缓冲区的创建
之前也许你已经看到了。我们使用alocate方法创建一个缓冲区。下面是一个创建方法集合。
注意,这些都是静态方法。
wrap方法使用一个现有的数组作为缓冲区备份。这里很重要,因为,其实缓冲区都需要这样一个真正存放数据的地方,我们可以看看如果allocate方法
HeapByteBuffer又是什么呢?继续跟踪
这里解释下,HeapByteBuffer是ByteBuffer的一个实现类。她直接使用父类的构造函数来初始化。super第一个参数是mark,-1表示undefined,然后是position,然后是limit,然后是capacity,接下来就是一个真正存放的数组,最后是数组的偏移。
这下可以理解了,缓冲区实际上是由缓冲区的几个属性、操作和一个存放数据的数组(内存)组成的。我们如果改变数组,或者是通过缓冲区改变数组,都会影响双方的。
例如 CharBuffer charBuffer = CharBuffer.wrap(myArray,12,42)
这样的操作,设置了初始的position和limit。我们可以使用clear函数,然后再开始填充缓冲区,这里的缓冲区,并不是从12到42,而是从0到myArray.length
这里,我们还应该看到一个函数,hasArray(),这说明并非所有的缓冲区都包含一个数组,那么什么样的缓冲区不包含数组呢?答案是直接缓冲区。通过allocate和wrap创建的缓冲区都是间接的。直接缓冲区我们待会儿说。但是无论是直接还是间接,事实上缓冲区都必须又一个存放数据的地方。
如果是直接缓冲区,我们是不能获取到数组的,所以array方法不能再直接缓冲区中调用,否则抛出UnsupportedOperationException异常。当然,如果是只读的缓冲区,我们也不能调用array方法或者是arrayOffset方法。
arrayOffset方法是返回作为备份数组的起始下标。这就说明,并非一个数组必须是所有元素都在缓冲区内,而是可以拆分的。但是这里还没讲到,所以,以上提到的缓冲区,如果使用arrayOffset方法,返回都是0.
为了方便,我们的CharBuffer提供了几个方法
我们可以这样用。
CharBuffer charBuffer = CharBuffer.wrap ("Hello World");
这对于字符集码和正则表达式处理都是很方便的。
2、复制缓冲区
这里的复制,仅仅是复制缓冲区,而不是数据。
这三个函数可以完成复制功能。前两个顾名思义,容易理解,最后一个slice,其实也很容易理解,分割缓冲区,这时,起决于position和limit,新的缓冲区将针对原来的数据中的position到limit之间的数据作为新的缓冲区数据。这时,arrayOffset方法返回的就是position的位置了。
例如:要创建一个映射到数组位置12-20(9个元素)的buffer对象:
3、字节缓冲区
虽然我们有很多缓冲区类型,除了boolean型都有一一对应。
但是,ByteBuffer作为一个基本的缓冲区,其作用非同寻常。我们的通道只和字节缓冲区打交道。并且,其他的缓冲区和字节缓冲区都可以转换。
说到这里,我们就不得不理解计算机如何存储数据了。
实际上,内存是按照字节为单位来存放数据的。现在最流行的便是8个位一个字节。我们熟悉的是int占用4个字节,这样,这四个字节如何排列,又成为一个问题。
这个问题已经是硬件问题了,因为不同的硬件有不同的表现。
对于intel处理器,一般都是小端(little endian)
而对于摩托罗拉,Sun的处理器,一般都是大端(big endian)
还有网络字节顺序实际上也是大端的。
那么这两者有什么区别呢?
假设 一个int值 0x01234567 (注意是16进制),内存从左到右增长。地址左小右大
那么他在大端的存放 就类似 : 0x01 0x23 0x45 0x67
那么他在小端的存放,就应该是: 0x67 0x45 0x23 0x01
所以我们的字节操作中有了顺序问题。
在JVM中,实际上我们是使用大端的。
看看关于Order的操作。
order返回系统使用的字节顺序。注意如果系统字节顺序和JVM不一致,可能性能上会有些许慢。
现在知道了字节顺序,那么字节缓冲区转换为其他缓冲区,也就清楚了。
比如,我们有一个4字节缓冲区 0x01 0x23 0x45 0x67
转换为一个IntBuffer,那么,根据字节顺序,如果是大端,直接就是 0x1234567
如果是小端顺序,那么读出来就是 0x67452301
注意:视图缓冲区一旦创建,字节顺序是不可改变的。视图缓冲区后面讲到。
4、直接缓冲区
前面说到,allocate和wrap创建的都是间接缓冲区。间接缓冲区,就是在操作系统和JVM之间其实有一层,我们如果对一个间接缓冲区使用,那么首先是要在操作系统层次创建一个临时缓冲区,然后copy过去,再操作,在删除临时缓冲区。这都很麻烦。
所以,直接缓冲区。也就是在操作系统,而不是JVM进程中创建缓冲区,这就绕过了JVM栈,所以依赖于具体的操作系统。事实上,在操作系统中创建缓冲区,比起在JVM中,更加消耗资源。(因为JVM中是已经划分好的内存,直接设置一下,而操作系统中要通过系统调用,个人理解。)
使用 allocateDirect方法就可以获得直接缓冲区。
public static ByteBuffer allocateDirect (int capacity)
public abstract boolean isDirect( );
isDirect对于直接缓冲区的非字节视图缓冲区,也可能返回true
5、视图缓冲区
可以这样理解,字节缓冲区是所有缓冲类型的基础。我们在通道中,网络中文件流中四处传递字节缓冲区。但是,一旦进入应用,我们就要通过视图缓冲区来解读具体的数据了。
字节缓冲区提供了很多这样的API
前面提到过从ByteBuffer到IntBuffer的转换。其实这种转换,是一种解释包装。
所以需要提供一个顺序,然后按照顺序和类型,转换成另外一种视图,而原始数据其实是不变的。
例如,我们可以这样;
注意,java中char是占两个字节的。我们的转换可以看下图:
[img]http://dl.iteye.com/upload/attachment/0066/6532/8141e964-096f-37a5-8adc-f7623cb2e9da.jpg[/img]
新的charBuffer视图其实还是使用的原来的缓冲区的数据,只是这时的元素变成了2个字节的char了。
看一段代码:
结果:
[i]pos=0, limit=10, capacity=10:'java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]'
pos=0, limit=5, capacity=5:'Hello'[/i]
可以看到,转换为视图后,limit和capacity都变了。
6、数据元素视图
上面的视图使用了一个视图类来转换。在ByteBuffer中,我们可以直接获取不同视图的元素。
这些操作一看便知。前面也提过其实就是根据大端小端的字节顺序,和这些数据类型的长度来组织数据。
这里需要注意的是:如果get或者是put时,数据不足,或者空间不够,都会发生异常。
如果不是用putXX的方法,直接getXX,那么可能产生意想不到的问题。
7、无符号数存取
对于java来说,没有无符号数处理(除了char)。为此,书中给出一个程序,用来处理无符号数缓冲区。
其基本原理是,使用比要存取的数据类型更大的数据类型来存放这个数,同时使用与运算强制设置符号位为0,剩余的位就可以作为数据而不是符号位了。
这里省略了其他部分。这样我们就可以看到带符号的
8、内存映射缓冲区
映射缓冲区是与文件存储的数据元素关联的字节缓冲区,它通过内存映射来访问。映射缓
冲区通常是直接存取内存的,只能通过 FileChannel 类创建。映射缓冲区的用法和直接缓冲
区类似,但是 MappedByteBuffer 对象具有许多文件存取独有的特征。
现在让我们在看看缓冲区的一些细节。
1、缓冲区的创建
之前也许你已经看到了。我们使用alocate方法创建一个缓冲区。下面是一个创建方法集合。
public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable {
// This is a partial API listing
public static CharBuffer allocate(int capacity);
public static CharBuffer wrap (char [] array);
public static CharBuffer wrap (char [] array, int offset, int length);
public final boolean hasArray( )
public final char [] array( )
public final int arrayOffset( )
}
注意,这些都是静态方法。
wrap方法使用一个现有的数组作为缓冲区备份。这里很重要,因为,其实缓冲区都需要这样一个真正存放数据的地方,我们可以看看如果allocate方法
/**
* Allocates a new byte buffer.
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, and its mark will be undefined. It will have a {@link #array
* </code>backing array<code>}, and its {@link #arrayOffset </code>array
* offset<code>} will be zero.
*
* @param capacity
* The new buffer's capacity, in bytes
*
* @return The new byte buffer
*
* @throws IllegalArgumentException
* If the <tt>capacity</tt> is a negative integer
*/
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer又是什么呢?继续跟踪
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
这里解释下,HeapByteBuffer是ByteBuffer的一个实现类。她直接使用父类的构造函数来初始化。super第一个参数是mark,-1表示undefined,然后是position,然后是limit,然后是capacity,接下来就是一个真正存放的数组,最后是数组的偏移。
这下可以理解了,缓冲区实际上是由缓冲区的几个属性、操作和一个存放数据的数组(内存)组成的。我们如果改变数组,或者是通过缓冲区改变数组,都会影响双方的。
例如 CharBuffer charBuffer = CharBuffer.wrap(myArray,12,42)
这样的操作,设置了初始的position和limit。我们可以使用clear函数,然后再开始填充缓冲区,这里的缓冲区,并不是从12到42,而是从0到myArray.length
这里,我们还应该看到一个函数,hasArray(),这说明并非所有的缓冲区都包含一个数组,那么什么样的缓冲区不包含数组呢?答案是直接缓冲区。通过allocate和wrap创建的缓冲区都是间接的。直接缓冲区我们待会儿说。但是无论是直接还是间接,事实上缓冲区都必须又一个存放数据的地方。
如果是直接缓冲区,我们是不能获取到数组的,所以array方法不能再直接缓冲区中调用,否则抛出UnsupportedOperationException异常。当然,如果是只读的缓冲区,我们也不能调用array方法或者是arrayOffset方法。
arrayOffset方法是返回作为备份数组的起始下标。这就说明,并非一个数组必须是所有元素都在缓冲区内,而是可以拆分的。但是这里还没讲到,所以,以上提到的缓冲区,如果使用arrayOffset方法,返回都是0.
为了方便,我们的CharBuffer提供了几个方法
public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable {
// This is a partial API listing
public static CharBuffer wrap (CharSequence csq)
public static CharBuffer wrap (CharSequence csq, int start, int end)
}
我们可以这样用。
CharBuffer charBuffer = CharBuffer.wrap ("Hello World");
这对于字符集码和正则表达式处理都是很方便的。
2、复制缓冲区
这里的复制,仅仅是复制缓冲区,而不是数据。
public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable {
// This is a partial API listing
public abstract CharBuffer duplicate( );
public abstract CharBuffer asReadOnlyBuffer( );
public abstract CharBuffer slice( );
}
这三个函数可以完成复制功能。前两个顾名思义,容易理解,最后一个slice,其实也很容易理解,分割缓冲区,这时,起决于position和limit,新的缓冲区将针对原来的数据中的position到limit之间的数据作为新的缓冲区数据。这时,arrayOffset方法返回的就是position的位置了。
例如:要创建一个映射到数组位置12-20(9个元素)的buffer对象:
char[] myBuffer = new char[100];
CharBuffer cb = CharBuffer.wrap(myBuffer);
cb.position(12).limit(21);
CharBuffer sliced = cb.slice();
3、字节缓冲区
虽然我们有很多缓冲区类型,除了boolean型都有一一对应。
但是,ByteBuffer作为一个基本的缓冲区,其作用非同寻常。我们的通道只和字节缓冲区打交道。并且,其他的缓冲区和字节缓冲区都可以转换。
说到这里,我们就不得不理解计算机如何存储数据了。
实际上,内存是按照字节为单位来存放数据的。现在最流行的便是8个位一个字节。我们熟悉的是int占用4个字节,这样,这四个字节如何排列,又成为一个问题。
这个问题已经是硬件问题了,因为不同的硬件有不同的表现。
对于intel处理器,一般都是小端(little endian)
而对于摩托罗拉,Sun的处理器,一般都是大端(big endian)
还有网络字节顺序实际上也是大端的。
那么这两者有什么区别呢?
假设 一个int值 0x01234567 (注意是16进制),内存从左到右增长。地址左小右大
那么他在大端的存放 就类似 : 0x01 0x23 0x45 0x67
那么他在小端的存放,就应该是: 0x67 0x45 0x23 0x01
所以我们的字节操作中有了顺序问题。
package java.nio;
public final class ByteOrder
{
public static final ByteOrder BIG_ENDIAN;
public static final ByteOrder LITTLE_ENDIAN;
public static ByteOrder nativeOrder();
public String toString();
}
在JVM中,实际上我们是使用大端的。
看看关于Order的操作。
public abstract class ByteBuffer extends Buffer
implements Comparable {
// This is a partial API listing
public final ByteOrder order();
public final ByteBuffer order(ByteOrder bo);
}
order返回系统使用的字节顺序。注意如果系统字节顺序和JVM不一致,可能性能上会有些许慢。
现在知道了字节顺序,那么字节缓冲区转换为其他缓冲区,也就清楚了。
比如,我们有一个4字节缓冲区 0x01 0x23 0x45 0x67
转换为一个IntBuffer,那么,根据字节顺序,如果是大端,直接就是 0x1234567
如果是小端顺序,那么读出来就是 0x67452301
注意:视图缓冲区一旦创建,字节顺序是不可改变的。视图缓冲区后面讲到。
4、直接缓冲区
前面说到,allocate和wrap创建的都是间接缓冲区。间接缓冲区,就是在操作系统和JVM之间其实有一层,我们如果对一个间接缓冲区使用,那么首先是要在操作系统层次创建一个临时缓冲区,然后copy过去,再操作,在删除临时缓冲区。这都很麻烦。
所以,直接缓冲区。也就是在操作系统,而不是JVM进程中创建缓冲区,这就绕过了JVM栈,所以依赖于具体的操作系统。事实上,在操作系统中创建缓冲区,比起在JVM中,更加消耗资源。(因为JVM中是已经划分好的内存,直接设置一下,而操作系统中要通过系统调用,个人理解。)
使用 allocateDirect方法就可以获得直接缓冲区。
public static ByteBuffer allocateDirect (int capacity)
public abstract boolean isDirect( );
isDirect对于直接缓冲区的非字节视图缓冲区,也可能返回true
5、视图缓冲区
可以这样理解,字节缓冲区是所有缓冲类型的基础。我们在通道中,网络中文件流中四处传递字节缓冲区。但是,一旦进入应用,我们就要通过视图缓冲区来解读具体的数据了。
字节缓冲区提供了很多这样的API
public abstract class ByteBuffer
extends Buffer implements Comparable {
// This is a partial API listing
public abstract CharBuffer asCharBuffer( );
public abstract ShortBuffer asShortBuffer( );
public abstract IntBuffer asIntBuffer( );
public abstract LongBuffer asLongBuffer( );
public abstract FloatBuffer asFloatBuffer( );
public abstract DoubleBuffer asDoubleBuffer( );
}
前面提到过从ByteBuffer到IntBuffer的转换。其实这种转换,是一种解释包装。
所以需要提供一个顺序,然后按照顺序和类型,转换成另外一种视图,而原始数据其实是不变的。
例如,我们可以这样;
ByteBuffer byteBuffer =
ByteBuffer.allocate(7).order(ByteOrder.BIG_ENDIAN);
CharBuffer charBuffer = byteBuffer.asCharBuffer();
注意,java中char是占两个字节的。我们的转换可以看下图:
[img]http://dl.iteye.com/upload/attachment/0066/6532/8141e964-096f-37a5-8adc-f7623cb2e9da.jpg[/img]
新的charBuffer视图其实还是使用的原来的缓冲区的数据,只是这时的元素变成了2个字节的char了。
看一段代码:
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
public class CharBufferView {
public static void print(Buffer buffer){
System.out.println("pos="+buffer.position()+", limit="+
buffer.limit() + ", capacity="+buffer.capacity()
+":'" + buffer.toString()+"'");
}
public static void main(String[] args)
throws Exception{
ByteBuffer byteBuffer = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
CharBuffer charBuffer = byteBuffer.asCharBuffer();
byteBuffer.put(0,(byte)0);
byteBuffer.put(1,(byte)'H');
byteBuffer.put(2,(byte)0);
byteBuffer.put(3,(byte)'e');
byteBuffer.put(4,(byte)0);
byteBuffer.put(5,(byte)'l');
byteBuffer.put(6,(byte)0);
byteBuffer.put(7,(byte)'l');
byteBuffer.put(8,(byte)0);
byteBuffer.put(9,(byte)'o');
CharBufferView.print(byteBuffer);
CharBufferView.print(charBuffer);
}
}
结果:
[i]pos=0, limit=10, capacity=10:'java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]'
pos=0, limit=5, capacity=5:'Hello'[/i]
可以看到,转换为视图后,limit和capacity都变了。
6、数据元素视图
上面的视图使用了一个视图类来转换。在ByteBuffer中,我们可以直接获取不同视图的元素。
public abstract class ByteBuffer
extends Buffer implements Comparable {
public abstract char getChar( );
public abstract char getChar (int index);
public abstract short getShort( );
public abstract short getShort (int index);
public abstract int getInt( );
public abstract int getInt (int index);
public abstract long getLong( );
public abstract long getLong (int index);
public abstract float getFloat( );
public abstract float getFloat (int index);
public abstract double getDouble( );
public abstract double getDouble (int index);
public abstract ByteBuffer putChar (char value);
public abstract ByteBuffer putChar (int index, char value);
public abstract ByteBuffer putShort (short value);
public abstract ByteBuffer putShort (int index, short value);
public abstract ByteBuffer putInt (int value);
public abstract ByteBuffer putInt (int index, int value);
public abstract ByteBuffer putLong (long value);
public abstract ByteBuffer putLong (int index, long value);
public abstract ByteBuffer putFloat (float value);
public abstract ByteBuffer putFloat (int index, float value);
public abstract ByteBuffer putDouble (double value);
public abstract ByteBuffer putDouble (int index, double value);
}
这些操作一看便知。前面也提过其实就是根据大端小端的字节顺序,和这些数据类型的长度来组织数据。
这里需要注意的是:如果get或者是put时,数据不足,或者空间不够,都会发生异常。
如果不是用putXX的方法,直接getXX,那么可能产生意想不到的问题。
7、无符号数存取
对于java来说,没有无符号数处理(除了char)。为此,书中给出一个程序,用来处理无符号数缓冲区。
其基本原理是,使用比要存取的数据类型更大的数据类型来存放这个数,同时使用与运算强制设置符号位为0,剩余的位就可以作为数据而不是符号位了。
public class Unsigned
{
public static short getUnsignedByte (ByteBuffer bb) {
return ((short)(bb.get( ) & 0xff));
}
public static void putUnsignedByte (ByteBuffer bb, int value) {
bb.put ((byte)(value & 0xff));
}
...
}
这里省略了其他部分。这样我们就可以看到带符号的
8、内存映射缓冲区
映射缓冲区是与文件存储的数据元素关联的字节缓冲区,它通过内存映射来访问。映射缓
冲区通常是直接存取内存的,只能通过 FileChannel 类创建。映射缓冲区的用法和直接缓冲
区类似,但是 MappedByteBuffer 对象具有许多文件存取独有的特征。