nio 支持面向缓冲区、基于通道(channel-based)的 I/O 操作方法。
JDK9 开始,包 java.nio 及其子包位于 java.base 模块。
nio 子系统不是为了取代面向流的 I/O 类。
NIO 基础
nio 系统构建在缓冲区(buffers)和通道之上。缓冲区用于存放数据,通道表示一个已打开的与 I/O 设备的连接。通过使用缓冲区,按需存入或取出数据。
所有的 buffers 都是 Buffer 类的子类,Buffer 类定义了 buffers 的核心功能:当前位置、limit 和容量。当前位置表示缓冲区中下一个将读取、写入数据的索引位置,limit 表示缓冲区最后一个有效索引的下一个索引位置,容量表示缓冲区可存放的元素数量。通常,limit 和容量相同。Buffer 也支持 mark()
和 reset()
方法。
Buffer 类的子类如下
- ByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- CharBuffer
- FloatBuffer
- DoubleBuffer
- MappedByteBuffer
MappedByteBuffer 类用于将一个文件映射到缓冲区。
上述类都提供了 get()
从缓冲区获取数据,put()
将数据存放到缓冲区。如果缓冲区只读,put()
方法不可用。allocate()
用于手动分配缓冲区,wrap()
将数组包装成缓冲区,slice()
创建缓冲区子序列。
通道表示一个已打开(open connection)的与 I/O 源或目标的连接。通道实现了 Channel 接口,该接口扩展了 Closeable 接口,从而扩展了 AutoCloseable 接口。可以被 try-with-resources 语句管理。
获得通道的一种方式是调用 getChannel()
方法,支持该方法的 I/O 类如下
- DatagramSocket
- FileInputStream
- FileOutputStream
- RandomAccessFile
- ServerSocket
- Socket
调用 getChannel()
返回的信道类型取决于调用对象的类型,比如 FileInputStream、FileOutputStream 和 RandomAccessFile 调用该方法返回 FileChannel 类型,Socket 调用该方法返回 SocketChannel 类型。
获得通道的另一个方法是使用 Files 类提供的静态方法。调用 newByteChannel()
返回字节信道 SeekableByteChannel 类型,SeekableByteChannel 接口由 FileChannel 实现。
信道提供的 read()
和 write()
方法用于执行 I/O 操作。
所有信道提供了访问和控制信道的方法。FileChannel 提供获取、设置当前索引的方法,文件信道之间传输信息,获得当前信道大小,锁信道等方法。FileChannel 提供的静态方法 open()
打开一个文件然后返回关联的信道,提供的 map()
方法将文件映射到缓冲区。
nio 使用的另两个实体是 charsets 和 selectors,charset 定义了字符到字节的映射关系,可以使用编码器将字符编码得到字节,可以使用解码器将字节解码得到字符。
selector 支持基于键的、非阻塞的多 I/O。
NIO.2
Path 接口封装文件路径,它继承的接口有:Watchable、Iterable<Path> 和 Comparable<Path>。其中,Watchable 接口描述监控状态是否发生改变的对象。
Path getName(int index)
方法返回 Path 接口中文件路径中单个路径元素,索引 0 表示根路径,依此类推。int getNameCount()
方法返回 Path 接口中路径元素的个数。Path resolve(Path path)
方法返回 path 的绝对路径。
JDK11 开始,静态工厂方法 of()
接收一个路径名或 URI,返回 Path 对象。
File 对象的 toPath()
方法可以将 File 对象转成 Path 对象,Path 对象的 toFile()
方法可以将 Path 对象转成 File 对象。
Files 类的静态方法用于处理 Path 对象表示的文件。它的 list()
、walk()
、lines()
和 find()
方法返回 Stream 对象。JDK11 开始,引入的 readString()
方法将文件内容以字符串的方式返回,writeString()
方法将 CharSequence 对象写入文件。
Files 类部分方法的参数为 OpenOption 接口,该接口表示以何种方式打开文件。它的一个实现类为 StandardOpenOption 类,定义的枚举值如下
值 | 含义 |
---|---|
APPEND | 在文件末尾开始写入 |
CREATE | 文件不存在时创建 |
CREATE_NEW | 仅当文件不存在时创建 |
DELETE_ON_CLOSE | 文件关闭时删除 |
DSYNC | 改变文件内容时直接写入存储设备 |
READ | 只读 |
SPARSE | 表示文件系统稀疏存储文件 |
SYNC | 改变文件内容或其元数据时直接写入存储设备 |
TRUNCATE_EXISTING | 已存在的文件打开时,清空文件内容 |
WRITE | 写 |
在 JDK11 之前,获得 Path 对象可以通过调用 Paths 类的 get()
方法。
// 拼接所有参数返回对应的 Path 对象
static Path get(String pathName, String... parts);
static Path get(URI uri);
JDK11 引入 Path 的 of()
方法用于获得 Path 对象,上述的 get()
方法在 JDK11 之后不推荐使用。
Path 对象仅表示一个文件的路径,不代表打开或者创建文件。
nio 使用多个接口表示文件属性,处于顶层的接口是 BasicFileAttributes 接口,它有两个子接口:DosFileAttributes 接口表示 FAT 文件系统的文件属性、PosixFileAttributes 接口表示符合 POSIX 标准的文件属性。
Files 类的静态方法 readAttributes()
返回表示文件属性的对象。它的形式之一为
/*
返回的文件属性对象与 path 指定路径的文件关联,具体的文件属性类型由
attrType 决定,取值为 BasicFileAttributes.class、
DosFileAttributes.class 和 PosixFileAttributes.class 三者之一
opts 默认值为符号链接
*/
static <A extends BasicFileAttributes>
A readAttributes(Path path, Class<A> attrType, LinkOption... opts);
Files 类的 getFileAttributeView()
方法返回的对象也可以访问文件属性。nio 定义的属性视图接口包括:AttributeView、BasicFileAttributeView、DosFileAttributeView 和 PosixFileAttributeView 接口等。
Files 类的部分静态方法也可以访问文件属性。
不是所有的文件系统支持全部的文件属性。
使用 FileSystem 和 FileSystems 类可以访问文件系统,FileSystems 类的 newFileSystem()
返回一个新的文件系统。FileStore 类封装文件存储系统。
NIO 系统的使用
nio 的使用划分为 3 个类别
- 基于 channel 的 I/O
- 基于流的 I/O
- 路径和文件系统操作
基于 channel 的 I/O
调用 Files.newByteChannel(Path path)
返回一个与 path 指定文件关联的 SeekableByteChannel 接口对象,默认情况下该对象的实际类型为实现了 SeekableByteChannel 接口的 FileChannel 类,因此可以转换为该类类型。SeekableByteChannel 接口封装了用于文件操作的 channel。
// path 表示文件, how 表示打开文件的方式,默认情况下以只读的方式打开文件
static SeekableByteChannel newByteChannel(Path path, OpenOption... how)
channel 的使用需要一个缓冲区,要么通过将一个已有数组包装为缓冲区要么动态分配一个缓冲区。ByteBuffer 的 allocate()
方法获得一个缓冲区。
static ByteBuffer allocate(int capacity)
channel 调用 read()
方法从文件中读取数据填入缓冲区。
// 返回实际读取的字节个数,读取文件末尾时返回 -1
int read(ByteBuffer buf)
public static void m() {
int count = -1;
// 获取与文件关联的 channel
try (SeekableByteChannel fC = Files.newByteChannel(Path.of("test.txt"))) {
// 获取缓冲区
var buf = ByteBuffer.allocate(128);
do {
// channel 从文件读取数据直到填满缓冲区
count = fC.read(buf);
// 将缓冲区指针从末尾回退到起始位置
buf.rewind();
for (int i = 0; i < count; i++) {
System.out.print((char)buf.get());
}
} while (count != -1);
System.out.println();
}
}
使用 channel 的另一种方式是将文件内容映射到一个缓冲区,然后使用这个缓冲区即可达到使用文件的目的。
该方式的步骤和前一种方法不同在于,newByteChannel()
方法返回的对象需要转换成 FileChannel 类,然后调用该类的 map()
方法达到映射的目的。
// start 表示从文件中开始映射的位置,size 表示映射的字节数量
MappedByteBuffer map(FileChannel.MapMode how, long start, long size)
how 的取值有如下三种
- MapMode.READ_ONLY 只读
- MapMode.READ_WRITE 读写
- MapMode.PRIVATE 将文件内容私有复制到缓冲区,缓冲区内容的改变不会影响文件的内容
map()
方法返回的 MappedByteBuffer 类是 ByteBuffer 的子类。
public static void m() {
try(var fC = (FileChannel)Files.newByteChannel(Path.of("test.txt"))) {
// 获取关联的文件字符个数
long size = fC.size();
var buf = fC.map(FileChannel.MapMode.READ_ONLY, 0, size);
for (int i = 0; i < size; i++) {
System.out.print((char)buf.get());
}
System.out.println();
}
}
通过 channel 将数据写入文件和从文件读取数据有一些不同之处。调用 newByteChannel(Path path, OpenOption... how)
方法时将 how 指定为 StandardOpenOption.WRITE 表示以写入的方式打开文件;指定为 StandardOpenOption.CREATE 表示当文件不存在时创建文件。newByteChannel()
方法返回的对象需要转换成 FileChannel 类型,然后调用 channel 的 write()
方法。该方法接收一个缓冲区,将缓冲区中的内容写入到与 channel 关联的文件中。
public static void m() {
try (var fC = (FileChannel)Files.newByteChannel(Path.of("test.txt"),
StandardOpenOption.WRITE,
StandardOpenOption.CREATE)) {
var buf = ByteBuffer.allocate(26);
for (int i = 0; i < 26; i++) {
buf.put((byte)('a' + i));
}
buf.rewind();
// 关联的文件已存在时,文件的前 26 个字节被覆盖重写,其他字节不变
fC.write(buf);
}
}
注意:每次操作缓冲区时,不论读还是写,当前缓冲区的指针都会移动。
将 channel 关联的文件与缓冲区映射后,缓冲区中的内容会自动写入到文件中。
public static void m() {
try (var fC = (FileChannel)Files.newByteChannel(Path.of("test.txt"),
StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
StandardOpenOption.READ)) {
// 必须以读写的方式映射才能写入
var buf = fC.map(FileChannel.MapMode.READ_WRITE, 0, 26);
for (int i = 0; i < 26; i++) {
buf.put((byte)('a' + i));
}
}
}
Files 类的 copy()
方法用于复制文件。
static Path copy(Path src, Path dest, CopyOption... how)
其中,how 决定了文件该如何复制。下面 how 的三种取值对于任何文件系统都适用
- StandardCopyOption.COPY_ATTRIBUTES 文件的属性也一起复制
- StandardLinkOption.NOFLLOW_LINKS 不使用符号链接
- StandardCopyOption.REPLACE_EXISTING 重写已存在的文件
public static void m(String[] args) {
var source = Path.of(args[0]);
var target = Path.of(args[1]);
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
}
基于流的 I/O
Files 类的 newInputStream()
和 newOutputStream()
方法接收一个 Path 对象,返回与该对象表示的文件关联的流。返回的流具有 nio 提供的特性。
static InputStream newInputStream(Path path, OpenOption... how)
how 的默认取值是 StandardOpenOption.READ。
public static void m() {
try (var is = Files.newInputStream(Path.of("test.txt"))) {
int i = -1;
do {
i = is.read();
if (i != -1) {
System.out.print((char)i)
}
} while (i != -1);
}
}
static InputStream newOutputStream(Path path, OpenOption... how)
how 的默认取值为 StandardOpenOption.WRITE、StandardOpenOption.CREATE 和 StandardOpenOption.TRUNCATE_EXISTING。
public static void m() {
try (var bos = new BufferedOutputStream(new OutputStream(Path.of("test.txt")))) {
for (int i = 0; i < 26; i++) {
bos.wirte((byte)('a' + i));
}
}
}
路径和文件系统操作
Path 对象的部分方法可以获取文件的路径相关属性、Files 类的部分静态方法接收一个 Path 类型参数,获取该 path 表示的文件属性。Files.readAttributes(Path)
方法返回一个 BasicFileAttributes 对象,该对象可以获取 path 表示的文件属性。
Files 类的静态方法可以读取表示目录的 Path 对象的目录内容。
static DirectoryStream<Path> newDirectoryStream(Path dir)
dir 表示目录时,newDirectoryStream()
方法返回的 DirectoryStream<Path> 对象用于获取目录中的内容。DirectoryStream<Path> 实现了 AutoCloseable 和 Iterable<Path> 接口。DirectoryStream<Path> 实现的迭代器仅能使用一次。
public static void m() {
try (var ds = Files.newDirectoryStream(Path.of("/dir"))) {
for (Path e : ds) {
var attr = Files.readAttributes(e, BasicFileAttributes.class);
if (attr.isDirectory()) {
System.out.print("dir: ");
} else {
System.out.print(" ");
}
System.out.println(e.getName(1));
}
}
}
过滤目录中的内容有两种方式。第一种
static DirectoryStream<Path> newDirectoryStream(Path dir, String wildcard)
根据 wildcard 表示的文件名或者一个通配符来过滤目录中的内容。
// 保留 dir 目录中以 a 或 b 开头,以 java 或 c 为后缀的文件
Files.newDirectoryStream(Path.of("/dir"), "{a,b}*.{java, c}")
第二种
static DirectoryStream<Path>
newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter)
DirectoryStream.Filter 接口有 boolean accept(T e)
方法,方法返回 true 则保留内容,返回 false 则去除。
public static void m() {
var how = new DirectoryStream.Filter<Path>() {
// 过滤掉不可写的文件
public boolean accept(Path name) throws IOException {
if (Files.isWritable(name)) {
return true;
}
return false;
}
};
try (var ds = Files.newDirectoryStream(Path.of("/dir"), how)) {
for (Path e : ds) {
System.out.println(e.getName(1));
}
}
}
Filter 类的 walkFileTree()
方法用于遍历一个目录中所有的文件和子目录及子目录中的文件和目录。之前只遍历一层目录。
static Path walkFileTree(Path root, FileVisitor<? super Path> fv)
fv 是 FileVisitor 接口类型对象,控制目录树如何遍历。
interface FileVisitor<T>
该接口有如下方法
FileVisitResult postVisitDirectory(T dir, IOException e)
访问目录后调用FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
访问目录前调用FileVisitResult visitFile(T file, BasicFileAttributes attrs)
访问文件时调用FileVisitResult visitFileFailed(T dir, IOException e)
访问文件失败时调用
上述方法返回 FileVisitResult 枚举类型,它定义了四个枚举变量
- CONTINUE 返回该值表示继续遍历
- SKIP_SIBLINGS 在
preVisitDirectory()
方法中返回时跳过该目录的兄弟节点,阻止postVisitDirectory()
方法的调用 - SKIP_SUBTREE 在
preVisitDirectory()
方法中返回时仅跳过目录和子目录 - TERMINATE 返回时停止遍历
SimpleFileVisitor 类实现了 FileVisitor 接口,自定义遍历时进行操作只需重写该类的特定方法。
class AFV extends SimpleFileVisitor<Path> {
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
throws IOException {
System.out.println(path);
return FileVisitResult.CONTINUE;
}
}
class Demo {
public static void main(String[] args) {
Files.walkFileTree(Path.of("/dir"), new AFV());
}
}
参考
[1] Herbert Schildt, Java The Complete Reference 11th, 2019.