和低效 IO 说再见,回头补一波 Java 7 的 NIO.2 特性

简介: 和低效 IO 说再见,回头补一波 Java 7 的 NIO.2 特性

其实在这之前已经写过一篇关于 Java 7 的新特性文章了,那篇文章主要介绍了 Java 7 的资源自动关闭、Switch String 实现原理、异常捕获 try-catch、新的二进制书写方式等,具体的内容也可以看下当初的这篇文章(补一波 Java 7 语法特性)。而在那篇文章里唯独没有介绍到 Java 7 中对于 IO 操作的更新,而这部分恰好又是非常重要的一部分,该还的总是要还的,现在补上。看完这篇文章你会了解到:

  1. 抽象文件路径操作方式,直观方便少 BUG。
  2. 高效文件操作方式,写入读取复制文件仅需一行
  3. 快速获取不同系统下的文件属性
  4. 遍历目录下文件和目录的多种方式,且十分高效。
  5. 反应式事件通知,监测文件变化。

在 Java 7 中,加强了文件操作相关功能,也就是新的 java.nio.file 包里的内容,它提供了诸如文件路径抽象、文件目录流、目录树、文件属性和变化监视服务等功能,可以大幅度提高我们对于文件的操作。微信图片_20220414163124.jpgNIO.2

文件路径

在 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 操作系统准备的。微信图片_20220414163128.pngJava7 文件属性

使用起来过于简单,直接代码奉上。

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 文件生成和自动删除,也被监测到了。

相关文章
|
5天前
|
Java Unix Go
【Java】(8)Stream流、文件File相关操作,IO的含义与运用
Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。!但本节讲述最基本的和流与 I/O 相关的功能。我们将通过一个个例子来学习这些功能。
71 1
|
16天前
|
Java API 数据处理
Java新特性:使用Stream API重构你的数据处理
Java新特性:使用Stream API重构你的数据处理
Java API 开发者
45 0
|
2月前
|
Java 测试技术 API
Java IO流(二):文件操作与NIO入门
本文详解Java NIO与传统IO的区别与优势,涵盖Path、Files类、Channel、Buffer、Selector等核心概念,深入讲解文件操作、目录遍历、NIO实战及性能优化技巧,适合处理大文件与高并发场景,助力高效IO编程与面试准备。
|
2月前
|
SQL Java 数据库连接
Java IO流(一):字节流与字符流基础
本文全面解析Java IO流,涵盖字节流、字符流及其使用场景,帮助开发者理解IO流分类与用途,掌握文件读写、编码转换、异常处理等核心技术,通过实战案例提升IO编程能力。
|
3月前
|
存储 Java Linux
操作系统层面视角下 Java IO 的演进路径及核心技术变革解析
本文从操作系统层面深入解析Java IO的演进历程,涵盖BIO、NIO、多路复用器及Netty等核心技术。分析各阶段IO模型的原理、优缺点及系统调用机制,探讨Java如何通过底层优化提升并发性能与数据处理效率,全面呈现IO技术的变革路径与发展趋势。
69 2
|
3月前
|
监控 Java API
现代 Java IO 高性能实践从原理到落地的高效实现路径与实战指南
本文深入解析现代Java高性能IO实践,涵盖异步非阻塞IO、操作系统优化、大文件处理、响应式网络编程与数据库访问,结合Netty、Reactor等技术落地高并发应用,助力构建高效可扩展的IO系统。
103 0
|
3月前
|
并行计算 Java API
Java List 集合结合 Java 17 新特性与现代开发实践的深度解析及实战指南 Java List 集合
本文深入解析Java 17中List集合的现代用法,结合函数式编程、Stream API、密封类、模式匹配等新特性,通过实操案例讲解数据处理、并行计算、响应式编程等场景下的高级应用,帮助开发者提升集合操作效率与代码质量。
153 1
|
3月前
|
安全 Java 微服务
Java 最新技术和框架实操:涵盖 JDK 21 新特性与 Spring Security 6.x 安全框架搭建
本文系统整理了Java最新技术与主流框架实操内容,涵盖Java 17+新特性(如模式匹配、文本块、记录类)、Spring Boot 3微服务开发、响应式编程(WebFlux)、容器化部署(Docker+K8s)、测试与CI/CD实践,附完整代码示例和学习资源推荐,助你构建现代Java全栈开发能力。
415 1
|
3月前
|
缓存 安全 Java
Java 并发新特性实战教程之核心特性详解与项目实战
本教程深入解析Java 8至Java 19并发编程新特性,涵盖CompletableFuture异步编程、StampedLock读写锁、Flow API响应式流、VarHandle内存访问及结构化并发等核心技术。结合电商订单处理、缓存系统、实时数据流、高性能计数器与用户资料聚合等实战案例,帮助开发者高效构建高并发、低延迟、易维护的Java应用。适合中高级Java开发者提升并发编程能力。
81 0

热门文章

最新文章