Java NIO 简介

JAVA ginotang 967℃ 0评论

Nio和普通IO的区别

  • 传统的IO操作是基于流的,例如字节流和字符流;而Nio是基于缓冲区的(Buffer对象)。
  • Nio可以同时进行读写操作(双向操作),而传统的IO则是单向的,只能读或写。
  • Nio可进行异步读写操作,而传统的IO则只能进行阻塞式操作。

Channel和Buffer(缓冲区)

在Nio中,数据总是在Channel对象和Buffer对象之间进行传输:把数据从Buffer写入Channel,或者从Channel读入数据到Buffer。Buffer是内存中的一个数据段,java提供了一些列的api来方便地操作这个数据对象。Channel是一个接口,我们通过和Channel连接的底层文件或网络套接字操作数据。Nio包中实现了几个比较重要的类:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel用于进行文件的读取和写入操作。

DatagramChannel用于向网络中通过udp的形式读取和写入数据

SocketChannel同样是向网络中读取和写入数据,但是是以tcp的形式进行操作。

ServerSocketChannel可用于监听网络中的tcp连接,每个连接创建一个SocketChannel对象。

创建Channel对象

在早期的API中,FileChannel可通过FileInputStream、FileOutputStream、RandomAccessFile对象的getChannel方法创建一个连接到底层文件的Channel对象。

而DatagramChannel和SocketChannel则可通过它们的静态方法open来创建对象。

下面的例子创建了一个FileChannel对象并从文件读取数据:

从java1.7 开始,可以使用新的apiFiles创建Channel对象,例如:

 

创建Buffer对象

Java NIO中有如下几种Buffer类型:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

其中最重要的类型是ByteBuffer,因为Channel中的读写方法只接受ByteBuffer类型的参数。其他的类型使用的时候都是通过ByteBuffer转换而来的。

在前面的代码中,我们通过ByteBuffer的静态方法allocate创建了一个容量为64的ByteBuffer对象:

缓冲区的容量(Capacity)

缓冲区的容量指的是该对象可容纳多少个指定类型的数据,例如前面的allocate(64),则该缓冲区可包含64个byte类型的元素,如果是DoubleBuffer的对象,例如:DoubleBuffer.allocate(32),则这个缓冲区可容纳32个Double类型的元素。缓冲区中的值从零开始索引,因此,在引用缓冲区中的值时,索引位置从0开始到capacity-1。

缓冲区的位置(position)和限制(limit)

缓冲区有两个模式,分别是读模式和写模式。刚初始化的缓冲区position在索引0处。在写模式中,position通常指的是已经写入的数据量,例如一个初始化容量为64的缓冲区,写入的数据量为32,则position为32。limit在写模式中指的是允许写入的数据量,默认情况下limit的值等于缓冲区capacity的值。

在读模式中,情况稍有不同。在向一个缓冲区写完数据后,再从这个缓冲区中读取数据,可以这样做:

flip()方法用于把缓冲区从写模式切换到读模式,额外地,调用flip()方法会把缓冲区的position重置为0,并把limit的值设置为写模式时position的值(之前写入的数据量),也就是说,limit在读模式表示我们从缓冲区能够读取的最大数据。

rewind方法

rewind方法用于重置position的值,通常我们想切换缓冲区到读模式的时候才会这样做,以便读取所有的数据。rewind方法并不会设置limit的值,limit的值依然等于capacity,这样,就会读取到缓冲区中的空数据。如果要达到flip()方法的效果,可以显式设置缓冲区的limit值:

clear方法和compact()方法

clear方法用于把position的值和limit的值还原到初始状态,即position=0,limit=capacity,但是,clear之后缓冲区中的数据依然存在。

compact方法用于压缩缓冲区。假如缓冲区中有部分数据还没有读取,但是这个时候你需要先向缓冲区中写数据,那么,调用compact方法会把未读取的数据移动到缓冲区开始的位置,然后把position设置到未读数据的后面,limit设置为capacity,这样,未读的数据就不会被覆盖。

标记(mark)和重置(reset)

对缓冲区使用标记是为了告诉缓冲区我们稍后使用reset想要返回的特定索引位置。例如:

上面的代码把标记设置为256,在经过一系列对缓冲区(position)的修改之后,可以使用reset方法重新返回到索引256的位置。reset之前必须设置了标记,否则reset方法会抛出InvalidMarkException异常。

传输数据到缓冲区

所有Buffer类都有两种put()方法,用于将单个数据类型传输到缓冲区:

  • put(byte b)
  • put(int index, byte b)

单个参数的put方法把字节传输到缓冲区的当前位置(position)并且将位置增加1,注意position的位置不能超过limit的限制,否则抛出BufferOverflowException异常;两个参数的put方法把字节传输到指定index的索引位置,但不改变position的值,如果index值为负数,或者index大于或等于缓冲区的limit值,则抛出IndexOutOfBoundsException异常。

除了可以把单个数据类型传输到缓冲区外,所有的Buffer类都有三个put方法,用于传输大块的数据到缓冲区。下面是这些方法:

  • put(byte[] array)
  • put(byte[] array, int offset, int length)
  • put(ByteBuffer src)

其他putXXX()方法

除了byte类型的数据,Buffer类还提供了支持其他数据类型操作的方法,例如:putChar()、putShort()、putInt()、putLong()、putDouble()、putFloat()。每个方法都传输对应类型的值,这些方法都只提供对单个值的传输,如果想传输一个数组,就必须使用循环。

从缓冲区提取数据

和put方法类似,ByteBuffer对象提供了一系列的get方法,用来提取缓冲区中的数据,如下:

  • get()           提取并返回当前缓冲区位置所在的字节并且增加position的值
  • get(int index)              返回索引位置index的字节
  • get(byte[] bytes)            从缓冲区位置0开始,往后提取bytes.length个字节,position的值增加bytes.length
  • get(byte[] bytes, int offset, int length)   从当前位置往后提取length个字节,储存到bytes数组中从offset开始的索引

分散读和集中写

分散读和集中写由两个Channel接口提供支持,它们分别是:ScatteringByteChannelGatheringByteChannel

分散读

分散读指的是把数据从Channel读入到多个Buffer对象。

ScatteringByteChannel的具体实现类如下:

下面的代码把一个文件的数据读入到多个Buffer对象中

在分散读的时候,当把第一个缓冲区填满之后,才会使用第二个缓冲区。

集中写

集中写的数据流向和分散读相反,集中写把数据从多个缓冲区取出,然后写入到一个Channel中。基本上实现了分散读的类同样实现了集中写。

 

下面的代码展示如何把多个缓冲区的数据写入和channel连接的底层文件:

假如一个容量为128的ByteBuffer,里面只有20个数据,那么集中写只会向channel写这20个数据,空白的数据被忽略。

视图缓冲区

视图缓冲区是指除ByteBuffer类型之外的其他缓冲区类型,由于这些缓冲区类型底层还是封装了ByteBuffer类型,所以称它们为ByteBuffer的视图缓冲区。以CharBuffer为例子,查看ByteBuffer的asCharBuffer()方法,就会看到类似下面的代码:

可以看到,当创建一个CharBuffer缓冲区的时候,新的CharBuffer容量实际上是ByteBuffer的剩余空间除以2得来的。需要注意的是,剩余空间只能转换能被2整除的部分,不能被2整除的部分会被舍弃。例如下面的例子:

意料之中,上面的代码初始化了一个容量为32的ByteBuffer,然后使用11个数据进行填充,因此,可用空间剩下21个,除以2,获得charBuffer的容量为10,余数1被舍弃。

另外,从代码中可以得出另一个结论:视图缓冲区和ByteBuffer缓冲区使用的是同一个内存空间。

并不是说,转换为CharBuffer后的空间ByteBuffer就不能够使用,实际上,ByteBuffer还是和原来一样,它还能够操作CharBuffer的那一段空间。转换为CharBuffer的好处是使我们操作char类型数据的时候更加方便。另外,ByteBuffer和CharBuffer的position和limit并不一样,它们不会互相干扰。如下代码:

因为CharBuffer是以两个字节为单位来保存数据的,所以haha刚好占用了CharBuffer的所有4个单位空间。

下图可以更清楚地描述两者之间的关系:

分析一下上图中的一些数据,第一次调用byteBuffer.put()的时候,byteBuffer的position是4,limit是12,因为没有执行flip()方法。而charBuffer在创建之后填充数据之前,它的position和limit值分别是0和4;填充数据之后,执行flip方法之前,position分别是4和4。所以,charBuffer的位置和限制只维护属于自己管理的那一段数据。

我们之前说过,视图缓冲区和ByteBuffer缓冲区使用的是同一个内存空间,也就是说,如果ByteBuffer的position和limit值管理不好的话,是很容易覆盖掉CharBuffer中的数据的,例如下面的代码:

代码中的注释已经说得很清楚,byteBuffer的第五个索引实际上是CharBuffer的第一个索引,因此原先CharBuffer中保存的h被数据b覆盖了。为了防止这样的事情发生,我们必须自己追踪ByteBuffer的位置和限制。

转载请注明:Pure nonsense » Java NIO 简介

喜欢 (0)
0 0 投票数
文章评分
订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x
()
x