在传统的I/O操作中,许多都是将数据直接写入或者将数据直接读到流中,但是在新NIO中,所有数据都是用缓冲区处理的。在之前已经多次接触过缓冲区,而且大多数的类都使用字节数组来做缓冲区。但是一个缓冲区不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
NIO.2的一个新特性就是异步能力。NIO的新功能会涉及到三个重要的类,Channel(通信管道)、Buffer(缓冲)和非阻塞式输出/输出的Selector类,还需要了解Charset类,可以实现字节序列与字符序列的转换。NIO包(java.nio.*)引入了四个关键的抽象数据类型,它们共同解决传统的I/O类中的一些问题。
这一章将主要讲解一下Buffer的实现,新NIO中提供的主要Buffer类及他们之间的相互关系如下:
对于每个非布尔原始数据类型都有一个缓冲区类,即ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和 ShortBuffer。那么为什么不提供BooleanBuffer呢?
尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。
1、Buffer类
在Buffer类中定义了四个非常重要的属性,如下:
// Invariants: mark <= position <= limit <= capacity private int mark = -1; private int position = 0; private int limit; private int capacity;
详细介绍如下:
(1)容量(capacity)
缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
(2)上界(limit)
缓冲区的第一个不能被读或写的元素。缓冲创建时,limit 的值等于 capacity 的值。假设 capacity = 1024,我们在程序中设置了 limit = 512,说明Buffer 的容量为 1024,但是从 512 之后既不能读也不能写,因此可以理解成,Buffer 的实际可用大小为 512。
(3)位置(position)
当写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
(4)标记(mark)
一个备忘位置。标记在设定前是未定义的(undefined)。在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)。
在使用 Buffer 时,我们实际操作的就是这四个属性的值。
这些属性是私有的,在子类中没有再重复定义,而是通过一些方法来操作。在初始化时调用Buffer类中的包权限构造函数,如下:
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; } } public final Buffer position(int newPosition) { // 由于limit限定可读或可写的末尾边界,新的读写位置不可以大于 if ((newPosition > limit) || (newPosition < 0)) throw new IllegalArgumentException(); position = newPosition; // mark<=position才是合法的 if (mark > position) mark = -1; return this; } public final Buffer limit(int newLimit) { // 不允许末尾有效边界大于容量 if ((newLimit > capacity) || (newLimit < 0)) throw new IllegalArgumentException(); limit = newLimit; if (position > limit) position = limit; if (mark > limit) mark = -1; return this; }在子类中都是通过super()来调用如上构造函数的。Buffer类中实现的方法都是针对这4个属性进行操作的,并且加入了final关键字,表示不可以被覆盖。
另外许多方法都返回了Buffer对象,这样就可以进行级联的操作。
效果图如下:
如上需要强制类型转换为byte字符,因为Java默认的为Unicode编码占用两个字节。由于兼容ASCII编码,所以省略前八位后值仍然准确。
// 写模式改变为读模式 public final Buffer flip() { limit = position; position = 0; mark = -1; return this; } // 不设置上界 public final Buffer rewind() { position = 0; mark = -1; return this; }调用flip()方法的结果如下:
Buffer类中还定义了一些抽象方法,这些抽象方法将在后面进行具体的讲解。
2、ByteBuffer类
子类的实现以ByteBuffer为例,其它子类的实现都非常相似,在这里不再一一讲解。
类中最主要的属性和构造方法如下:
final byte[] hb; // Non-null only for heap buffers final int offset; boolean isReadOnly; // Valid only for heap buffers ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) { super(mark, pos, lim, cap); // 调用父类的构造函数来初始化4个重要属性 this.hb = hb; this.offset = offset; }
下面开始介绍一些常用的方法。
1、allocate() 创建缓冲区
要想获得一个Buffer对象首先要指定容量的大小。 每一个Buffer类都有一个allocate()静态工厂方法,源代码如下:
public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity); // 分配一个HeapByteBuffer实例 }
得到一个HeapByteBuffer类的实例,构造函数如下:
HeapByteBuffer(int cap, int lim) { // package-private super(-1, 0, lim, cap, new byte[cap], 0); }也就是调用父类ByteBuffer的构造函数来为各个属性赋值,其中hb字节数组被赋值new byte[cap],而这个数组分配在Java堆上,是由虚拟机来管理的内存区域,还有一个直接分配内存的方法,如下:
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }使用直接字节缓冲区可能要比在JVM堆上分配缓冲区的效率更高一些。
2、get()和put() 存取元素
Buffer 类并没有包括 get() 或 put()方法。但是,每一个Buffer 的子类都有这两个方法,但它们所采用的参数类型,以及它们返回的数据类型,对每个子类来说都是唯一的,所以它们不能在顶层 Buffer 类中被抽象地声明。它们的定义必须被特定类型的子类所遵从。下面以 ByteBuffer 为例讲解get()和put()方法了。
public abstract byte get();
public abstract ByteBuffer put(byte b);
定义了无index的 get ()和 put()方法,称为相对存取,相对存取会自动影响缓冲区的位置属性。
其在HeapByteBuffer中的具体实现如下:
protected int ix(int i) { return i + offset; } public byte get() { return hb[ix(nextGetIndex())]; } public ByteBuffer put(byte x) { hb[ix(nextPutIndex())] = x; return this; }其中的hb就是ByteBuffer中定义的字节数组,offset是定义的偏移量,这里为0,表示不需要偏移。由于NIO偏向于字节存取,所以如果要从一个ByteBuffer中获取整数,则需要offset的值为4.
final int nextGetIndex() { // package-private if (position >= limit) throw new BufferUnderflowException(); return position++; } final int nextPutIndex() { // package-private if (position >= limit) throw new BufferOverflowException(); return position++; }
position 的值会自动前进。如果调用多次导致位置超出上界limit,则会抛出 BufferOverflowException 异常;这种不带索引参数的方法,
public abstract byte get(int index);
public abstract ByteBuffer put(int index, byte b);
带索引参数的方法,称为绝对存取,绝对存储不会影响缓冲区的位置属性。以put实现为例:
public ByteBuffer put(int i, byte x) { hb[ix(checkIndex(i))] = x; return this; } final int checkIndex(int i) { // package-private if ((i < 0) || (i >= limit)) throw new IndexOutOfBoundsException(); return i; }只会检查索引是否合法,并不会改变位置属性的值。
3、compact() 压缩元素
以ByteBuffer为例来看一下HeadByteBuffer类是如何实现这个方法的,源代码如下:
public ByteBuffer compact() { System.arraycopy(hb, ix(position()), hb, ix(0), remaining()); // 对元素进行批量移动 position(remaining()); // 设置下一个要写入的位置 limit(capacity()); // limit重置为capacity discardMark(); // mark标识置为无效 return this; // 返回这个ByteBuffer实例 }
操作属性的方法一般在Buffer中实现,如下:
public final int remaining() { // 有效元素的数量 return limit - position; } final void discardMark() { // mark重置为无效 mark = -1; }
压缩前: 压缩后
4、equals()和compareTo()方法
可以使用equals()和compareTo()方法比较两个Buffer,但是这是有区别的,equals()方法
两个Buffer相等的充要条件是:
equals()只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。
public boolean equals(Object ob) { if (this == ob) return true; if (!(ob instanceof ByteBuffer)) // 类型必须相同 return false; ByteBuffer that = (ByteBuffer)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; } private static boolean equals(byte x, byte y) { return x == y; }属性不同的缓冲区可以相同
属性不同,所表示的有效区域不同,但是数组内容相同,则被认为是不相等的,如下:
compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:
public int compareTo(ByteBuffer that) { int n = this.position() + Math.min(this.remaining(), that.remaining()); for (int i = this.position(), j = that.position(); i < n; i++, j++) { int cmp = compare(this.get(i), that.get(j)); // 通过相对存取,不会改变缓冲区的属性 if (cmp != 0) return cmp; } return this.remaining() - that.remaining(); } private static int compare(byte x, byte y) { return Byte.compare(x, y); }
Byte中的compare()方法源代码如下:
public static int compare(byte x, byte y) { return x - y; }
传送门 : Java 7之基础 - 实现比较 http://blog.csdn.net/mazhimazh/article/details/20038281
3、CharBuffer类
1、创建缓冲区
public static CharBuffer allocate(int capacity) { // 静态工厂方法 if (capacity < 0) throw new IllegalArgumentException(); return new HeapCharBuffer(capacity, capacity); }通过分配来创建一个新的缓冲区,与ByteBuffer的创建过程类似。不过还可以使用自己提供的数组来储存缓冲区中的数据元素,如下:
public static CharBuffer wrap(char[] array, int offset, int length) { try { return new HeapCharBuffer(array, offset, length); } catch (IllegalArgumentException x) { throw new IndexOutOfBoundsException(); } }
与创建的缓冲区相关的方法还有hasArray()和arrayOffset()方法,首先来看hasArray()方法,如下:
public final boolean hasArray() { return (hb != null) && !isReadOnly; }如果缓存区数组不为null且是可读写的,则返回值为true,这样就可以调用arrayOffset()方法了,如下:
public final int arrayOffset() { if (hb == null) throw new UnsupportedOperationException(); if (isReadOnly) throw new ReadOnlyBufferException(); return offset; }返回offset值,表示缓存区数据在数组中存储的开始位置的偏移量。
char[] myArray=new char[100]; // 自定义一个数组做为存储区 // positon=10,limit=20,capacity=myArray.length=100 CharBuffer cb=CharBuffer.wrap(myArray,10,20); if(cb.hasArray()){// true,表示是缓冲区有可存取的备份元素 System.out.println(cb.arrayOffset()); // 默认是数组的0位置 }
参考主要文献:
Java NIO Ron HitChens 著 斐小星 译