一、什么是缓冲区
-
Java的NIO中Buffer至关重要:buffer是读写的中介,主要和NIO的通道交互。数据是通过通道读入缓冲区和从缓冲区写入通道的。
-
缓冲区buffer的本质就是一块可以读写的内存块(buffer类内部是一个基本数据类型的数组)。这块内存块被包装成NIO的Buffer对象,并提供了一组方法方便读写。
-
对于每个非布尔原始数据类型都有一个缓冲区类。
二、Buffer的家族
Buffer是顶层抽象类,另外还有八个基本数据抽象类。
MappedByteBuffer是ByteBuffer的一个具体实现类。
三、Buffer的属性
3.1、容量capacity
capacity是缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
3.2、上界limit
缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
3.3、位置position
下一个要被读或写的元素的索引。位置会自动由相应的get( )和put( )函数更新。
3.4、标志mark
一个备忘位置。调用mark( )来设定mark = postion。调用reset( )设定position = mark。标记在设定前是未定义的(undefined)。
四、Buffer API
4.1、创建一个容量大小为10的字符缓冲区
4.2、存取put() get()
put()和get()方法并没有在Buffer API中,而是在子类中定义的。这是因为每一个Buffer类所采用的参数类型,以及它们返回的数据类型,对每个子类来说都是唯一的,所以它们不能在顶层Buffer类中被抽象地声明。
看看ByteBuffer类的put() get()方法:
public abstract class ByteBuffer extends Buffer implements Comparable{
// This is a partial API listing
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可以是相对的或者是绝对的,相对方案是不带有索引参数的函数。当相对函数被调用时,位置在返回时前进一。如果位置前进过多,相对运算就会抛出异常。对于put(),如果运算会导致位置超出上界,就会抛出BufferOverflowException异常。对于get(),如果位置不小于上界,就会抛出BufferUnderflowException异常。
4.3、填充
往缓冲区中put()五个字节:
bf.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'0');
ps:在java中,字符在内部以Unicode码表示,每个Unicode字符占16位。这里使用包含ascii字符集数值的字节。通过将char强制转换为byte,我们删除了前八位来建立一个八位字节值。这通常只适合于拉丁字符而不能适合所有可能的Unicode字符。
put()的绝对方案可以在不丢失位置的情况下进行一些更改:
bf.put(0,(byte)'M').put((byte)'w');
put(0,(byte)'M')将H换位M,且不改变position位置,依旧是5,put((byte)'w')才增加为6。
4.4、翻转flip()
如果把已经写满了缓冲区直接传递给一个通道,然后通道在缓冲区上执行get(),那么它将从刚刚插入的有用数据之外取出未定义数据。这时需要将位置值重新设为0,通道就会从正确位置开始获取。
但是它是怎样知道何时到达我们所插入数据末端的呢?这就是上界属性被引入的目的。
上界属性指明了缓冲区有效内容的末端。将上界属性设置为当前位置,然后将位置重置为0。
bf.limit(buffer.position()).position(0);
Buffer.flip()就是这个原理。
翻转后的缓冲区:
4.5、释放get()
布尔函数hasRemaining()会在释放缓冲区时告诉您是否已经达到缓冲区的上界。
以下是一种将数据元素从翻转后的缓冲区释放到一个数组的方法:
for (int i = 0; bf.hasRemaining( ), i++){
myByteArray [i] = buffer.get( );
}
remaining()函数将告知您从当前位置到上界还剩余的元素数目。
也可以通过下面的循环来释放翻转后的缓冲区:
int count = bf.remaining( );
for (int i = 0; i < count, i++) {
myByteArray [i] = buffer.get( );
}
一旦缓冲区对象完成填充并释放,它就可以被重新使用了。Clear()函数将缓冲区重置为空状态。它并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设回0。
/**
* 填充和释放缓冲区
* <p>
* Created by w1992wishes on 2017/6/13.
*/
public class BufferFillDrain {
private static int index = 0;
private static String[] strings =
{
"A random string value",
"The product of an infinite number of monkeys",
"Hey hey we're the Monkees",
"Opening act for the Monkees: Jimi Hendrix",
"'Scuse me while I kiss this fly", // Sorry Jimi ;-)
"Help Me! Help Me!",
};
public static void main(String[] args) {
CharBuffer buffer = CharBuffer.allocate(100);
while (fillBuffer(buffer)){
//缓冲区释放前先翻转
buffer.flip();
//翻转后再释放
drainBuffer(buffer);
//释放后应重置指针
buffer.clear();
}
}
//释放
private static void drainBuffer(CharBuffer buffer) {
while (buffer.hasRemaining())
System.out.print(buffer.get());
System.out.println("");
}
//填充
private static boolean fillBuffer(CharBuffer buffer) {
if (index >= strings.length)
return false;
String str = strings[index++];
Arrays.asList(str.toCharArray())
.stream()
.forEach( c -> buffer.put(c) );
return true;
}
}
4.6、压缩compact()
public abstract class ByteBuffer extends Buffer implements Comparable {
// This is a partial API listing
public abstract ByteBuffer compact( );
}
如果只想从缓冲区中释放一部分数据,而不是全部,然后重新填充。为了实现这一点,未读的数据元素需要下移以使第一个元素索引为0。尽管重复这样做会效率低下,但这有时非常必要,而API提供了一个compact()函数。这一缓冲区工具在复制数据时要比使用get()和put()函数高效得多。
bf.compact();
这里发生了几件事。
数据元素2-5被复制到0-3位置。
位置4和5不受影响,但现在正在或已经超出了当前位置,因此是“死的”。它们可以被之后的put()调用重写。
还要注意的是,位置已经被设为被复制的数据元素的数目。也就是说,缓冲区现在被定位在缓冲区中最后一个“存活”元素后插入数据的位置。
最后,上界属性被设置为容量的值,因此缓冲区可以被再次填满。调用compact()的作用是丢弃已经释放的数据,保留未释放的数据,并使缓冲区对重新填充容量准备就绪。
4.7、标记mark
标记,使缓冲区能够记住一个位置并在之后将其返回。
缓冲区的标记在mark( )函数被调用之前是未定义的,调用时标记被设为当前位置的值。
reset( )函数将位置设为当前的标记值。如果标记值未定义,调用reset( )将导致InvalidMarkException异常。
bf.position(2).mark().position(4);
bf.reset()
4.8、比较equlas()和compareTo()
所有的缓冲区都提供了一个常规的equals( )函数用以测试两个缓冲区的是否相等,以及一个compareTo( )函数用以比较缓冲区。
public abstract class ByteBuffer extends Buffer implements Comparable {
// This is a partial API listing
public boolean equals (Object ob)
public int compareTo (Object ob)
}
两个缓冲区可用下面的代码来测试是否相等:
if (buffer1.equals (buffer2)) { doSomething( ); }
如果每个缓冲区中剩余的内容相同,那么equals( )函数将返回true,否则返回false。
两个缓冲区被认为相等的充要条件是:
-
两个对象类型相同。包含不同数据类型的buffer永远不会相等,而且buffer绝不会等于非buffer对象。
-
两个对象都剩余同样数量的元素。Buffer的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相同。
-
在每个缓冲区中应被Get()函数返回的剩余数据元素序列必须一致。
两个相等的缓冲区
两个不相等的缓存区
缓冲区也支持用compareTo( )函数以词典顺序进行比较。这一函数在缓冲区参数小于,等于,或者大于引用compareTo( )的对象实例时,分别返回一个负整数,0和正整数。
这些就是所有典型的缓冲区所实现的java.lang.Comparable接口语义。
这意味着缓冲区数组可以通过调用java.util.Arrays.sort()函数按照它们的内容进行排序。
比较是针对每个缓冲区内剩余数据进行的,与它们在equals( )中的方式相同,直到不相等的元素被发现或者到达缓冲区的上界。如果一个缓冲区在不相等元素发现前已经被耗尽,较短的缓冲区被认为是小于较长的缓冲区。
4.9、批量移动
public abstract class CharBuffer extends Buffer implements CharSequence, Comparable {
// This is a partial API listing
public CharBuffer get (char [] dst)
public CharBuffer get (char [] dst, int offset, int length)
public final CharBuffer put (char[] src)
public CharBuffer put (char [] src, int offset, int length)
public CharBuffer put (CharBuffer src)
public final CharBuffer put (String src)
public CharBuffer put (String src, int start, int end)
}
有两种形式的get( )可供从缓冲区到数组进行的数据复制使用。
第一种形式只将一个数组作为参数,将一个缓冲区释放到给定的数组。
第二种形式使用offset和length参数来指定目标数组的子区间。
如果要求的数量的数据不能被传送,那么不会有数据被传递,缓冲区的状态保持不变,同时抛出BufferUnderflowException异常。因此当传入一个数组并且没有指定长度,就相当于要求整个数组被填充。如果缓冲区中的数据不够完全填满数组,会得到一个异常。这意味着如果想将一个小型缓冲区传入一个大型数组,需要明确地指定缓冲区中剩余的数据长度。
char [] bigArray = new char [1000];
// Get count of chars remaining in the buffer
int length = buffer.remaining( );
// Buffer is known to contain < 1,000 chars
buffer.get (bigArrray, 0, length);
Put()的批量版本工作方式相似,但以相反的方向移动数据,从数组移动到缓冲区。
如果缓冲区有足够的空间接受数组中的数据(buffer.remaining()>myArray.length),数据将会被复制到从当前位置开始的缓冲区,并且缓冲区位置会被提前所增加数据元素的数量。如果缓冲区中没有足够的空间,那么不会有数据被传递,同时抛出一个BufferOverflowException异常。