[关闭]
@lemonguge 2015-07-01T14:30:32.000000Z 字数 10074 阅读 1005

Java nio(一)

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实质上是一个容器对象。


NIO的优势

面向流的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对象。

在上面的介绍中出现了几个状态变量:位置、限制、容量和标记。笔者在介绍这些状态变量之前,不知道大家是否还记得在《继承与实现》所说过的“查看该体系中的顶层类,了解该体系的基本功能。”查看API发现ByteBuffer继承自抽象类Buffer,在笔者介绍Buffer这个类的时候,会同时对这几个状态变量进行讲解。

状态变量

API中能够看到:0 <= 标记 <= 位置 <= 限制 <= 容量 ,接下来了解一下缓冲区的状态变量:

  1. 容量(capacity):是缓冲区所包含的元素的数量,缓冲区的容量不能为负并且不能更改。
  2. 限制(limit):是第一个不应该读取或写入的元素的索引
  3. 位置(position):是下一个要读取或写入的元素的索引
  4. 标记(mark):是一个索引。

缓冲区的内部统计

如果缓冲区的底层实现为数组(还有底层实现不为数组的情况,下面会进行介绍),那么可以把索引理解为数组的角标。capacitylimitposition 这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。现在要将数据从一个输入通道拷贝到一个输出通道,来详细分析每一个变量。

首先,观察一个新创建的缓冲区,假设这个缓冲区的总容量为8个字节:

初始化缓冲区

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

缓冲区的初始状态变量

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

缓冲区的初始位置

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

从通道读取数据到缓冲区

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

再次读取

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

  1. 它将limit 设置为当前position
  2. 它将position 设置为0。

调用了flip方法

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

当我们从缓冲区中读取四个字节并将它们写入输出通道。这使得position 增加到4,而limit 不变,如下所示:

向输出通道写入数据

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

向输出通道写入最后一个

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

  1. 使limit 保持不变;
  2. position 设置为0。

调用了rewind方法

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

  1. limit 设置为与capacity 相同;
  2. 将position设置为0。

调用了clear方法

如果缓冲区中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。如果后续还需要这些未读数据,那么使用compact()方法(该方法不是Buffer体系顶层类中的方法),这个方法做了三件事情:

  1. 将所有未读的数据拷贝到缓冲区起始处;
  2. 将position设到最后一个未读元素正后面;
  3. 将limit设置为与capacity相同。

操作状态变量的方法

除了容量以外,我们可以设置限制、位置和标记。

不过笔者想向大家介绍了三个比较常用的方法,希望大家不要混淆了。

  1. Buffer clear()清除此缓冲区。使缓冲区为一系列新的通道读取相对放置操作做好准备:它将限制设置为容量大小,将位置设置为0。
  2. Buffer flip()反转此缓冲区。使缓冲区为一系列新的通道写入相对获取操作做好准备:它将限制设置为当前位置,然后将位置设置为0。
  3. Buffer rewind()重绕此缓冲区。使缓冲区为重新读取已包含的数据做好准备:它使限制保持不变,将位置设置为0。

对于上面的三个方法和compact()方法,如果缓冲区定义了标记,则都将丢弃该标记。相对位置为此缓冲区当前位置,绝对位置一般指明索引。hasRemaining()方法获取在当前位置和限制之间是否有元素(有元素返回true)。

  1. import java.nio.ByteBuffer;
  2. public class ByteBuf {
  3. public static void main(String[] args) {
  4. ByteBuffer buf = ByteBuffer.allocate(10);
  5. for (int i = 0; i < 7; i++) {
  6. buf.put((byte) i);
  7. }
  8. commonShow(buf);
  9. }
  10. // 常用方法展示
  11. public static void commonShow(ByteBuffer buf) {
  12. System.out.println("---初始化缓冲区的状态变量---");
  13. showBuffProps(buf);
  14. System.out.println("---调用了flip方法后的状态变量---");
  15. buf.flip();
  16. showBuffProps(buf);
  17. System.out.println("---设置缓冲区的位置为3后的状态变量---");
  18. buf.position(3);
  19. showBuffProps(buf);
  20. System.out.println("缓冲区索引为5的值:" + buf.get(5));
  21. System.out.println("---调用了compact方法后的状态变量---");
  22. buf.compact();
  23. showBuffProps(buf);
  24. System.out.println("压缩缓冲区后索引为2的值:" + buf.get(2));
  25. System.out.println("---调用了flip方法后的状态变量---");
  26. buf.flip();
  27. showBuffProps(buf);
  28. System.out.println("---设置缓冲区的位置为3后的状态变量---");
  29. buf.position(3);
  30. showBuffProps(buf);
  31. System.out.println("---调用了rewind方法后的状态变量---");
  32. buf.rewind();
  33. showBuffProps(buf);
  34. System.out.println("---调用了clear方法后的状态变量---");
  35. buf.clear();
  36. showBuffProps(buf);
  37. }
  38. // 展示缓冲区属性
  39. public static void showBuffProps(ByteBuffer buf) {
  40. System.out.println("position = " + buf.position());
  41. System.out.println(" limit = " + buf.limit());
  42. System.out.println("capacity = " + buf.capacity());
  43. }
  44. } /* Output:
  45. ---初始化缓冲区的状态变量---
  46. position = 7
  47. limit = 10
  48. capacity = 10
  49. ---调用了flip方法后的状态变量---
  50. position = 0
  51. limit = 7
  52. capacity = 10
  53. ---设置缓冲区的位置为3后的状态变量---
  54. position = 3
  55. limit = 7
  56. capacity = 10
  57. 缓冲区索引为5的值:5
  58. ---调用了compact方法后的状态变量---
  59. position = 4
  60. limit = 10
  61. capacity = 10
  62. 压缩缓冲区后索引为2的值:5
  63. ---调用了flip方法后的状态变量---
  64. position = 0
  65. limit = 4
  66. capacity = 10
  67. ---设置缓冲区的位置为3后的状态变量---
  68. position = 3
  69. limit = 4
  70. capacity = 10
  71. ---调用了rewind方法后的状态变量---
  72. position = 0
  73. limit = 4
  74. capacity = 10
  75. ---调用了clear方法后的状态变量---
  76. position = 0
  77. limit = 10
  78. capacity = 10
  79. *///:~

只需要通过输出语句就可以方便我们理解缓冲区的状态变量以及常用的几个方法。

内存与缓冲区的交互

接下来,我们来学习内存与ByteBuffer的交互,使用ByteBuffer类的get()put()方法直接访问缓冲区中的数据。

绝对方法会忽略limitposition 值,也不会影响它们,它完全绕过了缓冲区的内部统计。

  1. import java.nio.ByteBuffer;
  2. public class ByteBuff {
  3. public static void main(String[] args) {
  4. ByteBuffer buff = null;
  5. buff = ByteBuffer.allocate(1024);
  6. showBuffProps(buff);
  7. putBuff(buff);
  8. showBuffProps(buff);
  9. flipShow(buff);
  10. }
  11. // 调用flip方法后,显式缓冲器内容
  12. public static void flipShow(ByteBuffer buff) {
  13. System.out.println("---flipShow---");
  14. buff.flip();
  15. System.out.println("before get pos="+buff.position());
  16. System.out.println("before get limit="+buff.limit());
  17. int length = buff.limit();
  18. byte[] des = new byte[length];
  19. System.out.println("now get..");
  20. buff.get(des);
  21. System.out.println("after get pos="+buff.position());
  22. System.out.println("after get limit="+buff.limit());
  23. System.out.println("ByteBuffer String:" + new String(des));
  24. System.out.print("ByteBuffer byte:");
  25. buff.position(0);
  26. while(buff.hasRemaining())
  27. System.out.print((char)buff.get());
  28. System.out.println();
  29. System.out.print("ByteBuffer char:");
  30. System.out.print(buff.getChar(0)); //读取给定索引处的两个字节,并根据当前的字节顺序将它们组成char值。
  31. }
  32. // 向缓冲区写入
  33. public static void putBuff(ByteBuffer buff) {
  34. System.out.println("-->put run..");
  35. buff.put((byte) 97); // 将a写入相对位置一个字节,然后该位置递增。
  36. buff.put("bcd".getBytes());
  37. buff.put(4, (byte) 65); // 绝对位置写入一个字节A,位置不递增
  38. buff.putChar((char) 66); // B在position=4的位置,将写入2个字节,会把之前的A覆盖
  39. buff.putInt(67); // 将C写入当前位置4个字节,将该位置增加 4。
  40. }
  41. // 展示缓冲区属性
  42. public static void showBuffProps(ByteBuffer buff) {
  43. System.out.println("---propsShow---");
  44. System.out.println("cap="+buff.capacity());
  45. System.out.println("pos="+buff.position());
  46. System.out.println("limit="+buff.limit());
  47. }
  48. } /* Output:
  49. ---propsShow---
  50. cap=1024
  51. pos=0
  52. limit=1024
  53. -->put run..
  54. ---propsShow---
  55. cap=1024
  56. pos=10
  57. limit=1024
  58. ---flipShow---
  59. before get pos=0
  60. before get limit=10
  61. now get..
  62. after get pos=10
  63. after get limit=10
  64. ByteBuffer String:abcd B C // B把A给覆盖了
  65. ByteBuffer byte:abcd B C // B与C三个空格
  66. ByteBuffer char:慢 // 读取了a和b后,查询编码表所显示了"慢"
  67. *///:~

从输出可以看到,从绝对位置写入的"A"被后来的"B"给覆盖。除了刚刚讲解的get()put()方法,ByteBuffer还有用于读写不同类型的值的其他方法,而每一种读写类型的方法有绝对和相对的两种,以下是一个小的示例:

  1. import java.nio.ByteBuffer;
  2. public class TypesInByteBuffer {
  3. static public void main(String args[]) throws Exception {
  4. ByteBuffer buffer = ByteBuffer.allocate(64);
  5. buffer.putInt(30);
  6. buffer.putLong(7000000000000L);
  7. buffer.putDouble(Math.PI);
  8. // 为读取缓冲区的内容做准备
  9. buffer.flip();
  10. System.out.println(buffer.getInt());
  11. System.out.println(buffer.getLong());
  12. System.out.println(buffer.getDouble());
  13. }
  14. } /* Output:
  15. 30
  16. 7000000000000
  17. 3.141592653589793
  18. *///:~

缓冲区分片与数据共享

slice()方法(该方法不是Buffer体系顶层类中的方法)根据现有的缓冲区创建一个子缓冲区。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。新缓冲区的内容将从原来缓冲区的当前位置开始,到原来缓冲区的限制结束。注意,这两个缓冲区的位置、界限和标记值是相互独立的。

  1. import java.nio.ByteBuffer;
  2. public class Slice {
  3. public static void main(String[] args) {
  4. ByteBuffer buf = ByteBuffer.allocate(10);
  5. for (int i = 0; i < 7; i++) {
  6. buf.put((byte) i);
  7. }
  8. sliceData(buf);
  9. }
  10. // 子缓冲区与源缓冲区的共享数据展示
  11. public static void sliceData(ByteBuffer buf) {
  12. System.out.println("---调用了flip方法和设置位置为3后的状态变量---");
  13. buf.flip();
  14. buf.position(3);
  15. showBuffProps(buf);
  16. System.out.println("源缓冲区索引为5的值:" + buf.get(5));
  17. System.out.println("---调用了slice方法创建的子缓冲区的状态变量---");
  18. ByteBuffer slice = buf.slice();
  19. showBuffProps(slice);
  20. System.out.println("子缓冲区索引为2的值:" + slice.get(2));
  21. System.out.println("---修改子缓冲区索引为2的值后的状态变量---");
  22. slice.put(2, (byte) 2);
  23. System.out.println("子缓冲区索引为2的值:" + slice.get(2));
  24. System.out.println("源缓冲区索引为5的值:" + buf.get(5));
  25. }
  26. // 展示缓冲区属性
  27. public static void showBuffProps(ByteBuffer buf) {
  28. System.out.println("position = " + buf.position());
  29. System.out.println(" limit = " + buf.limit());
  30. System.out.println("capacity = " + buf.capacity());
  31. }
  32. } /* Output:
  33. ---调用了flip方法和设置位置为3后的状态变量---
  34. position = 3
  35. limit = 7
  36. capacity = 10
  37. 源缓冲区索引为5的值:5
  38. ---调用了slice方法创建的子缓冲区的状态变量---
  39. position = 0
  40. limit = 4
  41. capacity = 4
  42. 子缓冲区索引为2的值:5
  43. ---修改子缓冲区索引为2的值后的状态变量---
  44. 子缓冲区索引为2的值:2
  45. 源缓冲区索引为5的值:2
  46. *///:~

只需要通过输出语句就看到,虽然修改了子缓冲区的数据,但是源缓冲区的数据也发生了变化,说明了数据共享。

只读缓冲区

正如字面意思一样,可以读取缓冲区,但是不能向缓冲区写入。可以通过调用缓冲区的asReadOnlyBuffer()方法(该方法不是Buffer体系顶层类中的方法),将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。

  1. import java.nio.ByteBuffer;
  2. public class ReadOnlyBuf {
  3. public static void main(String[] args) {
  4. ByteBuffer buf = ByteBuffer.allocate(10);
  5. for (int i = 0; i < 7; i++) {
  6. buf.put((byte) i);
  7. }
  8. ByteBuffer readOnlyBuf = buf.asReadOnlyBuffer();
  9. System.out.println("只读缓冲区索引为5的值:" + readOnlyBuf.get(5));
  10. // ! readOnlyBuf.put(5, (byte) 10); // java.nio.ReadOnlyBufferException
  11. }
  12. } /* Output:
  13. 只读缓冲区索引为5的值:5
  14. *///:~

创建一个只读的缓冲区可以保证该缓冲区不会被修改,对保护数据很有用。注意,不能将只读的缓冲区转换为可写的缓冲区

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注