@lemonguge
2015-07-01T14:30:32.000000Z
字数 10074
阅读 1005
NIO
JDK1.4引入了一个新的I/O类库:java.nio.*,nio的意思是new io,其目的在于提高速度。实际上,旧的I/O包已经使用nio重新实现过,以便充分利用这种速度提高,因此,即使我们不显式地用nio编写代码,也能从中获益。
标准的I/O属于阻塞IO(read方法为阻塞式方法),基于字节流和字符流进行操作的,而nio是基于通道(Channel)和缓冲区(Buffer)进行操作。
速度的提高来自于所使用的结构更接近与操作系统执行I/O的方式:通道和缓冲器。
简单地说,把这个结构想象成一个煤矿,通道是一个包含煤层(数据)的矿藏,而缓冲区则是派送到矿藏的卡车。卡车载满煤炭而归,我们再从卡车上获得煤炭。
我们并没有直接和通道交互,只是和缓冲器交互,并把缓冲器派送到通道中。通道要么从缓冲器获得数据,要么向缓冲器发送数据。通道是对原I/O包中的流的模拟,一个Buffer实质上是一个容器对象。
面向流的I/O系统一次一个字节地处理数据(单个字节的移动)。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的I/O通常相当慢。
面向块的I/O系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的I/O缺少一些面向流的I/O所具有的优雅性和简单性。nio将最耗时的I/O操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。
通道和缓冲区是no中的核心对象,几乎在每一个I/O操作中都要使用它们。
缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
缓冲区是特定基本类型元素的线性有限序列,唯一直接与通道交互的缓冲区是
ByteBuffer
一个ByteBuffer可以在其底层字节数组上进行get/set操作(即字节的获取和设置)。事实上,对于每一种基本Java类型都有一种缓冲区类型,除了boolean类型。查看API可以发现ByteBuffer并没有构造函数,不过有两种常用的方式来创建ByteBuffer对象。
ByteBuffer allocate(int capacity)静态方法分配一个新的字节缓冲区。新缓冲区的位置将为零,其界限(限制)将为其容量,其标记是不确定的。它将具有一个底层实现数组,且其数组偏移量将为零。ByteBuffer wrap(byte[] array)静态方法将byte数组包装到缓冲区中。新缓冲区的容量和界限(限制)将为array.length,其位置将为零,其标记是不确定的。其底层实现数组将为给定数组,并且其数组偏移量将为零。(注意,缓冲区修改将导致数组修改)在上面的介绍中出现了几个状态变量:位置、限制、容量和标记。笔者在介绍这些状态变量之前,不知道大家是否还记得在《继承与实现》所说过的“查看该体系中的顶层类,了解该体系的基本功能。”查看API发现ByteBuffer继承自抽象类Buffer,在笔者介绍Buffer这个类的时候,会同时对这几个状态变量进行讲解。
在API中能够看到:0 <= 标记 <= 位置 <= 限制 <= 容量 ,接下来了解一下缓冲区的状态变量:
如果缓冲区的底层实现为数组(还有底层实现不为数组的情况,下面会进行介绍),那么可以把索引理解为数组的角标。capacity、limit 和position 这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。现在要将数据从一个输入通道拷贝到一个输出通道,来详细分析每一个变量。
首先,观察一个新创建的缓冲区,假设这个缓冲区的总容量为8个字节:

此时限制(limit)(第一个不应该读取或写入的元素的索引)为其容量(capacity),如下所示:

位置(position)设置为0,如果我们要写入一些数据到缓冲区中,那么下一个写入的数据就进入第1个slot。

由于容量(capacity)不会改变,所以我们在下面的讨论中可以忽略它。现在我们可以开始在新创建的缓冲区上进行读或写操作,首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从position 开始的位置,这时position 被设置为0。读完之后,position 就增加到3,limit 并没有改变。如下所示:

在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由position所指定的位置上,position因而增加2,此时position为5,limit并没有改变。

当向缓冲区写入数据时,缓冲区会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将缓冲区从写模式切换到读模式。在读模式下,可以读取之前写入到缓冲区的所有数据。这个方法做了两件非常重要的事:

我们现在可以将数据从缓冲区读取数据再,再写入输出通道了。position 被设置为0,这意味着我们得到的下一个字节是第一个字节。limit 已被设置为原来的position ,这意味着它包括以前读到的所有字节,并且一个字节也不多。
当我们从缓冲区中读取四个字节并将它们写入输出通道。这使得position 增加到4,而limit 不变,如下所示:

我们现在只剩下一个字节可写了。limit 在我们调用flip()方法时被设置为5,并且position 不能超过limit 。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得position 增加到5,并保持limit 不变,如下所示:

如果我们想再次从此缓冲区读取数据到另一个输出通道时,我们也可以调用rewind()方法,这个方法做了两件很重要的事情:

当我们将缓冲区所有的数据读取完成,应该调用clear()方法,这个方法也做了两件很重要的事情:

如果缓冲区中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。如果后续还需要这些未读数据,那么使用compact()方法(该方法不是Buffer体系顶层类中的方法),这个方法做了三件事情:
除了容量以外,我们可以设置限制、位置和标记。
Buffer mark()在此缓冲区的位置设置标记。int position()方法返回此缓冲区的位置;Buffer position(int newPosition)方法设置此缓冲区的位置。如果标记已定义且大于新的位置,则丢弃该标记。Buffer reset()将此缓冲区的位置重置为以前标记的位置,不更改也不丢弃标记的值。int limit()方法返回此缓冲区的限制;Buffer limit(int newLimit)方法设置此缓冲区的限制。如果位置大于新的限制,则将该位置设置为新限制。如果标记已定义且大于新限制,则丢弃该标记。不过笔者想向大家介绍了三个比较常用的方法,希望大家不要混淆了。
Buffer clear()清除此缓冲区。使缓冲区为一系列新的通道读取或相对放置操作做好准备:它将限制设置为容量大小,将位置设置为0。Buffer flip()反转此缓冲区。使缓冲区为一系列新的通道写入或相对获取操作做好准备:它将限制设置为当前位置,然后将位置设置为0。Buffer rewind()重绕此缓冲区。使缓冲区为重新读取已包含的数据做好准备:它使限制保持不变,将位置设置为0。对于上面的三个方法和compact()方法,如果缓冲区定义了标记,则都将丢弃该标记。相对位置为此缓冲区当前位置,绝对位置一般指明索引。hasRemaining()方法获取在当前位置和限制之间是否有元素(有元素返回true)。
import java.nio.ByteBuffer;public class ByteBuf {public static void main(String[] args) {ByteBuffer buf = ByteBuffer.allocate(10);for (int i = 0; i < 7; i++) {buf.put((byte) i);}commonShow(buf);}// 常用方法展示public static void commonShow(ByteBuffer buf) {System.out.println("---初始化缓冲区的状态变量---");showBuffProps(buf);System.out.println("---调用了flip方法后的状态变量---");buf.flip();showBuffProps(buf);System.out.println("---设置缓冲区的位置为3后的状态变量---");buf.position(3);showBuffProps(buf);System.out.println("缓冲区索引为5的值:" + buf.get(5));System.out.println("---调用了compact方法后的状态变量---");buf.compact();showBuffProps(buf);System.out.println("压缩缓冲区后索引为2的值:" + buf.get(2));System.out.println("---调用了flip方法后的状态变量---");buf.flip();showBuffProps(buf);System.out.println("---设置缓冲区的位置为3后的状态变量---");buf.position(3);showBuffProps(buf);System.out.println("---调用了rewind方法后的状态变量---");buf.rewind();showBuffProps(buf);System.out.println("---调用了clear方法后的状态变量---");buf.clear();showBuffProps(buf);}// 展示缓冲区属性public static void showBuffProps(ByteBuffer buf) {System.out.println("position = " + buf.position());System.out.println(" limit = " + buf.limit());System.out.println("capacity = " + buf.capacity());}} /* Output:---初始化缓冲区的状态变量---position = 7limit = 10capacity = 10---调用了flip方法后的状态变量---position = 0limit = 7capacity = 10---设置缓冲区的位置为3后的状态变量---position = 3limit = 7capacity = 10缓冲区索引为5的值:5---调用了compact方法后的状态变量---position = 4limit = 10capacity = 10压缩缓冲区后索引为2的值:5---调用了flip方法后的状态变量---position = 0limit = 4capacity = 10---设置缓冲区的位置为3后的状态变量---position = 3limit = 4capacity = 10---调用了rewind方法后的状态变量---position = 0limit = 4capacity = 10---调用了clear方法后的状态变量---position = 0limit = 10capacity = 10*///:~
只需要通过输出语句就可以方便我们理解缓冲区的状态变量以及常用的几个方法。
接下来,我们来学习内存与ByteBuffer的交互,使用ByteBuffer类的get()和put()方法直接访问缓冲区中的数据。
get()方法 byte get()相对方法,读取此缓冲区当前位置的字节,然后该位置递增。byte get(int index)绝对方法,读取指定索引处的字节。ByteBuffer get(byte[] dst, int offset, int length)相对批量的方法,dst 向其中写入字节的数组,offset偏移量,length写入给定数组中的字节的最大数量。将此缓冲区的字节传输到给定的目标数组中。如果此缓冲中剩余的字节少于满足请求所需的字节(即如果length>remaining()),则不传输字节且抛出BufferUnderflowException。因为会首先检查此缓冲区中缓冲区中是否具有足够的字节,这样可能效率更高。ByteBuffer get(byte[] dst)相对批量方法,等价与get(dst, 0, dst.length)方法。put()方法 ByteBuffer put(byte b)相对方法,将给定的字节写入此缓冲区的当前位置,然后该位置递增。ByteBuffer put(int index, byte b)绝对方法,将给定字节写入此缓冲区的给定索引处。ByteBuffer put(byte[] src, int offset, int length)相对批量方法,将给定源数组中的字节字传输到此缓冲区中,如果要从该数组中复制的字节多于此缓冲区中的剩余字节,即如果length>remaining(),则不传输字节且将抛出BufferOverflowException。因为会首先检查此缓冲区中是否有足够空间,这样可能效率更高。ByteBuffer put(byte[] src)相对批量方法,等价与put(src, 0, src.length)方法。ByteBuffer put(ByteBuffer src)相对批量方法,将给定源缓冲区src 中的剩余字节传输到此缓冲区中,同样会检查此缓冲区中是否有足够空间。绝对方法会忽略limit 和position 值,也不会影响它们,它完全绕过了缓冲区的内部统计。
import java.nio.ByteBuffer;public class ByteBuff {public static void main(String[] args) {ByteBuffer buff = null;buff = ByteBuffer.allocate(1024);showBuffProps(buff);putBuff(buff);showBuffProps(buff);flipShow(buff);}// 调用flip方法后,显式缓冲器内容public static void flipShow(ByteBuffer buff) {System.out.println("---flipShow---");buff.flip();System.out.println("before get pos="+buff.position());System.out.println("before get limit="+buff.limit());int length = buff.limit();byte[] des = new byte[length];System.out.println("now get..");buff.get(des);System.out.println("after get pos="+buff.position());System.out.println("after get limit="+buff.limit());System.out.println("ByteBuffer String:" + new String(des));System.out.print("ByteBuffer byte:");buff.position(0);while(buff.hasRemaining())System.out.print((char)buff.get());System.out.println();System.out.print("ByteBuffer char:");System.out.print(buff.getChar(0)); //读取给定索引处的两个字节,并根据当前的字节顺序将它们组成char值。}// 向缓冲区写入public static void putBuff(ByteBuffer buff) {System.out.println("-->put run..");buff.put((byte) 97); // 将a写入相对位置一个字节,然后该位置递增。buff.put("bcd".getBytes());buff.put(4, (byte) 65); // 绝对位置写入一个字节A,位置不递增buff.putChar((char) 66); // B在position=4的位置,将写入2个字节,会把之前的A覆盖buff.putInt(67); // 将C写入当前位置4个字节,将该位置增加 4。}// 展示缓冲区属性public static void showBuffProps(ByteBuffer buff) {System.out.println("---propsShow---");System.out.println("cap="+buff.capacity());System.out.println("pos="+buff.position());System.out.println("limit="+buff.limit());}} /* Output:---propsShow---cap=1024pos=0limit=1024-->put run..---propsShow---cap=1024pos=10limit=1024---flipShow---before get pos=0before get limit=10now get..after get pos=10after get limit=10ByteBuffer String:abcd B C // B把A给覆盖了ByteBuffer byte:abcd B C // B与C三个空格ByteBuffer char:慢 // 读取了a和b后,查询编码表所显示了"慢"*///:~
从输出可以看到,从绝对位置写入的"A"被后来的"B"给覆盖。除了刚刚讲解的get()和put()方法,ByteBuffer还有用于读写不同类型的值的其他方法,而每一种读写类型的方法有绝对和相对的两种,以下是一个小的示例:
import java.nio.ByteBuffer;public class TypesInByteBuffer {static public void main(String args[]) throws Exception {ByteBuffer buffer = ByteBuffer.allocate(64);buffer.putInt(30);buffer.putLong(7000000000000L);buffer.putDouble(Math.PI);// 为读取缓冲区的内容做准备buffer.flip();System.out.println(buffer.getInt());System.out.println(buffer.getLong());System.out.println(buffer.getDouble());}} /* Output:3070000000000003.141592653589793*///:~
slice()方法(该方法不是Buffer体系顶层类中的方法)根据现有的缓冲区创建一个子缓冲区。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。新缓冲区的内容将从原来缓冲区的当前位置开始,到原来缓冲区的限制结束。注意,这两个缓冲区的位置、界限和标记值是相互独立的。
import java.nio.ByteBuffer;public class Slice {public static void main(String[] args) {ByteBuffer buf = ByteBuffer.allocate(10);for (int i = 0; i < 7; i++) {buf.put((byte) i);}sliceData(buf);}// 子缓冲区与源缓冲区的共享数据展示public static void sliceData(ByteBuffer buf) {System.out.println("---调用了flip方法和设置位置为3后的状态变量---");buf.flip();buf.position(3);showBuffProps(buf);System.out.println("源缓冲区索引为5的值:" + buf.get(5));System.out.println("---调用了slice方法创建的子缓冲区的状态变量---");ByteBuffer slice = buf.slice();showBuffProps(slice);System.out.println("子缓冲区索引为2的值:" + slice.get(2));System.out.println("---修改子缓冲区索引为2的值后的状态变量---");slice.put(2, (byte) 2);System.out.println("子缓冲区索引为2的值:" + slice.get(2));System.out.println("源缓冲区索引为5的值:" + buf.get(5));}// 展示缓冲区属性public static void showBuffProps(ByteBuffer buf) {System.out.println("position = " + buf.position());System.out.println(" limit = " + buf.limit());System.out.println("capacity = " + buf.capacity());}} /* Output:---调用了flip方法和设置位置为3后的状态变量---position = 3limit = 7capacity = 10源缓冲区索引为5的值:5---调用了slice方法创建的子缓冲区的状态变量---position = 0limit = 4capacity = 4子缓冲区索引为2的值:5---修改子缓冲区索引为2的值后的状态变量---子缓冲区索引为2的值:2源缓冲区索引为5的值:2*///:~
只需要通过输出语句就看到,虽然修改了子缓冲区的数据,但是源缓冲区的数据也发生了变化,说明了数据共享。
正如字面意思一样,可以读取缓冲区,但是不能向缓冲区写入。可以通过调用缓冲区的asReadOnlyBuffer()方法(该方法不是Buffer体系顶层类中的方法),将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。
import java.nio.ByteBuffer;public class ReadOnlyBuf {public static void main(String[] args) {ByteBuffer buf = ByteBuffer.allocate(10);for (int i = 0; i < 7; i++) {buf.put((byte) i);}ByteBuffer readOnlyBuf = buf.asReadOnlyBuffer();System.out.println("只读缓冲区索引为5的值:" + readOnlyBuf.get(5));// ! readOnlyBuf.put(5, (byte) 10); // java.nio.ReadOnlyBufferException}} /* Output:只读缓冲区索引为5的值:5*///:~
创建一个只读的缓冲区可以保证该缓冲区不会被修改,对保护数据很有用。注意,不能将只读的缓冲区转换为可写的缓冲区。