1. 简介
在本文内容中,我们将介绍 Java IO 功能以及它们在不同 Java 版本中的变化。首先,我们将介绍初始 Java 版本中的java.io包。接下来,我们将介绍Java 1.4 中引入的java.nio包。最后,我们将介绍java.nio.file包,通常称为 NIO.2 包。
2. Java NIO 包
[Java 的第一个版本随java.io包]一起发布,引入了File类来访问文件系统。File**类表示文件和目录,并提供对文件系统的有限操作可以创建和删除文件、检查文件是否存在、检查读/写访问权限等。
它也存在一些缺点:
- 缺少复制方法——要复制文件,我们需要创建两个File实例并使用缓冲区从一个 File 实例读取并写入另一个File实例。
- 糟糕的错误处理 ——一些方法返回布尔值作为操作是否成功指示。
- 有限的文件属性集- 名称、路径、读/写权限、内存大小等等。
- 阻塞 API—— 我们的线程被阻塞,直到 IO 操作完成。
要读取文件,我们需要一个FileInputStream实例来从文件中读取字节:
@Test
public void readFromFileUsingFileIO() throws Exception {
File file = new File("src/test/resources/nio-vs-nio2.txt");
FileInputStream in = new FileInputStream(file);
StringBuilder content = new StringBuilder();
int data = in.read();
while (data != -1) {
content.append((char) data);
data = in.read();
}
in.close();
assertThat(content.toString()).isEqualTo("Hello from file!");
}
接下来,Java 1.4 引入了捆绑在java.nio包中的非阻塞 IO API (nio 代表新 IO)。引入 NIO 是为了克服java.io包的限制。该包引入了三个核心类:Channel、Buffer和Selector。
2.1.渠道
[Java NIO Channel]是一个允许我们读写缓冲区的类。 Channel类类似于Streams(这里我们说的是 IO Streams,而不是 Java 1.8 Streams),但有几个不同之处。 Channel是双向的,而Streams通常是单向的,它们可以异步读写。
Channel类有几个实现,包括用于文件系统读/写的FileChannel 、用于使用 UDP 通过网络读/写的[DatagramChannel ]和用于使用 TCP 通过网络读/写的SocketChannel 。
2.2.缓冲区
Buffer 是一块内存,我们可以从中读取或写入数据。NIO Buffer对象包装了一个内存块。Buffer类提供了一组与内存块配合使用的功能。要使用Buffer对象,我们需要了解Buffer类的三个主要属性:容量、位置和限制。****
- 容量定义了内存块的大小,当我们往缓冲区写入数据时,只能写入有限长度,当缓冲区满了,我们需要读取数据或者清除它。
- 位置是我们写入数据的起点。空缓冲区从 0 开始,直到容量 - 1。 此外,当我们读取数据时,我们从位置值开始。
- 限制表示我们如何从缓冲区写入和读取。
Buffer类有多种变体。每种基本 Java 类型各一个,不包括布尔类型和[MappedByteBuffer]。
要使用缓冲区,我们需要了解一些重要的方法:
- *allocate(int value) –*我们使用此方法来创建一定大小的缓冲区。
- flip() – 此方法用于从写入模式切换到读取模式
- *clear() –*清除缓冲区内容的方法
- *compact() –*仅清除已读内容的方法
- *rewind() –*将位置重置回 0,以便我们可以重新读取缓冲区中的数据
使用前面描述的概念,让我们使用Channel和Buffer类从文件中读取内容:
@Test
public void readFromFileUsingFileChannel() throws Exception {
RandomAccessFile file = new RandomAccessFile("src/test/resources/nio-vs-nio2.txt", "r");
FileChannel channel = file.getChannel();
StringBuilder content = new StringBuilder();
ByteBuffer buffer = ByteBuffer.allocate(256);
int bytesRead = channel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
content.append((char) buffer.get());
}
buffer.clear();
bytesRead = channel.read(buffer);
}
file.close();
assertThat(content.toString()).isEqualTo("Hello from file!");
}
初始化所有必需的对象后,我们从通道读取数据到缓冲区。接下来,在 while 循环中,我们使用flip() 方法标记要读取的缓冲区,然后一次读取一个字节,并将其附加到结果中。最后,我们清除数据并读取另一批数据。
2.3.选择器
[Java NIO Selector]允许我们使用单个线程管理多个通道。 要使用选择器对象监视多个通道,每个通道实例必须处于非阻塞模式,并且我们必须注册它。通道注册后,我们将获得一个代表通道和选择器之间连接的SelectionKey对象。当我们有多个通道连接到选择器时,我们可以使用select() 方法检查有多少个通道可供使用。调用select() 方法后,我们可以使用selectedKeys() 方法获取所有就绪通道。
2.4. NIO包的缺点
java.nio包引入的更改更多地与低级数据 IO 相关。虽然它们允许非阻塞 API,但其他方面仍然存在问题:
- 对符号链接的支持有限
- 对文件属性访问的有限支持
- 缺少更好的文件系统管理工具
3. Java NIO.2 包
Java 1.7 引入了新的java.nio.file包,也称为[NIO.2 包。此包采用异步方法实现]java.nio包中不支持的非阻塞 IO 。最重要的变化与高级文件操作有关。它们与Files、Path和Paths类一起添加。最值得注意的低级变化是添加了[AsynchroniousFileChannel]和AsyncroniousSocketChannel。
Path对象表示由分隔符分隔的目录和文件名的层次结构序列。根组件位于最左侧,而文件位于右侧。此类提供实用方法,例如getFileName() 、 getParent() 等。Path类还提供resolve和relativize方法,可帮助构建不同文件之间的路径。Paths 类是一组静态实用方法,它们接收String 或URI来创建Path实例。**********
Files类提供了使用前面描述的Path类并对文件、目录和符号链接进行操作的实用方法。它还提供了一种使用 readAttributes() 方法读取许多文件属性的方法。
最后,让我们看看 NIO.2 在读取文件时与以前的 IO 版本相比如何:
@Test
public void readFromFileUsingNIO2() throws Exception {
List<String> strings = Files.readAllLines(Paths.get("src/test/resources/nio-vs-nio2.txt"));
assertThat(strings.get(0)).isEqualTo("Hello from file!");
}
4. 结论
在本文中,我们介绍了java.nio和java.nio.file包的基础知识。我们可以看到,NIO.2 并不是 NIO 包的新版本。NIO 包引入了用于非阻塞 IO 的低级 API,而 NIO.2 引入了更好的文件管理。这两个包并不是同义词,而是互相补充。