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

看完这篇文章你会了解到:

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

在 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 特性的更多相关文章

  1. 还看不懂同事代码?快来补一波 Java 7 语法特性

    前言 Java 平台自出现到目前为止,已经 20 多个年头了,这 20 多年间 Java 也一直作为最流行的程序设计语言之一,不断面临着其他新兴编程语言的挑战与冲击.Java 语言是一种静态强类型语言 ...

  2. Java中的NIO及IO

    1.概述 Java NIO(New IO) 是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API.NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同, ...

  3. JAVA中的NIO (New IO)

    简介 标准的IO是基于字节流和字符流进行操作的,而JAVA中的NIO是基于Channel和Buffer进行操作的. 传统IO graph TB; 字节流 --> InputStream; 字节流 ...

  4. java中的NIO和IO到底是什么区别?20个问题告诉你答案

    摘要:NIO即New IO,这个库是在JDK1.4中才引入的.NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多. 本文分享自华为云社区<jav ...

  5. Java中的NIO和IO的对比分析

    总的来说,java中的IO和NIO主要有三点区别: IO NIO 面向流 面向缓冲 阻塞IO 非阻塞IO 无 选择器(Selectors) 1.面向流与面向缓冲 Java NIO和IO之间第一个最大的 ...

  6. Java IO编程全解(一)——Java的I/O演进之路

    转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7419117.html JDK1.4之前的早期版本,Java对I/O的支持并不完善,开发人员在开发高性能I/O ...

  7. 【java】io流之字符输入流:java.io.Reader类及子类的子类java.io.FileReader

    package 文件操作; import java.io.File; import java.io.FileReader; import java.io.IOException; import jav ...

  8. 【java】io流之字符输出流:java.io.Writer类及子类的子类java.io.FileWriter

    package 文件操作; import java.io.File; import java.io.FileWriter; import java.io.IOException; import jav ...

  9. java非阻塞NIO和阻塞IO

    1         非阻塞NIO和阻塞IO 1.1           定义 阻塞IO:线程被阻塞,去处理一个读取和写入,中间如果有等待时间,则线程被占用,也不能处理其他任务: 非阻塞IO(new I ...

随机推荐

  1. 学习一下 JVM (二) -- 学习一下 JVM 中对象、String 相关知识

    一.JDK 8 版本下 JVM 对象的分配.布局.访问(简单了解下) 1.对象的创建过程 (1)前言 Java 是一门面向对象的编程语言,程序运行过程中在任意时刻都可能有对象被创建.开发中常用 new ...

  2. CF1271C Shawarma Tent 题解

    通过分析样例可以发现,离学校越近的地点经过的路线也会越多,因此我们只要考虑学校周围的八个点即可.而且可以发现,对于一个点,路线会经过这个点的节点是确定的.因此在输入的时候可以统计学校周围八个节点被经过 ...

  3. 读取手机中的联系人信息(android.provider.ContactsContract)

    本篇开始讲如何从Android中得到本机联系人的信息.由于Android较快的版本升级,部分API已经发生了变化.本篇探究的通过ContentProvider机制获取联系人的API从Android2. ...

  4. Java 的开发效率究竟比 C++ 高在哪里?

    有几个原因     大师助手解决你的烦恼1. 语言上,Java是一个比C++更容易parse得多的语言,所以相应的工具链IDE会更容易做,无论多大的Java的项目,就是新手写完都不会有编译错误.但是写 ...

  5. 性能提升40%: 腾讯 TKE 用 eBPF 绕过 conntrack 优化 K8s Service

    Kubernetes Service 用于实现集群中业务之间的互相调用和负载均衡,目前社区的实现主要有userspace,iptables和IPVS三种模式.IPVS模式的性能最好,但依然有优化的空间 ...

  6. 使用服务进程启动asp.net core程序

    昨天在PuTTY中启动了测试网站,因为时间太晚,看到效果,立马关机睡觉,刚上床又想看看手机端访问效果,结果一打开,发现nginx找不到页面,意识到应该是退出PuTTY导致进程结束,于是今天赶紧查了解决 ...

  7. 银行应用USB Key身份认证方案

    前言:本文为USB Key的厂商SafeNet公司提供的宣传稿件,并不代表本博客作者的观点和看法. 本文的技术解决方案使用的是SafeNet自身的产品,虽然这个产品支持数字签名和PKI体系,可生成并储 ...

  8. [第二届全国中学生网络安全竞赛]bypass

    前几天拿到了线下赛的源码,做做看.这道主要是命令执行的黑名单绕过 先看看给出的代码: <?php highlight_file(__FILE__); error_reporting(0); $b ...

  9. Linux下find与exec的联手干大事

    在Linux下工作,find命令绝对是一个非常高频的命令.我们可以用find命令来找到符合某些关键词的文件,找到某些日期的文件,也可以设定一些正则表达式,找到一系列满足该条件的文件. 但是,如果只有一 ...

  10. 通过Xshell实现socket代理访问公司内网

    首先连接上Server,点击查看---隧道窗格 之后点击转移规则--空白处右键,添加 选择Dynamic,之后选择一个本地没有被占用的端口, 确定 浏览器设置 之后就可以访问公司内部的网站了