到目前为止,我们只是使用缓冲区将数据从一个通道转移到另一个通道。然而,程序经常需要直接处理数据。例如,您可能需要将用户数据保存到磁盘。在这种情况下,您必须将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。 或者,您可能想要从磁盘读取用户数据。在这种情况下,您要将数据从通道读到缓冲区中,然后检查缓冲区中的数据。
实际上,每一个基本类型的缓冲区都为我们提供了直接访问缓冲区中数据的方法,我们以ByteBuffer为例,分析如何使用其提供的get()和put()方法直接访问缓冲区中的数据。
The get () methods:
ByteBuffer类中有四个get()方法:
- byte get();
- ByteBuffer get( byte dst[] );
- ByteBuffer get( byte dst[], int offset, int length );
- byte get( int index );
第一个方法获取单个字节。第二和第三个方法将一组字节读到一个数组中。第四个方法从缓冲区中的特定位置获取字节。那些返回ByteBuffer的方法只是返回调用它们的缓冲区的this值。
此外,我们认为前三个get()方法是相对的,而最后一个方法是绝对的。“相对”意味着get()操作服从limit和position值,更明确地说,字节是从当前position读取的,而position在get之后会增加。另一方面,一个“绝对”方法会忽略limit和position值,也不会影响它们。事实上,它完全绕过了缓冲区的统计方法。
上面展示出的ByteBuffer中的get方法,在其他的buffer类中均有类似的方法,只不过处理的数据类型稍有差异而已。
The put( ) methods:
在ByteBuffer类中,共有5种put()方法:
- ByteBuffer put ( byte b );
- ByteBuffer put ( byte src[] );
- ByteBuffer put ( byte src[] , int offset, int length );
- ByteBuffer put ( ByteBuffer src );
- ByteBuffer put ( int index , byte b );
第一个方法放入了单个字节,第二个和第三个方法从数组中放入一组数据,第四个方法从一个源ByteBuffer中拷贝了一份数据到此ByteBuffer中,第五个方法在指定的位置放入了一个字节,那些返回ByteBuffer的方法只是简单的返回了此次调用的this对象。
和get方法一样,put方法也有类似的相对或者绝对的特征,前四个方法都是相对的,而第五个方法则是绝对的。
上面展示的是ByteBuffer类中的put方法,其他buffer类中也有类似的put方法,只不过处理的类型稍有不同而已。
有类型的get和put方法
除了前面展示的put和get方法之外,ByteBuffer还有一些额外的含有类型的方法,如下:
- getByte() ;
- getChar() ;
- getShort() ;
- getInt() ;
- getLong() ;
- getFloat() ;
- getDouble() ;
- putByte() ;
- putChar() ;
- putShort() ;
- putInt() ;
- putLong();
- putFloat() ;
- putDouble() ;
上面的每种方法,实际上都有两种变形,一种相对的一种绝对的,他们在读取有格式的二进制文件是非常有用,比如读入图像文件的头部的时候。
The Buffer at work An inner loop
下面的内存循环简要的描述了从一个input channel读入数据到一个output channel的过程。
while( true ){
buffer.clear();
int r = fcin.read( buffer );
if( -1 == r ){
break;
}
buffer.flip();
fout.write( buffer );
}
read和write方法看起来是如此的简单,因为buffer内部屏蔽了许多细节。clear和flip方法在读和写转换的时候调用。
More about buffers
到现在为止,你已经学会了大多数关于buffer的基础知识,我们的例子详细讲述了标准读写的过程,你应该可以像使用原IO包中那么简单的使用NIO了。
在这一章中我们将接触buffer的一些复杂的特性,比如allocation,wrapping和slicing等等。我们也将在java平台上学习NIO的一些新特性。你讲学到如何创建满足不同目的的buffer,比如保护数据不被修改的只读的buffer,映射底层操作系统buffer的directly buffer,我们将以创建一个memory-mapped file来结束这一节。
Buffer allocation 和 wrapping
在你读写数据之前,你必须创建一个buffer,并且为之分配空间,我们可以采用静态方法allocation()来做到。
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
此allocate方法在底层分配了一个指定大小的字节数组并包装起来作为ByteBuffer返回。
当然,你也可以将一个已经存在的数组包装成一个buffer,如下所示:
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );
这种情况下你必须对有类型的操作格外小心,一旦你这样做了,就破坏了buffer的包装性。
Buffer slicing
slice()方法将一个buffer分割成了许多子buffer,换句话说,他创建了一个新buffer和原buffer分享部分数据。最好的解释方法就是用实例来说明,我们首先创建一个10字节大小的ByteBuffer。
ByteBuffer buffer = ByteBuffer.alocate( 1024 );
然后往里填入数据,将n放在位置n上。
for ( int i = 0 ; i < buffer.capacity() ; ++i ){
buffer.put( (byte) i );
}
现在我们从位置3到位置6创建一个子buffer,这就像在原buffer上开了一个窗户。首先要指定起始位置和结束位置,通过设置position和limit的值来实现,然后调用buffer的slice方法:
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
slice是buffer的一个子集,但是,slice和buffer在底层的数据是共享的,这一点我们在下一节马上就要看到。
Buffer slice 和 data sharing
我们刚刚创建了原buffer的一个子集,并且知道他们的数据是共享的,我们来看看这意味着什么。我们将slice中的每一个元素都乘以11,for example,5将变成55.
for ( int i = 0 ; i < slice.capacity(); ++i ){
byte b = slice.get( i );
b *= 11;
slice.put( i,b );
}
最后,我们观察一下原buffer中的内容。
buffer.position( 0 );
buffer.limit( buffer.capacity() );
while( buffer.remaining() > 0 ){
System.out.println( buffer.get() );
}
结果发现slice中的元素发生了变化:
0
1
2
33
44
55
66
7
8
9
slice是非常好的提取buffer的方案,你可以编写一个函数来处理整个buffer,但是如果你希望处理一部分buffer,推荐使用slice,它将比使用这个buffer来的更方便更简单。
Read-only buffers
只读的buffer非常简单----你只能从中读取数据,而不能写入。你可以通过调用asReadOblyBuffer()方法来转换buffer的形态,它将创建一个新的buffer(数据是共享的),但却是只读的。
只读型的buffer对保护数据非常有用,当你向一个方法传递数据时,你无法知道方法内部对数据是否进行了修改,但创建一个只读型的buffer保证了数据不被修改。
但是你却不能讲一个只读型的buffer转换成一个可写的buffer。
Direct 和inDirect buffers
另外一个有用的ByteBuffer就是direct buffer。一个direct buffer就是一块被用指定的方式分配的内存,用来提高IO的速度。实际上,direct buffer的准确定义是依赖于实现的,sun公司这样描述direct buffers:
对于一个给定的direct buffer,java虚拟机将尽可能的通过native io 来操作。这就是说,他会使用底层操作系统的IO操作来尽可能的去避免拷贝buffer中的内容到中间缓冲器中。
在FastCopyFile.java这个例子中,我们可以看到direct buffer的运用。
当然,你也可以使用内存映射文件来创建direct buffer。
Memory-mapped file IO
内存映射文件IO是一个读写文件数据的方法,当然了,他比原始流或者channel方式要快的多。
内存映射文件IO是通过将文件内存映射到一个内存数组中来实现的,起初,这听起来意味着要将所有的文件读入内存,但实际上并不是这样。一般来说,只是将你实际要读取或者写入的部分文件内容映射到了文件中。
内存映射技术听起来很神奇,但却不是真的魔术。现在的操作系统实现文件系统通常也是将部分文件内容映射到内存中实现的,而java的内存映射技术只是简单的提供了一种使用底层操作系统这种功能的接口。
虽然创建、编写一个内存映射文件是分厂容易的,但却是非常危险的。因为你改动了数组中的一个元素,磁盘上的文件也随之改动了,在修改数据与保存到磁盘之间并没有分隔。
Mapping a file into memory
最简单的学习莫过于通过例子来学习了。在下面这个例子中,我们希望在内存中映射一个FileChannel(所有或者部分地)。所以我们采用了FileChannel.map()方法,下面这行代码映射了前1024字节的文件数据到内存中了。
1 | MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,0,1024 ); |
map方法返回了一个MappedByteBuffer,他是ByteBuffer的一个子类。所以,你也可以将他当作一个ByteBuffer来用,并且操作系统会为你映射好。
01 | /** |
02 | * 将源文件拷贝一份到目的文件中 |
03 | * @param src 源文件 |
04 | * @param des 目的文件 |
05 | * @throws Exception IO异常 |
06 | */ |
07 | public static void fastCopyFile( File src, File des ) throws Exception { |
08 |
09 | RandomAccessFile raf1 = new RandomAccessFile(src, "r"); |
10 | RandomAccessFile raf2 = new RandomAccessFile(des, "rw"); |
11 |
12 | FileChannel fcIn = raf1.getChannel(); |
13 | FileChannel fcOut = raf2.getChannel(); |
14 |
15 | long len = src.length(); |
16 | long index = 1; |
17 | MappedByteBuffer mappedByteBuffer = null ; |
18 | boolean isFinished = false; |
19 |
20 | while( !isFinished ){ |
21 |
22 | /** 如果最后一次读写不足8K,则取文件长度 */ |
23 | long begin = (index - 1) * BUFFER_SIZE; |
24 | long length = BUFFER_SIZE; |
25 |
26 | if( (begin + length)>= len ){ |
27 | isFinished = true; |
28 | length = len - begin ; |
29 | } |
30 |
31 | /** 映射文件部分内容到内存中 */ |
32 | mappedByteBuffer = fcIn.map(FileChannel.MapMode.READ_ONLY, begin, length); |
33 |
34 | /** 写入到目标文件中 */ |
35 | fcOut.write(mappedByteBuffer); |
36 |
37 | index ++; |
38 |
39 | /** 清空mappedByteBuffer */ |
40 | mappedByteBuffer.clear(); |
41 |
42 | } |
43 |
44 | /** 回收资源 */ |
45 | mappedByteBuffer = null; |
46 | fcOut.close(); |
47 | fcIn.close(); |
48 | raf2.close(); |
49 | raf1.close(); |
50 |
51 | } |
来源:oschina
链接:https://my.oschina.net/u/125782/blog/107641