和低效 IO 说再见,回头补一波 Java 7 的 NIO.2 特性
其实在这之前已经写过一篇关于 Java 7 的新特性文章了,那篇文章主要介绍了 Java 7 的资源自动关闭、Switch String 实现原理、异常捕获 try-catch、新的二进制书写方式等,具体的内容也可以看下当初的这篇文章(补一波 Java 7 语法特性)。而在那篇文章里唯独没有介绍到 Java 7 中对于 IO 操作的更新,而这部分恰好又是非常重要的一部分,该还的总是要还的,现在补上。
看完这篇文章你会了解到:
- 抽象文件路径操作方式,直观方便少 BUG。
- 高效的文件操作方式,写入读取复制文件仅需一行。
- 快速获取不同系统下的文件属性。
- 遍历目录下文件和目录的多种方式,且十分高效。
- 反应式事件通知,监测文件变化。
在 Java 7 中,加强了文件操作相关功能,也就是新的 java.nio.file 包里的内容,它提供了诸如文件路径抽象、文件目录流、目录树、文件属性和变化监视服务等功能,可以大幅度提高我们对于文件的操作。

文件路径
在 Java 7 之前对文件路径的操作都是以字符串的操作,使用时你需要把一个字符串直接扔进去,直接使用字符串操作是低效的,比如你要拼接父路径和子目录,你只能进行字符串的拼接。而且拼接这个本身操作就丢失了它作为文件路径的含义。另外使用字符串进行各种路径操作很有可能由于拼写错误而出现各种问题。
Java 7 的到来让这一切变的不一样了,它提供了 Path 接口用来表示路径的抽象,然后提供了一系列对于路径的操作方法,让这一切变得如此简单。
为了方便的创建 Path 对象,又提供了Paths 工具类,如何使用让我们先睹为快。
一切都从 Path path = Paths.get("/Users/darcy/java/"); 获取一个 Path 对象开始。
Path path = Paths.get("/Users/darcy/java/");
System.out.println("完整路径:" + path.toString());
Path pathParent = path.getParent();
System.out.println("父级路径:" + pathParent.toString());
Path pathRoot = path.getRoot();
System.out.println("根目录:" + pathRoot.toString());
int pathNameCount = path.getNameCount();
System.out.println("目录深度:" + pathNameCount);
Path pathIndex3 = path.getName(2);
System.out.println("第三级目录:" + pathIndex3);
Path subPath = path.subpath(1, 3);
System.out.println("第1级目录到第三级目录(包左不包右):" + subPath.toString());
// resolveSibling 从当前目录父目录开始拼接目录
Path pathResolveSibling = path.resolveSibling("PathDemo.java");
System.out.println("父目录开始拼接参数:" + pathResolveSibling.toString());
// resolve 把当前路径当作父路径,参数作为子目录或者文件
Path pathResolve = Paths.get("/Users/darcy/java/").resolve("PathDem.java");
System.out.println("当前目录拼接后的目录:" + pathResolve.toString());
// 参数路径相对于主体路径的相对路径
Path path1 = Paths.get("/Users/darcy/");
Path path2 = Paths.get("/Users/darcy/java/PathDemo.java");
Path path3 = path1.relativize(path2);
System.out.println("相对路径:" + path3.toString());
/* 输出结果
完整路径:/Users/darcy/java
父级路径:/Users/darcy
根目录:/
目录深度:3
第三级目录:java
第1级目录到第三级目录(包左不包右):darcy/java
父目录开始拼接参数:/Users/darcy/PathDemo.java
当前目录拼接后的目录:/Users/darcy/java/PathDem.java
相对路径:java/PathDemo.java
*/
可以看到上面代码里除了创建 Path 对象时输入了一次路径,后续的操作都是使用 Path 中的方法进行操作的,在此之前你可能需要各种字符串截取拼接,十分繁琐。
文件操作
还记得初学 Java IO 时,文件复制有多种写法,但是不管是哪一种,写起来都需要不少的代码,而且还需要考虑复制时的性能。读取文件那就更不用说了,定义各种读取和接收变量,各种验证。现在不一样了,不仅文件操作非常方便,而且像文件复制和读取等常用操作都可以一行搞定。
使用过于简单,直接代码。
// 如果文件不存在,则创建一个文件
Path path = Paths.get("test.txt");
Path pathBackup = Paths.get("test_bak.txt");
Path pathLink = Paths.get("test.txt.link");
Path pathDir = Paths.get("dir");
// 已存在则删除
Files.deleteIfExists(path);
Files.deleteIfExists(pathBackup);
Files.deleteIfExists(pathLink);
Files.deleteIfExists(pathDir);
// 创建文件写入内容
Path file = Files.createFile(path);
Files.write(path, "关注公众号:未读代码".getBytes());
Files.write(path, System.lineSeparator().getBytes(), StandardOpenOption.APPEND);
Files.write(path, "欢迎加我微信:wn8398".getBytes(), StandardOpenOption.APPEND);
System.out.println("创建文件:" + file.toString());
// 创建文件链接
pathLink = Files.createLink(pathLink, path);
System.out.println("创建文件:" + pathLink.toString());
// 创建目录
Path directory = Files.createDirectory(pathDir);
System.out.println("创建目录:" + directory.toString());
// 文件复制
Files.copy(path, pathBackup);
System.out.println("复制文件: " + path + " --> " + pathBackup);
// 读取文件
List<String> lines = Files.readAllLines(pathBackup);
for (String line : lines) {
System.out.println("文件读取:" + line);
}
上面展示了 Files 类的文件创建、删除、写入、拷贝、读取的写法,都是只有一行代码。
文件属性
和路径操作类似,Java 7 也提供了文件属性的抽象,增加了一系列文件属性的操作工具类。这部分代码在 java.nio.file.attribute 包内。它抽象出了一个 AttributeView 作为所有属性视图的父接口,然后用它的子类 Fi leAttributeView 表示文件视图,用子类 FileOwnerAttributeView 表示文件所有者的属性视图。前者属性如文件的创建时间、修改时间、是否目录等信息,后者则包含文件的相关信息。为了兼容不同的操作系统,Java 7 还提供了不同实现,如 DosFileAttributeView 视图,很明显他是为 Windows 操作系统准备的。

使用起来过于简单,直接代码奉上。
Path path = Paths.get("/Users/darcy/git/jdk-feature/README.md");
BasicFileAttributeView fileAttributeView = Files.getFileAttributeView(path, BasicFileAttributeView.class);
BasicFileAttributes basicFileAttributes = fileAttributeView.readAttributes();
FileTime creationTime = basicFileAttributes.creationTime();
FileTime lastModifiedTime = basicFileAttributes.lastModifiedTime();
FileTime lastAccessTime = basicFileAttributes.lastAccessTime();
System.out.println("创建时间:" + creationTime);
System.out.println("上次修改时间:" + lastModifiedTime);
System.out.println("上次访问时间:" + lastAccessTime);
boolean directory = basicFileAttributes.isDirectory();
boolean regularFile = basicFileAttributes.isRegularFile();
boolean symbolicLink = basicFileAttributes.isSymbolicLink();
System.out.println("是否目录:" + directory);
System.out.println("是否普通文件:" + regularFile);
System.out.println("是否符号链接:" + symbolicLink);
long size = basicFileAttributes.size();
System.out.println("文件大小:" + size);
PosixFileAttributeView linuxFileAttributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class);
UserPrincipal owner = linuxFileAttributeView.getOwner();
System.out.println("文件归属用户:" + owner.getName());
示例代码运行后得到如下输出。
创建时间:2020-09-06T13:35:14Z
上次修改时间:2020-09-06T13:35:14.649261371Z
上次访问时间:2020-09-06T13:35:14.680968254Z
是否目录:false
是否普通文件:true
是否符号链接:false
文件大小:3636
文件归属用户:darcy
文件列表流
在 Java 7 之前遍历文件目录和文件,你应该会选择 File 类的 listFiles 方法。
// 文件直接遍历,不会遍历子目录
String pathString = "/Users/darcy/project/mylab/src/main/java/com/wdbyte/java";
File file = new File(pathString);
File[] listFiles = file.listFiles();
for (File tempFile : listFiles) {
System.out.println("file list: " + tempFile.getAbsolutePath());
}
这种遍历方式看起来也是十分优雅的,可是这种方式在面对大量文件时,效率会变的很低。所以 Java 7 也对此进行了改进,引入了 DirectoryStream 文件列表流。它可以进行渐进式的文件遍历,每次读取一定数量,降低遍历时的性能开销,但是 DirectoryStream 遍历时只会遍历它的直接目录和文件,不会递归的遍历子目录。下面是它的遍历写法。
String pathString = "/Users/darcy/project/mylab/src/main/java/com/wdbyte/java";
// Path 直接遍历方式,不会遍历子目录
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(Paths.get(pathString))) {
for (Path pathTemp : directoryStream) {
System.out.println("DirectoryStream: " + pathTemp);
}
}
// Path 直接遍历方式 - 筛选 .class 文件
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(Paths.get(pathString), "*.java")) {
for (Path pathTemp : directoryStream) {
System.out.println("DirectoryStream file type is class : " + pathTemp);
}
}
这里扩展一下,在 Java 8 中对 Files 类进行了增强,引入了 Java 8 的 Lambda 表达式,增加了 walk 方法,遍历文件也有异曲同工之妙(下面的例子中用到了 Lambda 表达式)。
// 遍历所有目录和子目录
Stream<Path> pathStream = Files.walk(Paths.get("/Users/darcy/project/mylab/src/main/java/com/wdbyte"));
pathStream.forEach(pathTemp -> {
System.out.println("Stream: " + pathTemp.toString());
});
// 遍历所有目录和子目录 - 筛选 java 文件
pathStream = Files.walk(Paths.get("/Users/darcy/project/mylab/src/main/java/com/wdbyte"));
pathStream
.filter(pathTemp -> pathTemp.toString().endsWith(".java"))
.forEach(pathTemp -> {
System.out.println("Stream filter java: " + pathTemp.toString());
});
文件监视
文件监视,也就是可以动态的监测指定目录的文件或者内容的变化,应用场景很多,比如热部署时检查 class 文件是否更新,或者每当有文件进来时就进行操作。在这之前你只能通过循环调用 listFiles 并与上次的调用结果对比才可以找到文件的变化,而现在可以通过通知的方式进行反应式的逻辑处理,一切变的简单了。
被监视的对象要实现 Watchable 接口,然后通过 register 方法注册到监视服务 WatchService 接口的实现,同时指定要监视的事件类型。
// 创建
StandardWatchEventKinds.ENTRY_CREATE,
// 删除
StandardWatchEventKinds.ENTRY_DELETE,
// 更新
StandardWatchEventKinds.ENTRY_MODIFY
具体怎么使用呢?通过下面这个例子看下代码如何实现,下面的代码对文件夹 /Users/darcy/test 进行监测,注册的感兴趣事件是创建、删除、更新操作。
WatchService watchService = FileSystems.getDefault().newWatchService();
Path path = Paths.get("/Users/darcy/test");
path.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
while (true) {
WatchKey watchKey = watchService.take();
// 获取事件类型
for (WatchEvent<?> pollEvent : watchKey.pollEvents()) {
// 具体的事件上下文信息
Path tempPath = (Path)pollEvent.context();
Kind<?> kind = pollEvent.kind();
if (kind.name().equals(StandardWatchEventKinds.ENTRY_CREATE.name())) {
System.out.println("创建了一个文件:" + tempPath.toString());
}
if (kind.name().equals(StandardWatchEventKinds.ENTRY_DELETE.name())) {
System.out.println("删除了一个文件:" + tempPath.toString());
}
if (kind.name().equals(StandardWatchEventKinds.ENTRY_MODIFY.name())) {
System.out.println("修改了一个文件:" + tempPath.toString());
}
}
// 事件处理完毕后要进行 reset 才能继续监听事件
watchKey.reset();
// 取消监视
// watchKey.cancel();
}
注册事件监听后,通过一个循环,调用 take() 方法获取事件结果,得到事件后再判断事件类型进行日志输出。我启动后进行了简单测试,下面是日志输出。
# 下面是我的操作
➜ test pwd
/Users/darcy/test
➜ test touch test.txt # 创建文件
➜ test vim test.txt # 修改文件
➜ test rm test.txt # 删除文件
# 得到的日志输出
创建了一个文件:test.txt
创建了一个文件:.test.txt.swp
修改了一个文件:test.txt
删除了一个文件:.test.txt.swp
删除了一个文件:test.txt
因为使用 vim 编辑,所以有临时的 swp 文件生成和自动删除,也被监测到了。
往期 Java 新特性系列文章:
最后的话
文章已经收录在 Github.com/niumoo/JavaNotes ,欢迎Star和指教。更有一线大厂面试点,Java程序员需要掌握的核心知识等文章,也整理了很多我的文字,欢迎 Star 和完善,希望我们一起变得优秀。
文章有帮助可以点个「赞」或「分享」,都是支持,我都喜欢!
文章每周持续更新,要实时关注我更新的文章以及分享的干货,可以关注「 未读代码 」公众号或者我的博客。

和低效 IO 说再见,回头补一波 Java 7 的 NIO.2 特性的更多相关文章
- 还看不懂同事代码?快来补一波 Java 7 语法特性
前言 Java 平台自出现到目前为止,已经 20 多个年头了,这 20 多年间 Java 也一直作为最流行的程序设计语言之一,不断面临着其他新兴编程语言的挑战与冲击.Java 语言是一种静态强类型语言 ...
- Java中的NIO及IO
1.概述 Java NIO(New IO) 是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API.NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同, ...
- JAVA中的NIO (New IO)
简介 标准的IO是基于字节流和字符流进行操作的,而JAVA中的NIO是基于Channel和Buffer进行操作的. 传统IO graph TB; 字节流 --> InputStream; 字节流 ...
- java中的NIO和IO到底是什么区别?20个问题告诉你答案
摘要:NIO即New IO,这个库是在JDK1.4中才引入的.NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多. 本文分享自华为云社区<jav ...
- Java中的NIO和IO的对比分析
总的来说,java中的IO和NIO主要有三点区别: IO NIO 面向流 面向缓冲 阻塞IO 非阻塞IO 无 选择器(Selectors) 1.面向流与面向缓冲 Java NIO和IO之间第一个最大的 ...
- Java IO编程全解(一)——Java的I/O演进之路
转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7419117.html JDK1.4之前的早期版本,Java对I/O的支持并不完善,开发人员在开发高性能I/O ...
- 【java】io流之字符输入流:java.io.Reader类及子类的子类java.io.FileReader
package 文件操作; import java.io.File; import java.io.FileReader; import java.io.IOException; import jav ...
- 【java】io流之字符输出流:java.io.Writer类及子类的子类java.io.FileWriter
package 文件操作; import java.io.File; import java.io.FileWriter; import java.io.IOException; import jav ...
- java非阻塞NIO和阻塞IO
1 非阻塞NIO和阻塞IO 1.1 定义 阻塞IO:线程被阻塞,去处理一个读取和写入,中间如果有等待时间,则线程被占用,也不能处理其他任务: 非阻塞IO(new I ...
随机推荐
- [状压DP]P1441 题解 砝码称重
前置知识:状压DP 洛谷传送门 emm....看到题目,我第一个想到的就是枚举.暴力大法好! 具体怎么枚举?当然是子集枚举啦!枚举出每一个可能的砝码选择方案.对于每一个合法的(也就是选取数量等于\(n ...
- 使用 Python破解大众点评字体加密(SVG反扒)
前言 大众点评拥有大量高质量评论信息.种草信息,同时也有非常严格的反扒机制. 今天我们一起使用 Python破解大众点评字体加密,获取极具商业价值的信息. 本文知识点: requests 的使用 xp ...
- 总结删除文件或文件夹的7种方法-JAVA IO基础总结第4篇
本文是Java IO总结系列篇的第4篇,前篇的访问地址如下: 总结java中创建并写文件的5种方式-JAVA IO基础总结第一篇 总结java从文件中读取数据的6种方法-JAVA IO基础总结第二篇 ...
- 加载PHP的配置扩展文件
一.在PHP的安装目录中去创建一个 php.ini 的配置文件 复制php.ini-development文件,修改成php.ini文件(注意:不要多加一个空格或标点什么的).其中 php.ini ...
- 【HttpRunner v3.x】笔记—8.用例引用、变量传递
看到这里,对于httprunner已经有了一个大概的了解,现在想对于一些比较重要或者常用的功能,进行一些实践操作. 毕竟那谁说过,"纸上得来终觉浅,绝知此事要躬行." 上一篇提到了 ...
- 深入了解Kafka【二】工作流程及文件存储机制
1.Kafka工作流程 Kafka中的消息以Topic进行分类,生产者与消费者都是面向Topic处理数据. Topic是逻辑上的概念,而Partition是物理上的概念,每个Partition分为多个 ...
- termux 进阶
直接正题. 1.优化键位: mkdir $HOME/.termux echo "extra-keys = [['ESC','/','-','HOME','UP','END','PGUP'], ...
- Pinpoint 一款强大的APM工具
背景 程序的监控一直是程序员最头痛的事情之一,现网程序有问题怎么办?看进程看端口 top/free/df 三件套?网络抓包?看日志?所以为了满足这些初级需求很多公司都做了主机监控,进程端口监听等功能, ...
- 百度网盘,实现免费不限速,10M/S?
前段时间,各大消息都说百度网盘实现了免费和不限速的『提速模式』,可以达到10M/S,于是我带着好奇想要进行测试一下,探一探真假,毕竟只有自己动手实践才知道真理,结果,辜负众望,一向对用户限速还限制上传 ...
- agumaster 分页方案
本文例程下载:https://files.cnblogs.com/files/xiandedanteng/agumaster20200430-1.zip 之前的分页方案有点小瑕疵,这回修正了一下. 控 ...