在丑陋的 Java I/O 编程方式诞生多年以后,Java终于简化了文件读写的基本操作。
在 Java7 中对此引入了巨大的改进。这些新元素被放在 java.nio.file 包下面,过去人们通常把 nio 中的 n 理解为 new 即新的 io,现在更应该当成是 non-blocking 非阻塞 io(io就是input/output输入*/*输出)。最重要的是 Java8 新增的 streams 与文件结合使得文件操作编程变得更加优雅。
我们将看一下文件操作的两个基本组件:
文件或者目录的路径;
文件本身。
一个 Path 对象表示一个文件或者目录的路径,是一个跨操作系统(OS)和文件系统的抽象,目的是在构造路径时不必关注底层操作系统,代码可以在不进行修改的情况下运行在不同的操作系统上。java.nio.file.Paths 类包含一个重载方法 static get(),该方法接受一系列 String 字符串或一个统一资源标识符(URI)作为参数,并且进行转换返回一个 Path 对象:
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathInfo {
static void show(String id, Object p) {
System.out.println(id + ":" + p);
}
static void info(Path p) {
show("toString", p);
show("Exists", Files.exists(p)); // 检查一个Path在文件系统中是否存在
show("RegularFile", Files.isRegularFile(p)); // 判断是否是一个文件
show("Directory", Files.isDirectory(p));
show("Absolute", p.isAbsolute());
show("FileName", p.getFileName());
show("Parent", p.getParent());
show("Root", p.getRoot());
System.out.println("******************");
}
public static void main(String[] args) {
System.out.println(System.getProperty("os.name"));
info(Paths.get("C:", "path", "to", "nowhere", "NoFile.txt"));
Path p = Paths.get("PathInfo.java");
info(p);
Path ap = p.toAbsolutePath();
info(ap);
info(ap.getParent());
try {
info(p.toRealPath());
} catch (IOException e) {
System.out.println(e);
}
URI u = p.toUri();
System.out.println("URI: " + u);
Path puri = Paths.get(u);
System.out.println(Files.exists(puri));
File f = ap.toFile();
}
}
Path 对象可以非常容易地生成路径的某一部分:
import java.nio.file.Path;
import java.nio.file.Paths;
public class PartsOfPaths {
public static void main(String[] args) {
System.out.println(System.getProperty("os.name"));
/*
* PS:System.setProperty 设置 user.dir,不设置的话,
* Paths.get("PartsOfPaths.java").toAbsolutePath()输出的就是 D:\demo\PartsOfPaths.java
* 并不是全限定名的类路径了。
*/
System.setProperty("user.dir", "D:\\demo\\src\\main\\java\\files");
Path p = Paths.get("PartsOfPaths.java").toAbsolutePath();
/*
* PS:path.getName(int index)和path.getNameCount():分层拆分路径,
* 例如对于/a/b/c,getNameCount()返回的就是3,
* 而对于path.getName(int index),当index分别为0~2时,
* 得到的结果分别是 a、b、c。
*/
for (int i = 0; i < p.getNameCount(); i++) {
System.out.println(p.getName(i));
}
/*
* PS:endsWith() 并不是用来匹配后缀的,而是用来匹配 Path 的
* 所以这里是 false
*/
System.out.println("ends with '.java': " + p.endsWith(".java"));
/*
* PS:Path 也实现了 Iterable 接口,因此我们也可以通过增强的 for-each 进行遍历
* path.startsWith(Path other) 或 path.startsWith(String other):
* 判断 path 的是不是以 other 开头的。
* 对于 /a/b/c,不是 start with a,而是 start with "/"
* 本例的 start with 为 "D:\" ,通过 getRoot() 可以获取
*/
for (Path pp : p) {
System.out.print(pp + ": ");
System.out.print(p.startsWith(pp) + " : ");
System.out.println(p.endsWith(pp));
}
System.out.println("Starts with " + p.getRoot() + " " + p.startsWith(p.getRoot()));
}
}
输出:
Windows 10
demo
src
main
java
files
PartsOfPaths.java
ends with '.java': false
demo: false : false
src: false : false
main: false : false
java: false : false
files: false : false
PartsOfPaths.java: false : true
Starts with D:\ true
—PS: Path 接口,主要用来在文件系统中定位文件,通常表示系统相关的文件路径。Paths 静态工具类,用来根据String格式的路径或者URI返回Path的实例
Files 工具类包含一系列完整的方法用于获得 Path 相关的信息。
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathAnalysis {
static void say(String id, Object result) {
System.out.println(id + ": " + result);
}
public static void main(String[] args) throws IOException {
System.out.println(System.getProperty("os.name"));
System.setProperty("user.dir", "D:\\demo\\src\\main\\java\\files");
Path p = Paths.get("PathAnalysis.java").toAbsolutePath();
say("Exists", Files.exists(p));
say("Directory", Files.isDirectory(p));
say("Executable", Files.isExecutable(p));
say("Readable", Files.isReadable(p));
say("RegularFile", Files.isRegularFile(p));
say("Writable", Files.isWritable(p));
say("notExists", Files.notExists(p));
say("Hidden", Files.isHidden(p));
say("size", Files.size(p));
say("FileStore", Files.getFileStore(p));
say("LastModified: ", Files.getLastModifiedTime(p));
say("Owner", Files.getOwner(p));
say("ContentType", Files.probeContentType(p));
say("SymbolicLink", Files.isSymbolicLink(p));
if (Files.isSymbolicLink(p)) {
say("SymbolicLink", Files.readSymbolicLink(p));
}
// 个测试方法 getPosixFilePermissions() 之前我们需要确认一下当前文件系统是否支持 Posix 接口,否则会抛出运行时异常
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
say("PosixFilePermissions", Files.getPosixFilePermissions(p));
}
}
}
输出:
Windows 10
Exists: true
Directory: false
Executable: true
Readable: true
RegularFile: true
Writable: true
notExists: false
Hidden: false
size: 1674
FileStore: 软件 (D:)
LastModified: : 2023-06-14T09:26:50.330762Z
Owner: T (User)
ContentType: null
SymbolicLink: false
推荐一个大佬写的文章:文件操作工具类Files
我们必须能通过对 Path 对象增加或者删除一部分来构造一个新的 Path 对象。我们使用 relativize() 移除 Path 的根路径,使用 resolve() 添加 Path 的尾路径(不一定是“可发现”的名称)。
Files 工具类包含大部分我们需要的目录操作和文件操作方法。
使用静态的 FileSystems 工具类获取"默认"的文件系统,也可以在 Path 对象上调用 getFileSystem() 以获取创建该 Path 的文件系统。你可以获得给定 URI 的文件系统,还可以构建新的文件系统(对于支持它的操作系统)。
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
public class FileSystemDemo {
static void show(String id, Object o) {
System.out.println(id + ": " + o);
}
public static void main(String[] args) {
System.out.println(System.getProperty("os.name"));
FileSystem fsys = FileSystems.getDefault();
for (FileStore fs : fsys.getFileStores()) { // 获取系统盘
show("File Store", fs);
}
for (Path rd : fsys.getRootDirectories()) { // 根目录
show("Root Directory", rd);
}
show("Separator", fsys.getSeparator()); // 分隔符
// 返回此文件系统的可选操作
show("UserPrincipalLookupService", fsys.getUserPrincipalLookupService());
show("isOpen", fsys.isOpen());
show("isReadOnly", fsys.isReadOnly());
// 返回创建此文件系统的提供程序
show("FileSystemProvider", fsys.provider());
// 返回文件系统支持的文件属性视图名称
show("File Attribute Views", fsys.supportedFileAttributeViews());
}
}
输出:
Windows 10
File Store: 系统 (C:)
File Store: 软件 (D:)
Root Directory: C:\
Root Directory: D:\
Separator: \
UserPrincipalLookupService: sun.nio.fs.WindowsFileSystem$LookupService$1@4eec7777
isOpen: true
isReadOnly: false
FileSystemProvider: sun.nio.fs.WindowsFileSystemProvider@3b07d329
File Attribute Views: [owner, dos, acl, basic, user]
通过 WatchService 可以设置一个进程对目录中的更改做出响应。在这个例子中,delTxtFiles() 作为一个单独的任务执行,该任务将遍历整个目录并删除以 .txt 结尾的所有文件,WatchService 会对文件删除操作做出反应:
import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
public class PathWatcher {
static Path test = Paths.get("D:\\demo\\src\\test");
static void delTxtFiles() {
try {
Files.walk(test).filter(f -> f.toString().endsWith(".txt"))
.forEach(f -> {
System.out.println("deleting " + f);
try {
Files.delete(f);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException, InterruptedException {
// PS:在 D:\demo\src\test 下 创建 Hello.txt
Files.createFile(test.resolve("Hello.txt"));
// PS:获取一个路径监听
WatchService watcher = FileSystems.getDefault().newWatchService();
// PS:将路径监听放在 test 上,监听事件为 delete
// 可以选择 ENTRY_CREATE,ENTRY_DELETE 或 ENTRY_MODIFY(其中创建 和删除不属于修改)
test.register(watcher, ENTRY_DELETE);
// PS:过调用 Executors.newSingleThreadScheduledExecutor() 产生一个 ScheduledExecutorService 对象,
// 然后调用 schedule() 方法传递所需函数的方法引用,并且设置在运行之前应该等待的时间
Executors.newSingleThreadScheduledExecutor().schedule(PathWatcher::delTxtFiles, 250, TimeUnit.MILLISECONDS);
// PS:当目标事件发生时,会返回一个包含 WatchEvent 的 Watchkey 对象。
// 展示的这三种方法是能对 WatchEvent 执行的全部操作。
WatchKey key = watcher.take();
for (WatchEvent evt : key.pollEvents()) {
System.out.println("evt.context(): " + evt.context() + "\nevt.count(): " + evt.count() + "\nevt.kind(): " + evt.kind());
// PS:status 为 0:表示正常退出程序,也就是结束当前正在运行中的java虚拟机。
// status 为 1 或 -1 或 任何其他非零值 :表示非正常退出当前程序。
// 正常退出 是指如果当前程序还有在执行的任务,则等待所有任务执行完成以后再退出;
// 非正常退出 是只要时间到了,立刻停止程序运行,不管是否还有任务在执行。
System.exit(0);
}
输出:
deleting D:\demo\src\test\Hello.txt
evt.context(): Hello.txt
evt.count(): 1
evt.kind(): ENTRY_DELETE
如果说"监视这个目录",自然会包含整个目录和下面子目录,但实际上 的:只会监视给定的目录,而不是下面的所有内容。如果需要监视整个树目录,必须在整个树的每个子目录上放置一个 Watchservice。
到目前为止,为了找到文件,我们一直使用相当粗糙的方法,在 path 上调用 toString() ,然后使用 string 操作查看结果。事实证明, java.nio.file 有更好的解决方案:通过在 FileSystem 对象上调用 getPathMatcher() 获得一个 PathMatcher ,然后传入您感兴趣的模式。模式有两个选项: glob 和 regex 。
在这里,我们使用 glob 查找以 .tmp 或 .txt 结尾的所有 Path :
import java.io.IOException;
import java.nio.file.*;
public class Find {
public static void main(String[] args) throws IOException {
Path test = Paths.get("D:\\demo\\src\\test");
Files.createDirectories(test.resolve("dir.tmp"));
// PS: **/ 表示“当前目录及所有子目录”
// 单 * 表示“任何东西”,然后是一个点,然后大括号表示正在寻找以 .tmp 或 .txt 结尾的东西
PathMatcher matcher = FileSystems.getDefault()
.getPathMatcher("glob:**/*.{tmp,txt}");
Files.walk(test).filter(matcher::matches)
.forEach(System.out::println);
}
}
输出:
D:\demo\src\test\a\1.tmp
D:\demo\src\test\a\2.tmp
D:\demo\src\test\b\3.txt
D:\demo\src\test\b\4.txt
D:\demo\src\test\dir.tmp
—PS:先在 test 目录下面手动创建了几个文件,dir.tmp 是代码运行创建
Files.readAllLines() 一次读取整个文件(因此,“小”文件很有必要),产生一
个 List 。
先在 test 目录下,创建 Cheese.dat 内容为:
// streams/Cheese.dat
Not much of a cheese shop really, is it?
Finest in the district, sir.
And what leads you to that conclusion?
Well, it's so clean.
It's certainly uncontaminated by cheese.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ListOfLines {
public static void main(String[] args) throws IOException {
Path path = Paths.get("D:\\demo\\src\\test\\Cheese.dat").toAbsolutePath();
// PS:Files.readAllLines 入参可以是 path 返回一个 list
Files.readAllLines(path).stream()
.filter(line -> !line.startsWith("//"))
.map(line -> line.substring(0, line.length() / 2))
.forEach(System.out::println);
}
}
输出:
Not much of a cheese
Finest in the
And what leads you
Well, it's
It's certainly uncon
Files.write() 被重载以写入 byte 数组或任何 Iterable 对象(它也有 Charset 选项):
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Random;
public class Writing {
static Random rand = new Random(47);
static final int SIZE = 1000;
public static void main(String[] args) throws IOException {
byte[] bytes = new byte[SIZE];
// PS:生成随机字节并将它们放入用户提供的字节数组中。生成的随机字节数等于字节数组的长度
rand.nextBytes(bytes);
Path path = Paths.get("D:\\demo\\src\\test\\bytes.dat");
// PS:Files.write 放入 byte 数组
Files.write(path, bytes);
System.out.println("bytes.dat: " + Files.size(path));
Path path2 = Paths.get("D:\\demo\\src\\test\\Cheese.dat");
Path path3 = Paths.get("D:\\demo\\src\\test\\Cheese.txt");
List<String> stringList = Files.readAllLines(path2);
// PS:Files.write 放入任何 Iterable 对象
Files.write(path3,stringList);
System.out.println("Cheese.txt: " + Files.size(path3));
}
}
输出:
bytes.dat: 1000
Cheese.txt: 199
—PS:运行完代码在 test 下会自动创建 bytes.dat 和 Cheese.txt
如果文件大小有问题怎么办? 比如说:
文件太大,如果你一次性读完整个文件,你可能会耗尽内存。
您只需要在文件的中途工作以获得所需的结果,因此读取整个文件会浪费时间。
Files.lines() 方便地将文件转换为行的 Stream :
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ReadLineStream {
public static void main(String[] args) throws IOException {
Path path = Paths.get("D:\\demo\\src\\test\\Cheese.dat");
Files.lines(path).skip(2)
.findFirst()
.ifPresent(System.out::println);
}
}
输出:
Finest in the district, sir.
虽然本章对文件和目录操作做了相当全面的介绍,但是仍然有没被介绍的类库中的功能——一定要研究 java.nio.file 的 Javadocs,尤其是 java.nio.file.Files 这个类。
Java 7 和 8 对于处理文件和目录的类库做了大量改进。使用文件现在很简单,甚至很有趣,这是你以前永远想不到的。
推荐一篇文章:详解 File、Path、Paths、Files 四个类,Java操作文件不再难
自我学习总结: