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

相关文章
|
2月前
|
存储 安全 Java
Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
【10月更文挑战第17天】Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
78 2
|
2月前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
48 3
|
2月前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
37 2
|
2月前
|
存储 算法 Java
Java Set因其“无重复”特性在集合框架中独树一帜
【10月更文挑战第14天】Java Set因其“无重复”特性在集合框架中独树一帜。本文深入解析Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定的数据结构(哈希表、红黑树)确保元素唯一性,并提供最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的`hashCode()`与`equals()`方法。
36 3
|
2月前
|
安全 Java API
Java 17新特性让你的代码起飞!
【10月更文挑战第4天】自Java 8发布以来,Java语言经历了多次重大更新,每一次都引入了令人兴奋的新特性,极大地提升了开发效率和代码质量。本文将带你从Java 8一路走到Java 17,探索那些能让你的代码起飞的关键特性。
93 1
|
6天前
|
存储 监控 Java
Java的NIO体系
通过本文的介绍,希望您能够深入理解Java NIO体系的核心组件、工作原理及其在高性能应用中的实际应用,并能够在实际开发中灵活运用这些知识,构建高效的Java应用程序。
24 5
|
11天前
|
存储 Java 开发者
什么是java的Compact Strings特性,什么情况下使用
Java 9引入了紧凑字符串特性,优化了字符串的内存使用。它通过将字符串从UTF-16字符数组改为字节数组存储,根据内容选择更节省内存的编码方式,通常能节省10%至15%的内存。
|
20天前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
35 6
|
24天前
|
Java
java 中 IO 流
Java中的IO流是用于处理输入输出操作的机制,主要包括字节流和字符流两大类。字节流以8位字节为单位处理数据,如FileInputStream和FileOutputStream;字符流以16位Unicode字符为单位,如FileReader和FileWriter。这些流提供了读写文件、网络传输等基本功能。
42 9
|
1月前
|
分布式计算 Java API
Java 8引入了流处理和函数式编程两大新特性
Java 8引入了流处理和函数式编程两大新特性。流处理提供了一种声明式的数据处理方式,使代码更简洁易读;函数式编程通过Lambda表达式和函数式接口,简化了代码书写,提高了灵活性。此外,Java 8还引入了Optional类、新的日期时间API等,进一步增强了编程能力。这些新特性使开发者能够编写更高效、更清晰的代码。
33 4
下一篇
DataWorks