Java NIO 之 Channel 和 Buffer

图片 23

Buffer其实就是是一个容器对象,它包含一些要写入或者刚读出的数据。在NIO中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,您将数据直接写入或者将数据直接读到Stream对象中。

  • Channel
  • Channel Characteristics
  • Java NIO Channel Classes
  • buffer
  • 什么是缓冲区?
  • 缓冲区类型
  • 缓冲区内部细节
  • NIO Buffer Characteristics
  • How to Read from NIO Buffer
  • How to Write to NIO Buffer
  • Java NIO 读写文件实例程序

 原文链接     作者:Jakob Jenkov     译者:airu     校对:丁一

Java
NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的。

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO
Buffer对象,并提供了一组方法,用来方便的访问该块内存。

在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问NIO中的数据,您都是将它放到缓冲区中。

Channel

Java
NIO中,channel用于数据的传输。类似于传统IO中的流的概念。channel的两端是buffer和一个entity,不同于IO中的流,channel是双向的,既可以写入,也可以读取。而流则是单向的,所以channel更加灵活。我们在读取数据或者写入数据的时候,都必须经过channel和buffer,也就是说,我们在读取数据的时候,先利用channel将IO设备中的数据读取到buffer,然后从buffer中读取,我们在写入数据的时候,先将数据写入到buffer,然后buffer中的数据再通过channel传到IO设备中。

图片 1

image.png

我们知道NIO的特点就是将IO操作更加类似于底层IO的流程。
我们可以通过底层IO的机制更好的理解channel。

所有的系统I/O都分为两个阶段:等待就绪和操作。

  • 等待就绪就是从IO设备将数据读取到内核中的过程。
  • 操作就是将数据从内核复制到进程缓冲区的过程。

channel就可以看作是IO设备和内核区域的一个桥梁,凡是与IO设备交互都必须通过channel,而buffer就可以看作是内核缓冲区。这样整个过程就很好理解了。

我们看一下读取的过程
先从IO设备,网卡或者磁盘将内容读取到内核中,对应于NIO就是从网卡或磁盘利用channel将数据读到buffer中
然后就是内核中的数据复制到进程缓冲区,对应于就是从buffer中读取数据

写入的过程则是:
先从进程将数据写到内核中,对应于就是进程将数据写入到buffer中,
然后内核中的数据再写入到网卡或者磁盘中,对应于就是,buffer中的数据利用channel传输到IO设备中。

图片 2

image.png

以上其实就是NIO基本的利用channel和buffer进行读取和写入的流程。

Buffer的基本用法

使用Buffer读写数据一般遵循以下四个步骤:

  1. 写入数据到Buffer
  2. 调用flip()方法
  3. 从Buffer中读取数据
  4. 调用clear()方法或者compact()方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

下面是一个使用Buffer的例子:

RandomAccessFile aFile = new RandomAccessFile(“data/nio-data.txt”,
“rw”);

FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.

while (bytesRead != -1) {

    buf.flip();  //make buffer ready for read   

    while(buf.hasRemaining()){

        System.out.print((char) buf.get()); // read 1 byte at a time

    }

    buf.clear(); //make buffer ready for writing

    bytesRead = inChannel.read(buf);

}

aFile.close();

 

缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

Channel Characteristics

  • 与传统IO中的流不同,channel是双向的,可读可写
  • channel从buffer中读取数据,写入数据也是先写入到buffer
  • channel可以实现异步读写操作
  • channel可以设置为阻塞和非阻塞的模式
  • 非阻塞模式意味着,当读不到数据或者缓冲区已满无法写入的时候,不会把线程睡眠
  • 只有socket的channel可以设置为非阻塞模式,文件的channel是无法设置的。文件的IO一定是阻塞的
  • 如果是文件channel的话,channel可以在channel之间传输数据

Buffer的capacity,position和limit

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO
Buffer对象,并提供了一组方法,用来方便的访问该块内存。

为了理解Buffer的工作原理,需要熟悉它的三个属性:

  • capacity
  • position
  • limit

position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。

这里有一个关于capacity,position和limit在读写模式中的说明,详细的解释在插图后面。

图片 3

 

 

最常用的缓冲区类型是ByteBuffer。
一个ByteBuffer可以在其底层字节数组上进行get/set操作(即字节的获取和设置)。
ByteBuffer不是NIO中唯一的缓冲区类型。事实上,对于每一种基本Java类型都有一种缓冲区类型(只有boolean类型没有其对应的缓冲区类):

Java NIO Channel Classes

channel主要有两大类,四个具体的类

  • FileChannel
    文件的读写是不可以设置为非阻塞模式
  • SocketChannel
    根据tcp和udp,服务端和客户端,又可以分为, SocketChannel,
    ServerSocketChannel and DatagramChannel.它们是可以设置为非阻塞模式的

capacity

作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer

buffer

position

当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后,
position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity
– 1.

当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0.
当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

每一个Buffer类都是Buffer接口的一个实例。 除了ByteBuffer,
每一个Buffer类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准I/O操作都使用ByteBuffer,所以它具有所有共享的
缓冲区操作以及一些特有的操作。我们来看一下Buffer的类层次图吧:

什么是缓冲区?

Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入
Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O
中,您将数据直接写入或者将数据直接读到 Stream 对象中。
在 NIO
库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问
NIO 中的数据,您都是将它放到缓冲区中。
缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不
仅仅
是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

limit

在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。
写模式下,limit等于Buffer的capacity。

当切换Buffer到读模式时,
limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

图片 4

缓冲区类型

最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer
可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。
ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java
类型都有一种缓冲区类型:

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

每一个 Buffer 类都是 Buffer 接口的一个实例。 除了 ByteBuffer,每一个
Buffer
类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准 I/O
操作都使用
ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作。

Buffer的类型

Java NIO 有以下Buffer类型

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

p<>

如你所见,这些Buffer类型代表了不同的数据类型。换句话说,就是可以通过char,short,int,long,float
或 double类型来操作缓冲区中的字节。

MappedByteBuffer 有些特别,在涉及它的专门章节中再讲。

每个 Buffer 都有以下的属性:

缓冲区内部细节

本节将介绍 NIO 中两个重要的缓冲区组件:状态变量和访问方法 (accessor)。
状态变量是前一节中提到的”内部统计机制”的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。
在从通道读取数据时,数据被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要查看数据。这是使用
访问方法 get()
来完成的。同样,如果要将原始数据放入缓冲区中,就要使用访问方法 put()。

状态变量
可以用三个值指定缓冲区在任意时刻的状态:position,limit,capacity
这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。我们将在下面的小节中详细分析每一个变量,还要介绍它们如何适应典型的读/写(输入/输出)进程。在这个例子中,我们假定要将数据从一个输入通道拷贝到一个输出通道。

Buffer的分配

要想获得一个Buffer对象首先要进行分配。
每一个Buffer类都有一个allocate方法。下面是一个分配48字节capacity的ByteBuffer的例子。

 

1 ByteBuffer buf = ByteBuffer.allocate(48);

 

这是分配一个可存储1024个字符的CharBuffer:

 

1 CharBuffer buf = CharBuffer.allocate(1024);

 

capacity

Position

您可以回想一下,缓冲区实际上就是美化了的数组。在从通道读取时,您将所读取的数据放到底层的数组中。
position
变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。因此,如果您从通道中读三个字节到缓冲区中,那么缓冲区的
position 将会设置为3,指向数组中第四个元素。
同样,在写入通道时,您是从缓冲区中获取数据。 position
值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的
position 将被设置为5,指向数组的第六个元素。

向Buffer中写数据

写数据到Buffer有两种方式:

  • 从Channel写到Buffer。
  • 通过Buffer的put()方法写到Buffer里。

从Channel写到Buffer的例子

 

1 int bytesRead = inChannel.read(buf); //read into buffer.

 

通过put方法写Buffer的例子:

 

1 buf.put(127);

 

put方法有很多版本,允许你以不同的方式把数据写入到Buffer中。例如,
写到一个指定的位置,或者把一个字节数组写入到Buffer。
更多Buffer实现的细节参考JavaDoc。

这个 Buffer 最多能放多少数据。 capacity 一般在 buffer 被创建的时候指定。

Limit

limit
变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
position 总是小于或者等于 limit。

flip()方法

flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。

换句话说,position现在用于标记读的位置,limit表示之前写进了多少个byte、char等
—— 现在能读取多少个byte、char等。

limit

Capacity

缓冲区的 capacity
表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ―
或者至少是指定了准许我们使用的底层数组的容量。
limit 决不能大于 capacity。

图片 5

image.png

实例:
我们首先观察一个新创建的缓冲区。出于本例子的需要,我们假设这个缓冲区的
总容量 为8个字节。 Buffer 的状态如下所示:

图片 6

image.png

回想一下 ,limit 决不能大于 capacity,此例中这两个值都被设置为
8。我们通过将它们指向数组的尾部之后(如果有第8个槽,则是第8个槽所在的位置)来说明这点。

图片 7

image.png

position
设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0
。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0 。
position 设置如下所示:

图片 8

image.png

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

图片 9

image.png

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

图片 10

image.png

limit 没有改变。
flip
现在我们要将数据写到输出通道中。在这之前,我们必须调用 flip()
方法。这个方法做两件非常重要的事:
它将 limit 设置为当前 position。
它将 position 设置为 0。
前一小节中的图显示了在 flip 之前缓冲区的情况。下面是在 flip
之后的缓冲区:

图片 11

image.png

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

图片 12

image.png

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

图片 13

Position advanced to 5, limit unchanged

clear
最后一步是调用缓冲区的 clear()
方法。这个方法重设缓冲区以便接收更多的字节。 Clear
做两种非常重要的事情:
它将 limit 设置为与 capacity 相同。
它设置 position 为 0。
下图显示了在调用 clear() 后缓冲区的状态:

图片 14

image.png

缓冲区现在可以接收新的数据了。

从Buffer中读取数据

从Buffer中读取数据有两种方式:

  1. 从Buffer读取数据到Channel。
  2. 使用get()方法从Buffer中读取数据。

从Buffer读取数据到Channel的例子:

 

1 //read from buffer into channel.

 

 

2 int bytesWritten = inChannel.write(buf);

 

使用get()方法从Buffer中读取数据的例子

 

1 byte aByte = buf.get();

 

get方法有很多版本,允许你以不同的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。更多Buffer实现的细节参考JavaDoc。

在 Buffer 上进行的读写操作都不能越过这个下标。当写数据到 buffer 中时,
limit 一般和 capacity 相等,当读数据时, limit 代表 buffer
中有效数据的长度。

NIO Buffer Characteristics

  • buffer是java NIO中的块的基础
  • buffer可以提供一个固定大小的容器来读取和写入数据
  • 任意一个buffer都是可读的,只有选中的buffer才可写
  • buffer是channel的端点
  • 在只读的模式下,buffer的内容不可变,但是的他/她的几个变量,position,limit都是可变的
  • 默认情况下,buffer不是线程安全的

rewind()方法

Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。

position

How to Read from NIO Buffer

  • 首先创建一个指定大小的buffer
    ByteBuffer byteBuffer = ByteBuffer.allocate(512);
  • 将buffer转换为读模式
    byteBuffer.flip();
  • 然后从channel中读取数据到buffer中
    int numberOfBytes = fileChannel.read(byteBuffer);
  • 用户从buffer中读取数据
    char c = (char)byteBuffer.get();

clear()与compact()方法

一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。

如果调用的是clear()方法,position将被设回0,limit被设置成
capacity的值。换句话说,Buffer
被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。

如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。

如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。

compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。

position变量跟踪了向缓冲区中写入了多少数据或者从缓冲区中读取了多少数据。
更确切的说,当您从通道中读取数据到缓冲区中时,它指示了下一个数据将放到数组的哪一个元素中。比如,如果您从通道中读三个字节到缓冲区中,那么缓冲区的
position将会设置为3,指向数组中第4个元素。反之,当您从缓冲区中获取数据进行写通道时,它指示了下一个数据来自数组的哪一个元素。比如,当您
从缓冲区写了5个字节到通道中,那么缓冲区的 position
将被设置为5,指向数组的第六个元素。

How to Write to NIO Buffer

  • Create a buffer by allocating a size.
    ByteBuffer byteBuffer = ByteBuffer.allocate(512);//512 becomes the capacity
  • Put data into buffer
    byteBuffer.put((byte) 0xff);

mark()与reset()方法

通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如:

buffer.mark();

//call buffer.get() a couple of times, e.g. during parsing.

buffer.reset();  //set position back to mark.

 

mark

Java NIO 读写文件实例程序

下面的程序实现了一个简单的利用buffer和channel读取数据

package Channel;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class BufferExample {

    public static void main(String[] args) throws IOException {
        Path path = Paths.get("temp.data");
        write(path);
        read(path);
    }

    private static void write(Path path) throws IOException {
        String input = "NIO Buffer Hello World!";
        byte[] inputBytes = input.getBytes();
        ByteBuffer byteBuffer = ByteBuffer.allocate(inputBytes.length);
        byteBuffer.put(inputBytes);
        byteBuffer.flip();
        FileChannel channelWrite = FileChannel.open(path,
                StandardOpenOption.CREATE, StandardOpenOption.WRITE);
        channelWrite.write(byteBuffer);
        channelWrite.close();
    }

    private static void read(Path path) throws IOException {
        FileChannel channelRead = FileChannel.open(path);
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        int readBytes = channelRead.read(byteBuffer);
        if(readBytes > 0) {
            byteBuffer.flip();
            byte[] bytes = new byte[byteBuffer.remaining()];
            byteBuffer.get(bytes);
            String fileContent = new String(bytes, "utf-8");
            System.out.println("File Content: " + fileContent);
        }
        channelRead.close();
    }

}

equals()与compareTo()方法

可以使用equals()和compareTo()方法两个Buffer。

一个临时存放的位置下标。调用 mark() 会将 mark 设为当前的 position
的值,以后调用 reset() 会将 position 属性设置为 mark 的值。 mark
的值总是小于等于 position 的值,如果将 position 的值设的比 mark
小,当前的 mark 值会被抛弃掉。

参考

  • http://javapapers.com/java/java-nio-buffer/
  • http://www.importnew.com/19816.html
  • https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html

equals()

当满足下列条件时,表示两个Buffer相等:

  1. 有相同的类型(byte、char、int等)。
  2. Buffer中剩余的byte、char等的个数相等。
  3. Buffer中所有剩余的byte、char等都相同。

如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。

这些属性总是满足以下条件:

compareTo()方法

compareTo()方法比较两个Buffer的剩余元素(byte、char等),
如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:

  1. 第一个不相等的元素小于另一个Buffer中对应的元素 。
  2. 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。

对缓冲区的读写操作首先要知道缓冲区的下限、上限和当前位置。下面这些变量的值对Buffer类中的某些操作有着至关重要的作用:

  1. limit:所有对Buffer读写操作都会以limit变量的值作为上限。
  2. position:代表对缓冲区进行读写时,当前游标的位置。
  3. capacity:代表缓冲区的最大容量(一般新建一个缓冲区的时候,limit的值和capacity的值默认是相等的)。

flip、rewind、clear这三个方法便是用来设置这些值的。

0 <= mark <= position <= limit <= capacity

clear方法

public final Buffer clear()

{

    position = 0; //重置当前读写位置

    limit = capacity; 

    mark = -1;  //取消标记

    return this;

}

 

clear方法将缓冲区清空,一般是在重新写缓冲区时调用。

缓冲区的内部实现机制:

flip方法

public final Buffer flip() {

limit = position;

position = 0;

mark = -1;

return this;

}

反转缓冲区。首先将限制设置为当前位置,然后将位置设置为
0。如果已定义了标记,则丢弃该标记。
常与compact方法一起使用。通常情况下,在准备从缓冲区中读取数据时调用flip方法。

下面我们就以数据从一个输入通道拷贝到一个输出通道为例,来详细分析每一个变量,并说明它们是如何协同工作的:
初始变量:

rewind方法

 

1public final Buffer rewind() {

2 position = 0;

3 mark = -1;

4 return this;

5}

 

 

 

以上三种方法均使用final修饰,java.nio.Buffer的所有子类均使用同一种flip、clear和rewind机制。

 

我们首先观察一个新创建的缓冲区,以ByteBuffer为例,假设缓冲区的大小为8个字节,ByteBuffer初始状态如下:

图片 15
回想一下
,limit决不能大于capacity,此例中这两个值都被设置为8。我们通过将它们指向数组的尾部之后(第8个槽位)来说明这点。

图片 16
我们再将position设置为0。表示如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入
slot 0。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自slot
0。position设置如下所示:

图片 17
由于缓冲区的最大数据容量capacity不会改变,所以我们在下面的讨论中可以忽略它。

第一次读取:

现在我们可以开始在新创建的缓冲区上进行读/写操作了。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从
position开始的位置,这时position被设置为0。读完之后,position就增加到了3,如下所示,limit没有改变。

图片 18

第二次读取:

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

图片 19

flip:

现在我们要将数据写到输出通道中。在这之前,我们必须调用flip()方法。
其源代码如下:

    public final Buffer flip() 
    {  
        limit = position;  
        position = 0;  
        mark = -1;  
        return this;  
    }

这个方法做两件非常重要的事:

i  它将limit设置为当前position。
ii 它将position设置为0。

上一个图显示了在flip之前缓冲区的情况。下面是在flip之后的缓冲区:

图片 20

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

第一次写入:

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

图片 21

第二次写入:

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

图片 22clear:

最后一步是调用缓冲区的clear()方法。这个方法重设缓冲区以便接收更多的字节。其源代码如下:

    public final Buffer clear() 
    {  
        osition = 0;  
        limit = capacity;  
        mark = -1;  
        return this;  
    }

clear做两种非常重要的事情:

i 它将limit设置为与capacity相同。
ii 它设置position为0。

下图显示了在调用clear()后缓冲区的状态,
此时缓冲区现在可以接收新的数据了。

图片 23

至此,我们只是使用缓冲区将数据从一个通道转移到另一个通道,然而,程序经常需要直接处理数据。例如,您可能需要将用户数据保存到磁盘。在这种情况下,您必须将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。
或者,您可能想要从磁盘读取用户数据。在这种情况下,您要将数据从通道读到缓冲区中,然后检查缓冲区中的数据。实际上,每一个基本类型的缓冲区都为我们提供了直接访问缓冲区中数据的方法,我们以ByteBuffer为例,分析如何使用其提供的get()和put()方法直接访问缓冲区中的数据。

a)    get()

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()方法,这些方法除了不是处理字节外,其它方面是是完全一样的,它们处理的是与该缓冲区类相适应的类型。

注:这里我们着重看一下第二和第三这两个方法

ByteBuffer get( byte dst[] );  
ByteBuffer get( byte dst[], int offset, int length );

这两个get()主要用来进行批量的移动数据,可供从缓冲区到数组进行的数据复制使用。第一种形式只将一个数组
作为参数,将一个缓冲区释放到给定的数组。第二种形式使用 offset 和 length
参数来指
定目标数组的子区间。这些批量移动的合成效果与前文所讨论的循环是相同的,但是这些方法
可能高效得多,因为这种缓冲区实现能够利用本地代码或其他的优化来移动数据。

buffer.get(myArray)    等价于:

buffer.get(myArray,0,myArray.length);

注:如果您所要求的数量的数据不能被传送,那么不会有数据被传递,缓冲区的状态保持不
变,同时抛出 BufferUnderflowException
异常。因此当您传入一个数组并且没有指定长度,您就相当于要求整个数组被填充。如果缓冲区中的数据不够完全填满数组,您会得到一个
异常
。这意味着如果您想将一个小型缓冲区传入一个大数组,您需要明确地指定缓冲区中剩
余的数据长度。上面的第一个例子不会如您第一眼所推出的结论那样,将缓冲区内剩余的数据
元素复制到数组的底部。例如下面的代码:

        String str = "com.xiaoluo.nio.MultipartTransfer";

        ByteBuffer buffer = ByteBuffer.allocate(50);

        for(int i = 0; i < str.length(); i++)
        {
            buffer.put(str.getBytes()[i]);
        }

        buffer.flip();byte[] buffer2 = new byte[100];

        buffer.get(buffer2);

        buffer.get(buffer2, 0, length);

        System.out.println(new String(buffer2));

这里就会抛出java.nio.BufferUnderflowException异常,因为数组希望缓存区的数据能将其填满,如果填不满,就会抛出异常,所以代码应该改成下面这样:

    //得到缓冲区未读数据的长度
        int length = buffer.remaining();

        byte[] buffer2 = new byte[100];

        buffer.get(buffer2, 0, length);

b)    put()

ByteBuffer类中有五个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 );

第一个方法
写入(put)单个字节。第二和第三个方法写入来自一个数组的一组字节。第四个方法将数据从一个给定的源ByteBuffer写入这个
ByteBuffer。第五个方法将字节写入缓冲区中特定的 位置
。那些返回ByteBuffer的方法只是返回调用它们的缓冲区的this值。
与get()方法一样,我们将把put()方法划分为“相对”或者“绝对”的。前四个方法是相对的,而第五个方法是绝对的。上面显示的方法对应于ByteBuffer类。其他类有等价的put()方法,这些方法除了不是处理字节之外,其它方面是完全一样的。它们处理的是与该缓冲区类相适应的类型。

c)    类型化的 get() 和 put() 方法

除了前些小节中描述的get()和put()方法,
ByteBuffer还有用于读写不同类型的值的其他方法,如下所示:

getByte()
getChar()
getShort()
getInt()
getLong()
getFloat()
getDouble()
putByte()
putChar()
putShort()
putInt()
putLong()
putFloat()
putDouble()

事实上,这其中的每个方法都有两种类型:一种是相对的,另一种是绝对的。它们对于读取格式化的二进制数据(如图像文件的头部)很有用。

下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。

        while(true)
        {
            //clear方法重设缓冲区,可以读新内容到buffer里
            buffer.clear();

            int val = inChannel.read(buffer);

            if(val == -1)
            {
                break;
            }

            //flip方法让缓冲区的数据输出到新的通道里面
            buffer.flip();

            outChannel.write(buffer);
        }

read()和write()调用得到了极大的简化,因为许多工作细节都由缓冲区完成了。clear()和flip()方法用于让缓冲区在读和写之间切换。

好了,缓冲区的内容就暂且写到这里,下一篇我们将继续NIO的学习–通道(Channel).

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图