Java NIO(New IO)是 Java 的替代 IO API(来自 Java 1.4),意味着替代标准 Java IO 和 Java Networking API。 Java NIO 提供了与标准 IO API 不同的使用 IO 的方式。

概述

Java NIO 主要有以下三个核心组件:

  • Channels
  • Buffers
  • Selectors

除此之外,Java NIO 还有许多类和组件,但是 NIO 的核心 API 主要还是由 channel,bufferselector 构成。其余的组件,如 PipeFileLock,只是与这三个核心组件一起使用的实用程序类,本篇文章先来来介绍 Channel。

通常,NIO 中的所有 IO 都以 Channel 开始,Channel(通道)有点像 Stream(流),但是它又有一些区别:

  • 你既可以从 Channle 中读取数据,也可以向其写入数据。而 Stream 通常只能单向操作,要么读,要么写;
  • Channel 支持异步读取或写入数据;
  • 数据从 Channel 中读取到 Buffer 中,从 Buffer 写入到 Channel 中。

示意图如下:

channels-buffers

Channel 实现类

以下是 Java NIO 中最重要的 Channel 实现类:

  • FileChannel:从文件中读取数据,或写入数据到文件中
  • DatagramChannel:可以通过 UDP 协议在网络上读写数据。
  • SocketChannel:可以通过 TCP 协议在网络上读写数据。
  • ServerSocketChannel:监听传入的 TCP 连接,就像 Web 服务器一样。 对于每个传入连接,都会创建一个 SocketChannel。

本篇文章先来介绍 FileChannel,其余的后面再来介绍。

FileChannel

Java NIO FileChannel 是一个与文件相连接的通道。通过 FileChannel 你可以从文件中读取数据,以及写入数据到文件中。FileChannel 可以看作是标准 Java IO API 的替代方案。

FileChannel 只能以阻塞模式来使用,不能使用非阻塞模式。

Opening a FileChannel

在使用 FileChannel 之前,你必须先打开它。你不能直接打开一个 FileChannel,你需要通过 InputStream,OutputStream 或 RandomAccessFile 来获取 FileChannel,调用 getChannel()

代码示例:

1
2
3
4
5
6
7
8
FileOutputStream outputStream = new FileOutputStream("file.txt");
FileChannel fileChannel = outputStream.getChannel();

FileInputStream inputStream = new FileInputStream("file.txt");
FileChannel fileChannel = inputStream.getChannel();

RandomAccessFile accessFile = new RandomAccessFile("file.txt", "rw");
FileChannel fileChannel = accessFile.getChannel();

Reading Data from a FileChannel

调用 read() 方法可以从 FileChannel 中读取相应的数据。例如:

1
2
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = fileChannel.read(buf);

首先需要先分配一个 Buffer,然后调用 read () 方法 ,从 Channel 中读取数据到 Buffer 中。

返回的 int 数据,告诉我们有多少数据已被写入到了 buffer 中,如果返回 -1 ,则表示到达了 EOF (end-of-file)。

Writing Data to a FileChannel

写入数据需要调用 FileChannel.write () 方法,例如:

1
2
3
4
5
6
7
8
9
10
11
12
String newData = "New String to wirte to file...." + System.currentTimeMillis();

ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.clear();
buffer.put(newData.getBytes());
buffer.flip();

RandomAccessFile accessFile = new RandomAccessFile("file.txt", "rw");
FileChannel fileChannel = accessFile.getChannel();
while (buffer.hasRemaining()) {
fileChannel.write(buffer);
}

由于没法保证有多少 bytes 将会被写入到 FileChannel 中,因此需要将 FileChannle.write () 方法在 while 循环中调用,指导 Buffer 中没有可写入的数据为止。

Closing a FileChannel

用完 FileChannel 之后,必须将其关闭。

1
channel.close();    

FileChannel Position

在读取或写入 FileChannel 时,你可以在特定位置执行此操作。

可以通过调用 position() 方法获取 FileChannel 对象的当前位置。也可以通过调用 position(long pos) 方法来设置 FileChannel 的位置。示例:

1
2
3
4
long pos = fileChannel.position();

long newPos = 12;
fileChannel.position(newPos);

如果你在文件 EOF 之后设置 position,并尝试从 Channel 中读取数据,将会返回 -1 的结束标志。

如果你在文件 EOF 之后设置 position,并尝试写入数据到 Channel 中,文件将会扩展已符合设置的 position,并容纳新的字节。这可能会导致 “文件漏洞”,磁盘上的物理文件在写入数据中会存在间隙。

FileChannel Size

方法 size() 返回 channel 所连接的文件的字节大小,例如:

1
long fileSize = channel.size();    

FileChannel Truncate

通过 FileChannel.truncate() 方法,你可以截取文件,例如截取某个 FileChannel 1024 个字节

1
channel.truncate(1024);

FileChannel Force

FileChannel.force() 方法会将所有未写入的数据强制从 channel 刷到磁盘上。出于性能原因,操作系统可能会将数据缓存在内存中,因此在调用 force () 方法之前,无法保证写入 Channel 的数据是否写入到了磁盘中。

force() 有个 boolean 参数,表示是否需要刷新文件的 mete data(元数据)。

1
channel.force(true)

Channel to Channel Transfers

你可以使用 Java NIO 将数据从一个 Channel 直接转移到另一个 Channel 上。假如这个 Channel 是 FileChannel,那么你可以通过 transferTo () 和 transferFrom () 来实现。

channel-transfer

transferFrom()

FileChannel.transferFrom () :将数据从源 Channel 转移到目标 Channel 中。代码如下:

1
2
3
4
5
6
7
8
9
10
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();

long position = 0;
long count = fromChannel.size();

toChannel.transferFrom(fromChannel, position, count);

position:指定数据传输的起始位置。

count:指定最大的传输的字节数。

transferTo()

1
2
3
4
5
6
7
8
9
10
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();

long position = 0;
long count = fromChannel.size();

fromChannel.transferTo(position,count,toChannel);

参考资料