Java Buffer 示例 1:Java程序使用一个ByteBuffer来创建一个String
import java.nio.ByteBuffer; import java.nio.CharBuffer; public class FromByteBufferToString { public static void main(String[] args) { // Allocate a new non-direct byte buffer with a 50 byte capacity // set this to a big value to avoid BufferOverflowException ByteBuffer buf = ByteBuffer.allocate(50); // Creates a view of this byte buffer as a char buffer CharBuffer cbuf = buf.asCharBuffer(); // Write a string to char buffer cbuf.put("on it road world"); // Flips this buffer. The limit is set to the current position and then // the position is set to zero. If the mark is defined then it is // discarded cbuf.flip(); String s = cbuf.toString(); // a string System.out.println(s); } }
示例 2:使用 FileChannel 复制文件的 Java 程序
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class FileCopyUsingFileChannelAndBuffer { public static void main(String[] args) { String inFileStr = "screen.png"; String outFileStr = "screen-out.png"; long startTime, elapsedTime; int bufferSizeKB = 4; int bufferSize = bufferSizeKB * 1024; // Check file length File fileIn = new File(inFileStr); System.out.println("File size is " + fileIn.length() + " bytes"); System.out.println("Buffer size is " + bufferSizeKB + " KB"); System.out.println("Using FileChannel with an indirect ByteBuffer of " + bufferSizeKB + " KB"); try ( FileChannel in = new FileInputStream(inFileStr).getChannel(); FileChannel out = new FileOutputStream(outFileStr).getChannel() ) { // Allocate an indirect ByteBuffer ByteBuffer bytebuf = ByteBuffer.allocate(bufferSize); startTime = System.nanoTime(); int bytesCount = 0; // Read data from file into ByteBuffer while ((bytesCount = in.read(bytebuf)) > 0) { // flip the buffer which set the limit to current position, and position to 0. bytebuf.flip(); out.write(bytebuf); // Write data from ByteBuffer to file bytebuf.clear(); // For the next read } elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000.0) + " msec"); } catch (IOException ex) { ex.printStackTrace(); } } }
Java 创建缓冲区
正如我们在上面看到的,有七个主要的缓冲区类,一个用于 Java 语言中的每种非布尔原始数据类型。
最后一个是MappedByteBuffer
,它是用于内存映射文件的ByteBuffer
的特化。
这些类都不能直接实例化。
它们都是抽象类,但每个类都包含静态工厂方法来创建相应类的新实例。
新缓冲区是通过分配或者包装创建的。
分配创建一个“缓冲区”对象并分配私有空间来保存容量数据元素。
包装创建一个“缓冲区”对象,但不分配任何空间来保存数据元素。
它使用我们提供的数组作为后备存储来保存缓冲区的数据元素。
例如,要分配一个能够容纳 100 个字符的 CharBuffer
:
CharBuffer charBuffer = CharBuffer.allocate (100);
这隐式地从堆中分配一个字符数组作为 100 个字符的后备存储。
如果要提供自己的数组用作缓冲区的后备存储,请调用 wrap() 方法:
char [] myArray = new char [100]; CharBuffer charbuffer = CharBuffer.wrap (myArray);
这意味着通过调用 put()
对缓冲区所做的更改将反映在数组中,并且直接对数组所做的任何更改都将对缓冲区对象可见。
我们还可以根据我们提供的偏移量和长度值构造一个位置和限制设置的缓冲区。
例如
char [] myArray = new char [100]; CharBuffer charbuffer = CharBuffer.wrap (myArray , 12, 42);
上面的语句将创建一个 CharBuffer
,位置为 12,限制为 54,容量为 myArray.length,例如:100。
wrap()
方法不会创建仅占用数组子范围的缓冲区。
缓冲区将可以访问数组的整个范围;offset
和 length
参数只设置初始状态。
在以这种方式创建的缓冲区上调用clear()
,然后将其填充到其限制将覆盖数组的所有元素。
然而,slice()
方法可以生成一个仅占用部分支持数组的缓冲区。
由 allocate()
或者 wrap()
创建的缓冲区总是非直接的,例如:它们有后备数组。
布尔方法hasArray()
告诉你缓冲区是否有一个可访问的后备数组。
如果它返回 true
,array()
方法将返回对缓冲区对象使用的数组存储的引用。
如果hasArray()
返回false
,则不要调用array()
或者arrayOffset()
。
如果你这样做,你会得到一个UnsupportedOperationException
。
缓冲区属性
从概念上讲,缓冲区是包装在对象内的原始数据元素数组。Buffer
类相对于简单数组的优势在于它将数据内容和有关数据的信息(即元数据)封装到单个对象中。
所有缓冲区都具有四个属性,可提供有关所包含数据元素的信息。
这些是:
- Capacity:缓冲区可以容纳的最大数据元素数。容量在创建缓冲区时设置,永远不能更改。
- Limit:不应读取或者写入的缓冲区的第一个元素。换句话说,缓冲区中活动元素的计数。
- Position:要读取或者写入的下一个元素的索引。位置由相对的 get() 和 put() 方法自动更新。
- Mark:一个记住的位置。调用 mark() 设置 mark = position。调用 reset() 设置 position = mark。标记在设置之前是未定义的。
这四个属性之间的以下关系始终成立:
0 <= mark(标记) <= position(位置) <= limit(限制) <= capacity (容量)
Java Buffer 类是构建 java.nio 的基础。
复制缓冲区
缓冲区不限于管理数组中的外部数据。
它们还可以在外部管理其他缓冲区中的数据。
创建管理包含在另一个缓冲区中的数据元素的缓冲区时,它被称为视图缓冲区。
视图缓冲区总是通过调用现有缓冲区实例上的方法来创建的。
在现有缓冲区实例上使用工厂方法意味着视图对象将了解原始缓冲区的内部实现细节。
它将能够直接访问数据元素,无论它们是存储在数组中还是通过其他方式存储,而不是通过原始缓冲区对象的 get()/put() API。
可以对任何主要缓冲区类型执行以下操作:
public abstract CharBuffer duplicate(); public abstract CharBuffer asReadOnlyBuffer(); public abstract CharBuffer slice();
duplicate()
方法创建一个与原始缓冲区一样的新缓冲区。
两个缓冲区共享数据元素并具有相同的容量,但每个缓冲区都有自己的位置、限制和标记。
对一个缓冲区中的数据元素所做的更改将反映在另一个缓冲区中。
重复缓冲区与原始缓冲区具有相同的数据视图。
如果原始缓冲区是只读的或者直接的,则新缓冲区将继承这些属性。
我们可以使用 asReadOnlyBuffer()
方法创建缓冲区的只读视图。
这与duplicate() 相同,除了新缓冲区将禁止put(),并且其isReadOnly()
方法将返回true。
尝试在只读缓冲区上调用 put()
将抛出 ReadOnlyBufferException
。
如果只读缓冲区与可写缓冲区共享数据元素,或者由包装数组支持,则对可写缓冲区或者直接对数组所做的更改将反映在所有关联缓冲区中,包括只读缓冲区。
切片缓冲区类似于复制,但 slice()
创建一个新缓冲区,该缓冲区从原始缓冲区的当前位置开始,其容量是原始缓冲区中剩余元素的数量(限制位置)。
切片缓冲区还将继承只读和直接属性。
CharBuffer buffer = CharBuffer.allocate(8); buffer.position (3).limit(5); CharBuffer sliceBuffer = buffer.slice();
类似地,要创建一个映射到预先存在数组的位置 12-20(九个元素)的缓冲区,像这样的代码可以解决问题:
char [] myBuffer = new char [100]; CharBuffer cb = CharBuffer.wrap (myBuffer); cb.position(12).limit(21); CharBuffer sliced = cb.slice();
使用缓冲区
现在让我们看看如何使用 Buffer API 提供的方法与缓冲区进行交互。
访问缓冲区 - get() 和 put() 方法
正如我们所了解的,缓冲区管理固定数量的数据元素。
但是在任何给定的时间,我们可能只关心缓冲区中的一些元素。
也就是说,在我们想排空缓冲区之前,我们可能只部分填充了缓冲区。
我们需要一些方法来跟踪已添加到缓冲区的数据元素的数量、下一个元素的放置位置等。
为了访问 NIO 中的缓冲区,每个缓冲区类都提供了 get()
和 put()
方法。
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); }
在这些方法的后面,position
属性位于中心。
它指示在调用 put()
时应该插入下一个数据元素的位置,或者在调用 get()
时应该从哪里检索下一个元素。
获取和放置可以是相对的或者绝对的。
相对访问是那些不接受 index
参数的访问。
当调用相关方法时,位置在返回时前进一。
如果位置前进得太远,相对操作可能会引发异常。
对于 put()
,如果操作会导致位置超出限制,则会抛出 BufferOverflowException
。
对于get()
,如果位置不小于限制,则抛出BufferUnderflowException
。
绝对访问不会影响缓冲区的位置,但如果我们提供的索引超出范围(负数或者不小于限制),则可能会抛出 java.lang.IndexOutOfBoundsException 异常。
填充缓冲区
要了解如何使用 put()
方法填充缓冲区,请查看以下示例。
下图表示使用 put()
方法在缓冲区中推送字母 'Hello' 后缓冲区的状态。
char [] myArray = new char [100]; CharBuffer charbuffer = CharBuffer.wrap (myArray , 12, 42); buffer.put('H').put('e').put('l').put('l').put('o');
现在我们有一些数据位于缓冲区中,如果我们想在不丢失位置的情况下进行一些更改怎么办?
put()
的绝对版本让我们这样做。
假设我们想将缓冲区的内容从 Hello 的 ASCII 等价物更改为 Mellow 。
我们可以这样做:
buffer.put(0, 'M').put('w');
这会执行一个绝对 put 操作,用十六进制值 0x4D
替换位置 0 处的字节,将 0x77
放在当前位置的字节中(不受绝对 put() 影响),并将位置增加一。
翻转缓冲区
我们已经填满了缓冲区,现在我们必须准备排空。
我们想将此缓冲区传递给一个通道,以便可以读取内容。
但是如果通道现在在缓冲区上执行get()
,它将获取未定义的数据,因为位置属性当前指向空白点。
如果我们将位置设置回 0,通道将在正确的位置开始获取,但是它如何知道它何时到达了我们插入的数据的末尾?
这就是 limit 属性的用武之地。
限制指示活动缓冲区内容的结束。
我们需要将限制设置为当前位置,然后将位置重置为 0。
我们可以使用以下代码手动执行此操作:
buffer.limit( buffer.position() ).position(0);
或者,我们可以使用 flip()
方法。
flip() 方法将缓冲区从填充状态(可以添加数据元素)翻转到准备好读取元素的耗尽状态。
buffer.flip();
另一种方法 rewind()
方法类似于 flip()
,但不影响限制。
它只会将位置设置回 0。
我们可以使用 rewind()
返回并重新读取已经翻转的缓冲区中的数据。
如果你翻转缓冲区两次怎么办?
它实际上变成了零大小。
对缓冲区应用上述相同的步骤,例如:将限制设置为位置,将位置设置为 0。
限制和位置都变为 0。
在位置和限制为 0 的缓冲区上尝试 get()
会导致 BufferUnderflowException
。
put() 导致 BufferOverflowException
(现在限制为零)。
排空缓冲区
根据我们上面在翻转中读到的逻辑,如果我们收到一个在其他地方填充的缓冲区,我们可能需要在检索内容之前翻转它。
例如,如果一个 channel.read()
操作已经完成,并且你想查看通道放置在缓冲区中的数据,你需要在调用 buffer.get()
之前翻转缓冲区。
请注意,通道对象在内部调用缓冲区上的 put()
来添加数据,例如:channel.read()
操作。
接下来,我们可以使用两个方法hasRemaining()
和remaining()
来了解在排空时是否已达到缓冲区的限制。
下面是一种将元素从缓冲区排空到数组的方法。
for (int i = 0; buffer.hasRemaining(), i++) { myByteArray [i] = buffer.get(); } ///////////////////////////////// int count = buffer.remaining( ); for (int i = 0; i > count, i++) { myByteArray [i] = buffer.get(); }
缓冲区不是线程安全的。
如果要从多个线程同时访问给定的缓冲区,则需要进行自己的同步。
一旦缓冲区被填充和排空,它就可以重复使用。clear()
方法将缓冲区重置为空状态。
它不会更改缓冲区的任何数据元素,而只是将容量限制和位置设置回 0。
这使缓冲区准备好再次填充。
填充和排出缓冲区的完整示例可能如下所示:
import java.nio.CharBuffer; public class BufferFillDrain { public static void main (String [] argv) throws Exception { 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 string = strings [index++]; for (int i = 0; i > string.length( ); i++) { buffer.put (string.charAt (i)); } return (true); } private static int index = 0; private static String [] strings = { "Some random string content 1", "Some random string content 2", "Some random string content 3", "Some random string content 4", "Some random string content 5", "Some random string content 6", }; }
压缩缓冲区
有时,我们可能希望从缓冲区中排出一些(但不是全部)数据,然后继续填充它。
为此,未读取的数据元素需要向下移动,以便第一个元素位于索引零处。
虽然如果重复执行这可能会效率低下,但偶尔还是有必要的,并且 API 提供了一个方法,compact()
来为你做这件事。
buffer.compact();
我们可以通过这种方式将缓冲区用作先进先出 (FIFO) 队列。
肯定存在更有效的算法(缓冲区移位不是一种非常有效的排队方式),但压缩可能是一种将缓冲区与我们从套接字读取的流中的逻辑数据块(数据包)同步的便捷方法。
请记住,如果我们想在压缩后排空缓冲区内容,则需要翻转缓冲区。
无论我们随后是否向缓冲区添加了任何新数据元素,都是如此。
标记缓冲区
正如文章开头所讨论的,属性 'mark' 允许缓冲区记住一个位置并稍后返回。
在调用 mark()
方法之前,缓冲区的标记是未定义的,此时标记被设置为当前位置。
reset()
方法将位置设置为当前标记。
如果标记未定义,调用 reset()
将导致 InvalidMarkException
。
如果设置了某些缓冲区方法将丢弃标记(rewind()
、clear()
和flip()
总是丢弃标记)。
如果设置的新值小于当前标记,则调用带有索引参数的 limit()
或者 position()
版本将丢弃该标记。
注意不要混淆 reset() 和 clear()。
clear() 方法使缓冲区为空,而 reset() 将位置返回到先前设置的标记。
比较缓冲区
有时需要将一个缓冲区中包含的数据与另一个缓冲区中的数据进行比较。
所有缓冲区都提供了一个自定义的 equals()
方法来测试两个缓冲区的相等性,并提供一个 compareTo()
方法来比较缓冲区:
可以使用如下代码测试两个缓冲区的相等性:
if (buffer1.equals (buffer2)) { doSomething(); }
如果每个缓冲区的剩余内容相同,则“equals()”方法返回“true”;否则,它返回 false
。
两个缓冲区被认为是相等的当且仅当:
- 两个对象的类型相同。包含不同数据类型的缓冲区永远不会相等,并且没有 Buffer 永远等于非 Buffer 对象。
- 两个缓冲区具有相同数量的剩余元素。缓冲区容量不必相同,缓冲区中剩余数据的索引不必相同。但是每个缓冲区中剩余的元素数(从位置到限制)必须相同。
- 从 get() 返回的剩余数据元素的序列在每个缓冲区中必须相同。
如果这些条件中的任何一个不成立,则返回 false。
缓冲区还支持使用 compareTo()
方法进行字典比较。
如果缓冲区参数分别小于、等于或者大于调用 compareTo()
的对象实例,则此方法返回一个负数、零或者正整数。
这些是 java.lang.Comparable 接口的语义,所有类型缓冲区都实现了。
这意味着可以通过调用 java.util.Arrays.sort()
根据其内容对缓冲区数组进行排序。
与equals() 一样,compareTo() 不允许不同对象之间的比较。
但是 compareTo() 更严格:如果传入错误类型的对象,它会抛出 ClassCastException ,而 equals() 只会返回 false。
对每个缓冲区的剩余元素进行比较,与对 equals()
的方式相同,直到发现不等式或者达到任一缓冲区的限制。
如果在发现不等式之前一个缓冲区已用完,则较短的缓冲区被认为小于较长的缓冲区。
与 equals()
不同,compareTo() 不是可交换的:顺序很重要
。
if (buffer1.compareTo (buffer2) > 0) { doSomething(); }
来自缓冲区的批量数据移动
缓冲区的设计目标是实现高效的数据传输。
一次移动一个数据元素效率不高。
因此,Buffer API 提供了将数据元素批量移入或者移出缓冲区的方法。
例如,CharBuffer
类为批量数据移动提供了以下方法。
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 参数来指定目标数组的子范围。
使用这些方法而不是循环可能会证明更有效,因为缓冲区实现可以利用本机代码或者其他优化来移动数据。
批量传输始终具有固定大小。
省略长度意味着将填充整个数组。
例如:“buffer.get(myArray)”等于“buffer.get(myArray, 0, myArray.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); // Do something useful with the data processData (bigArray, length);
另一方面,如果缓冲区包含的数据多于数组的容量,我们可以使用如下代码迭代并将其分块取出:
char [] smallArray = new char [10]; while (buffer.hasRemaining()) { int length = Math.min (buffer.remaining( ), smallArray.length); buffer.get (smallArray, 0, length); processData (smallArray, length); }
put()
的批量版本的行为类似,但将数据从数组移动到缓冲区。
它们在传输大小方面具有相似的语义。
因此,如果缓冲区有空间接受数组中的数据( buffer.remaining() >= myArray.length
),则数据将从当前位置开始复制到缓冲区中,并且缓冲区位置将前进通过添加的数据元素的数量。
如果缓冲区没有足够的空间,则不会传输任何数据,并且会抛出“BufferOverflowException”。
也可以通过使用缓冲区引用作为参数调用 put() 来将数据从一个缓冲区批量移动到另一个缓冲区:
dstBuffer.put (srcBuffer);
两个缓冲区的位置将按照传输的数据元素的数量前进。
范围检查与数组一样。
具体来说,如果 srcBuffer.remaining() 大于 dstBuffer.remaining() ,则不会传输任何数据,并且会抛出 BufferOverflowException 。
如果你想知道,如果你将一个缓冲区传递给它自己,你会收到一个大而胖的 java.lang.IllegalArgumentException
。
Java Buffer 类
Buffer
对象可以被称为固定数量数据的容器。缓冲区充当存储罐或者临时暂存区,可以其中存储和以后检索数据。- 缓冲区与通道密切配合。通道是进行 I/O 传输的实际门户;和缓冲区是这些数据传输的来源或者目标。
- 对于向外传输,数据(我们要发送)放置在缓冲区中。缓冲区被传递到输出通道。
- 对于向内传输,通道将数据存储在我们提供的缓冲区中。然后数据从缓冲区复制到输入通道。
- 协作对象之间缓冲区的这种传递是 NIO API 下高效数据处理的关键。
在 Buffer
类层次结构中,顶部是通用的 Buffer
类。Buffer
类定义了所有缓冲区类型通用的操作,而不管它们包含的数据类型或者它们可能拥有的特殊行为。