在JDK1.4的java.nio.*包中引入了新的I/O库,主要目的是为了提高速度,原有的I/O库也已经使用nio重新实现过了。速度的提高来自于nio所使用的结构更接近于操作系统执行I/O的方式,使用了通道和缓冲器,我们直需要和缓冲器交互,并把缓冲器派送到通道。
那么根据上面的说明,我们操作流时所做的事情就应该是得到通道,然后从通道中得到缓冲器,操作缓冲器得到数据。或者是把数据保存到缓冲器然后把缓冲器送给通道。看例子:
- 例子中使用流类的getChannel()方法得到通道FileChannel。
- ByteBuffer是一个字节的缓冲器,只能保存字节,可以使用put方法进行直接填充。也可以使用warp(byte[])方法把已经存在的字节数组包装到ByteBuffer中。
- 通过FileChannel的position方法能够随意移动其在文件中的位置。
- 只读访问的时候,必须使用allocate()方法分配ByteBuffer。或者是使用allocateDirect()来分配,这种分配方式耦合了系统的缓冲器,会随着平台的不同而不同。
- 一旦使用fc的read方法,就会告知FC向ByteBuffer存储字节。这是我们必须调用缓冲器上的flip(),让它做好让别人读取字节的准备。
- channelCopy方法中使用了两个通道进行文件的存储,其中使用writer(buffer)的时候要注意,使用之后需要使用buffer.clear()来清空缓存区,以方便缓存区继续接受值
- 在channelCopy中的方式能够看到两个通道是通过Buffer暂存了一下数据进行转移的,这种方式看起来就比较笨拙,咋个不能两个通道直接进行转移呢。transferTo方法就演示了两个通道直接交互的方式。
上面的例子中每次只能够读取一个字节的数据,这样似乎有点儿笨,如果能把读取出来的字节数组转化为字符串就比较合适了。使用buff.asCharBuffer()方法就能够实现这一点。但是如果是使用字符型的缓存就会涉及到编码的问题,看例子:
直接打印buff.asCharBuffer()会是乱码,必须要经过编码处理
处理的方式为两种,要不就在输出的时候进行解码像writeByUTF中那样,或者是在输入的时候进行编码。
获取原始类型
ByteBuffer虽然只能保存字节类型的数据,但是它提供了产生各种不同原始类型的方法。例子如下:
不管是通过试图还是缓冲器直接添加的数据实际上都是改变了缓冲器中的byte的内容。
当使用不同的试图查看数据的时候,查看的结果是按照下列规则进行的
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 97 |
a | |||||||
0 | 0 | 0 | 97 | ||||
0 | 97 | ||||||
4.8E-322 |
字节排序方式。
有高位优先和低位优先两种。高位优先是把比较重要的字节放在地址最高的存储器单元,低位反之。ByteBuffer默认为高位反之。看例子:
看这个例子就很好的展示了怎样使用bb.order(ByteOrder.LITTLE_ENDIAN);更改读取的次序。
综上所述,使用nio进行数据转移的时候我们要知道只有ByteBuffer才能够跟通道进行交互。
缓冲器的组成
缓冲器是能够跟通道进行通信的唯一方式。下面就来说一下这个缓冲器的内部结构。
在缓冲器中存在的是byte类型的数据和用于操作数据的四个索引。这些索引为:mark,position,limit,capacity。
其中mark代表标记能够把这个标记指定到某个位置,其他操作会借助这个标记。
position是当前缓存所在的位置。
limit指限额。
capacity指容量。
缓冲器又提供了如下的方法来使用这些索引。
capacity()得到容量,也就是position。
clear() 。清空缓存区,将position置0,limit置为容量。
flip()。 将limit置为position,position设置为0.此方法用于准备从缓存区读取已经写入的数据。
limit()。返回limit值。
mark()将mark设置为position。
position()返回position的值。
remaining() 返回limit-position。
hasRemaining() 若有介于position和limit之间的元素,则返回true。
看例子:
最开始调用put之后的结果是position指向0,limit指向容量。
当调用了mark之后。会把mark指向当前position的位置。使用get方法后position会+1.
调用reset()方法会把position的值设置为mark的值。使用了put方法之后position也会+1.
这里还要说一下flip()方法,因为在调用了read方法之后limit的值为容量,但是有可能并没有填满,而这时position指向的是最后一个存在的元素,因此调用了flip()方法之后能够保证limit指向了最后一个存在的元素,这个时候使用hasRemaining判断的时候能够保证不会输出空的元素。
存储器映射文件
这种方式允许我们创建和修改哪些因为太大而不能放入内存的文件。使用存储器映射文件我们可以假定整个文件都在内存中,而且可以完全把它当做非常大的数组来访问。
使用这种映射文件的方式要比常规的流的方式效率上高很多。
对象序列化
把一个对象转化为一个字节序列,对象的序列化加入到java语言中主要是为了对RMI提供支持,使得其他计算机能够像使用本地对象一样使用远程对象。另一个原因是为了支持javaBeans的序列化。
只要对象实现了Serializable接口(这个接口仅仅是一个标记,没有任何的方法),对象就能够被序列化。
上面的例子表示了一个对象进行序列号之后是能够进行还原的。
序列化的控制。
如果我们想对序列号进行一些控制,可以怎样办呢?
在上面这个例子中,我们可以通过实现Externalizable接口来控制序列化,首先必须有public的默认的构造器,因为反序列化的时候会调用。
接口中有两个方法writeExternal在进行序列号的时候会调用,我们可以通过out.writeXX()来控制序列化哪些元素。
还有一个readExternal方法,可以通过in.readXX()方法来控制还原哪些元素。
如果不想使用Externalizable接口还可以在实现了Serializable接口后,在类中自定义writeObject(ObjectOutputStream output)
和readObject(ObjectInputStream input)方法,分别来控制序列号和反序列化的动作。
下面的例子可以看出transient关键字在默认的序列化中是不会被序列化的,但是可以通过手工添加的方式进行序列化。
序列化能够维护整个对象网的信息
在这个例子中我们会发现在同一个流中读出的list中的两个hourse对象所含有的原来一个People的引用在反序列化之后仍然是一个引用。
这个可以成为深度复制,及不光复制了原来对象的引用,还复制了整个对象网。