Java 文件操作
# 初识
文件和目录操作是每个语言中常用的操作。在了解Java语言文件操作的过程中,你可能需要先对Java IO有一定的认识,参考:Java IO
在Java 7之前,主要是用File类来对文件进行操作;在Java 7之后,可以考虑使用新的类(主要是Path和Files)来对文件进行操作。
下面的示例会同时展示File和Path/Files的应用。
# 文件对象创建
传统方式,不论是文件还是对象,都可以直接构建:
File dir = new File("d:/");
// 判断是否是目录
dir.isDirectory();
File txtFile = new File("d:/1.txt");
// 以下两种构建方式的第一个参数都表示父目录
txtFile = new File(dir, "1.txt");
txtFile = new File("d:/", "1.txt");
// 判断是否是文件
txtFile.isFile();
除了文件、目录判断之外,还有以下判断方法:
- isAbsolute 是否是绝对路径
- isHidden 是否是隐藏文件
- exist 文件是否存在
- canExecute 是否可执行
- canRead 是否可读
- canWrite 是否可写
NIO方式:
Path dir = Paths.get("d:/");
Files.isDirectory(dir);
Path txtFile = Paths.get("d:/1.txt");
txtFile = Paths.get("d:/", "1.txt");
txtFile = dir.resolve("1.txt");
Files.isRegularFile(txtFile);
NIO中把路径和文件操作分开了。
Path,即路径。他的判断方法还有:
- isAbsolute 是否是绝对路径
- endsWith 是否以给定路径结尾
- startsWith 是否以给定路径开头
Paths用来方便的获取路径Path。
文件操作则都在Files中,他还有其他判断方法:
- isExecutable 是否是可执行文件
- isHidden 是否是隐藏文件
- isReadable 是否可读
- isSymbolicLink 是否是链接文件
- isWritable 是否可写
- exists 是否存在
# File与Path转换
新旧api之间的转换是很简单的:
Path path = new File("d:/1.txt").toPath();
File file = Paths.get("d:/1.txt").toFile();
# 获取类方法
获取文件路径、大小、名称等:
File txtFile = new File("d:/1.txt");
// 获得绝对路径
txtFile.getAbsolutePath();
// 获得父目录
txtFile.getParentFile();
// 获得文件名称
txtFile.getName();
// 获得文件大小(字节)
txtFile.length();
// 获得标准路径(去除..或.符号)
txtFile.getCanonicalPath();
// 获得所在分区(盘)的空间大小
txtFile.getTotalSpace();
// 获得所在分区(盘)的已使用的空间
txtFile.getUsableSpace();
// 获得所在分区(盘)的剩余的空间
txtFile.getFreeSpace();
NIO中 Path获取类方法:
Path txtFile = Paths.get("d:/1.txt");
// 获得标准路径(去除..或.符号)
txtFile.normalize();
// 获得父目录
txtFile.getParent();
// 获得文件名称
txtFile.getFileName();
// 获得文件系统(盘符)
txtFile.getFileSystem();
// 获得根路径
txtFile.getRoot();
// 获得名称元素的数量
txtFile.getNameCount();
# 路径解析
获得相对路径:
Paths.get("/a/b").relativize(Paths.get("/c/d")); // ../../c/d
Paths.get("/a/b").relativize(Paths.get("/a/b/c/d")); // c/d
给定路径解析,如果给定路径是绝对路径,则返回;如果是子目录,则组合:
Paths.get("/a").resolve("1.txt"); // /a/1.txt
Paths.get("/a").resolve("/c/1.txt"); // /c/1.txt
// 替换子目录
Paths.get("/a/b").resolveSibling("c") // /a/c
# 获取属性
Files中获取文件最后修改时间:
Files.getLastModifiedTime(Paths.get("d:/1.txt"), LinkOption.NOFOLLOW_LINKS);
getLastModifiedTime有个可选参数LinkOption,它是一个枚举,且只有一个值:LinkOption.NOFOLLOW_LINKS。
对于getLastModifiedTime方法来说,option参数的作用:默认情况下,遵循符号链接并读取链接的最终目标的文件属性。如果选项NOFOLLOW_LINKS存在,则不遵循符号链接,即读取符号链接本身时间。
读取文件属性:
Path txtFile = Paths.get("d:/1.txt");
// 读取文件基础属性,包括:最后修改时间、最后访问时间等等
BasicFileAttributes basicFileAttributes = Files.readAttributes(txtFile, BasicFileAttributes.class);
Map<String, Object> all = Files.readAttributes(txtFile, "*");
// Windows系统专用
BasicFileAttributes dosFileAttributes = Files.readAttributes(txtFile, DosFileAttributes.class);
Map<String, Object> dosAll = Files.readAttributes(txtFile, "dos:*");
// Unix系统专用(Posix为Unix系统的应用编程规范)
PosixFileAttributes posixFileAttributes = Files.readAttributes(txtFile, PosixFileAttributes.class);
读取属性的其他方法:
Object lastAccessTime = Files.getAttribute(txtFile, "lastAccessTime");
BasicFileAttributeView fileAttributeView = Files.getFileAttributeView(txtFile, BasicFileAttributeView.class);
BasicFileAttributes basicFileAttributes = fileAttributeView.readAttributes();
读取文件大小、存储盘:
Files.size(txtFile);
FileStore fileStore = Files.getFileStore(txtFile);
fileStore.getUsableSpace();
fileStore.getTotalSpace();
fileStore.getUnallocatedSpace();
fileStore.getBlockSize();
还有一个特别有用的方法:probeContentType,探测文件类型:
Files.probeContentType(Paths.get("D:\\1.jpg")); // image/jpeg
Files.probeContentType(Paths.get("D:\\1.webp"))); // image/webp
Files.probeContentType(Paths.get("D:\\1.jfif"))); // image/jpeg
Files.probeContentType(Paths.get("D:\\1.txt"))); // text/plain
Files.probeContentType(Paths.get("D:\\1.mp4"))); // video/mp4
Files.probeContentType(Paths.get("D:\\QQ.exe"))); // application/x-msdownload
Files.probeContentType(Paths.get("D:\\1.xlsx"))); // application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
注意,这个方法不会根据真实的文件内容判断MIME类型,而是会根据后缀来判断类型。比如你把mp4文件后缀修改为txt,那么返回结果就是:text/plain
# 读取权限
获得拥有者和权限:
UserPrincipal owner = Files.getOwner(txtFile);
owner.getName();
// 仅限Unix系统使用
Set<PosixFilePermission> posixFilePermissions = Files.getPosixFilePermissions(txtFile);
# 设置属性和权限
传统方式:
File txtFile = new File("d:/1.txt");
// 设为可执行
txtFile.setExecutable(true);
// 设为可读
txtFile.setReadable(true);
// 设为可写
txtFile.setWritable(true);
// 设为只读
txtFile.setReadOnly(true);
// 设置最后修改时间
txtFile.setLastModified(millis);
NIO:
Path path = Paths.get("d:/1.txt");
// 设置属性
Files.setAttribute(path,"lastAccessTime",millis);
// 设置最后修改时间
Files.setLastModifiedTime(path,millis);
// 设置文件所有者
FileSystem fileSystem = path.getFileSystem();
UserPrincipalLookupService service = fileSystem.getUserPrincipalLookupService();
UserPrincipal userPrincipal = service.lookupPrincipalByName("joe");
Files.setOwner(path, userPrincipal);
// 设置权限
Set<PosixFilePermission> perms = new HashSet<>();
perms.add(PosixFilePermission.OWNER_READ);
perms.add(PosixFilePermission.OWNER_WRITE);
perms.add(PosixFilePermission.OWNER_EXECUTE);
// perms = PosixFilePermissions.fromString("rwxr--r--"); // 从文字中解析权限
Files.setPosixFilePermissions(path, perms);
# 读取、写入内容
读取文件内容:
Path txtFile = Paths.get("d:/1.txt");
Files.readAllLines(txtFile, StandardCharsets.UTF_8);
Files.readString(txtFile, StandardCharsets.UTF_8);
Files.readAllBytes(txtFile);
Files.readSymbolicLink(txtFile);
Files.newByteChannel(txtFile);
Files.newInputStream(txtFile);
Files.newBufferedReader(txtFile);
Files.newOutputStream(txtFile);
写入文件内容:
List<String> lines = Lists.newArrayList("1","2");
Files.write(txtFile, lines, StandardCharsets.UTF_8);
Files.write(txtFile, new byte[]{ ... });
Files.writeString(txtFile, "1\n2");
# 创建、删除、移动
传统方式的演示:
// 创建目录
new File("d:/data").mkdir();
// 创建任何不存在的目录
new File("d:/data/abc").mkdirs();
File file = new File("d:/1.txt");
// 创建文件(上层目录必须已存在)。创建之前最好用exists()方法判断一下,以免异常
file.createNewFile();
// 重命名(也可用于移动)
file.renameTo(new File("d:/2.txt"));
// 删除
file.delete();
file.deleteOnExit();
// 没有复制方法,需要自己实现
NIO方式:
Files.createFile(Paths.get("d:/1.txt"));
Files.createDirectory(Paths.get("d:/data"));
Files.createDirectories(Paths.get("d:/data/abc"));
// 创建临时文件
Files.createTempFile("temp-", ".txt");
// 创建临时目录
Files.createTempDirectory("tdir-");
你会发现不管是传统API还是新API,都存在一些缺失,比如移动、删除目录等,这就要借助工具库来实现了,后面会讲到。
# 遍历
传统方式主要是list和listFiles方法:
File dir = new File("d:/data");
// 获得文件和目录
String[] list = dir.list();
// 获得FilenameFilter过滤后的文件
list = dir.list((dir1, name) -> name.endsWith(".jpg"));
// 获得文件和目录
File[] files = dir.listFiles();
// 获得FileFilter过滤后的文件
dir.listFiles(File::canRead);
// 获得FilenameFilter过滤后的文件
dir.listFiles((dir1,name) -> name.endsWith(".jpg"));
NIO:
Path path = Paths.get("d:/data");
// 文件搜索。会递归目录进行搜索,第二个参数表示深度,第三个参数表示过滤
try (Stream<Path> pathStream = Files.find(path, 3, ((path1, basicFileAttributes) -> basicFileAttributes.isRegularFile()))) {
pathStream.forEach(System.out::println);
}
// 目录遍历(非深度)
try (DirectoryStream<Path> pathStream = Files.newDirectoryStream(path)) {
pathStream.forEach(System.out::println);
}
// 目录遍历(非深度)的另一种方式,是惰性且线程安全的
try (Stream<Path> pathStream = Files.list(path)) {
pathStream.forEach(System.out::println);
}
// 深度优先的目录遍历。第二个参数是可选的,表示遍历的深度,如果省略则为Integer.MAX_VALUE
try (Stream<Path> pathStream = Files.walk(path, 5)) {
pathStream.forEach(System.out::println);
}
// 深度优先的目录遍历。使用FileVisitor,通过FileVisitResult枚举来控制遍历流程,比如终止(TERMINATE)、跳过此目录(SKIP_SUBTREE)、继续(CONTINUE)等
Files.walkFileTree(path, new FileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (attrs.isOther()){
return FileVisitResult.TERMINATE;
}else {
return FileVisitResult.CONTINUE;
}
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
});
# 工具类库
我们熟悉的Commons IO,maven引入:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
比较有用的方法:
- byteCountToDisplaySize 字节数转为人类可读,比如TB、GB
- cleanDirectory 清除目录下文件
- copyDirectory 复制目录
- deleteDirectory 删除目录下文件和本身
- moveDirectory 移动目录
# RandomAccessFile
RandomAccessFile随机文件访问(包括读/写)的类,他的核心方法是seek,可以移动文件指针。
比如从1024个字节处开始,读取1024个字节:
try (RandomAccessFile file = new RandomAccessFile("D://1.txt", "r")) {
file.seek(1024);
file.read(new byte[1024]);
}
或者在文件末尾写入内容:
try (RandomAccessFile file = new RandomAccessFile("D://1.txt", "rw")) {
file.seek(file.length());
file.writeChars("who am i");
}
第二个参数是访问模式,有四种:
- r 只读
- rw 可读写,文件关闭时同步内容到磁盘
- rws 可读写,每次写入都会更新元数据(最后更新时间)和同步内容到磁盘
- rwd 可读写,每次写入都会同步内容到磁盘
RandomAccessFile常用于多线程下载、断点续传、追加文件内容等场景。
# 总结
通过本章内容,你会看到新IO相对于传统IO更加的现代。不过要注意在相当一部分项目中,传统IO因为概念简单,操作方便也被大家普遍使用。
把新旧API放在一起学习,会更加有利于你的记忆,希望你有所收获!
祝你变得更强!