Java> 文件操作(Files, Path, File)
概要
Files: 操作文件的工具类,Java7加入,封装了用户机器上处理文件系统所需所有功能。包含了文件创建、复制、写入、读出、删除,获取文件信息,快捷访问、遍历目录等功能。使用较File更方便,由于结合了Path/Stream
Path: 表示文件路径,Java7加入,常用Paths创建,配合Files使用。
File: 传统文件类,Java 1.0加入,功能强大,但使用繁琐。
Path类
Path通过表示一个目录名序列,后面还可以跟着一个文件名,来表示路径。
创建方式
- 通过指定路径字符串 Paths.get()
通过Paths.get() 拼接多个字符串,组成路径。
包含2类:1)绝对路径;2)相对路径。
路径不必是一个存在的文件,仅仅只是一个抽象的名字序列。只有当要创建文件的时候,才会调用方法根据路径创建文件。
Path absolute = Paths.get("C:\\Users", "test.txt"); // 绝对路径, 使用Windows风格路径分隔符
Path relative = Paths.get("config", "properties", "user.properties"); // 相对路径
- 通过已有Path + 字符串组合 Path.resolve()和Path.resolveSibling()
// resolve path
Path basePath = Paths.get("rss"); // 通过字符串获取路径
Path resolvePath = basePath.resolve("resolvePath"); // 组合basePath和"resolvePath"得到新路径
Path resolveSibling = basePath.resolveSibling("resolveSibling"); // 得到basePath兄弟路径"resolveSibling"
// 打印转换path
System.out.println("basePath = " + basePath.toAbsolutePath());
System.out.println("resolvePath = " + resolvePath.toAbsolutePath());
System.out.println("resolveSibling = " + resolveSibling.toAbsolutePath());
- 产生相对路径relativize
relativize是resolve逆操作。
p.resolve(r)结果产生路径q = "p/r";p.relative(q)产生r,即r="../q"。简单来说,就是resolve是利用母路径path+字符串(作为子路径),组合成新路径;relativize是通过母路径 - 组合的新路径,得到相对路径。
// relative path
System.out.println(basePath.relativize(resolvePath));
- 打印转换path和relative path运行结果
basePath = F:\workspace\IDEA\Java_Core2\rss
resolvePath = F:\workspace\IDEA\Java_Core2\rss\resolvePath
resolveSibling = F:\workspace\IDEA\Java_Core2\resolveSibling
resolvePath
- 其他常用操作
normalize 移除所有冗余.和..部件
toAbsolutePath 产生给定路径的绝对路径
getParent 获取父路径
getFileName 获取文件名
getRoot 获取根目录,Unix是 / , Windows是所在盘符根目录
toFile 转换成File类对象
通过Path构建Scanner对象
Scanner in = new Scanner(Paths.get("C:\\Users\test.txt"));
Files类
创建文件
- 创建目录
如果目录已经存在会抛出异常FileAlreadyExistsException. 创建目录是原子性的
Path path = Paths.get("dir");
Files.createDirectory(path); // 创建以path为路径的目录
- 创建文件
如果文件已经存在会抛出异常FileAlreadyExistsException. 创建文件是原子性的
Path path = Paths.get("file");
Files.createDirectory(pat); // 创建以path为路径的文件, 文件可以与目录路径及同名
- 在给定位置或者系统指定位置,创建临时文件/目录
Path newPath = Files.createTempFile(dir, prefix, suffix); // dir路径下, 创建以prefix为前缀, suffix为后缀的名称的文件
Path newPath = Files.createTempFile(prefix, suffix); // 系统默认临时目录路径下, 创建以prefix为前缀, suffix为后缀的名称的文件
Path newPath = Files.createTempDirectory(dir, prefix); // dir路径下, 创建以prefix为前缀, suffix为后缀的名称的目录
Path newPath = Files.createTempDirecotry(prefix); // 系统默认临时目录路径下, 创建以prefix为前缀, suffix为后缀的名称的目录
dir是一个Path对象,给定创建临时文件路径;
prefix,suffix可以为null字符串,分别指定文件名前缀、后缀;
系统默认临时文件夹路径,Win10x64:C:\Users\Martin\AppData\Local\Temp
读写文件
- 读取/写中小文件
/* 一次读取所有文件内容 */
// 一次按二进制读取所有文件内容
byte[] bytes = Files.readAllBytes(path); // 文件路径Path -> 二进制数组byte[]
// 将bytes转换成字符串
String content = new String(bytes, charset); // charset指定字符编码, 如StandardCharsets.UTF_8
// 一次按行读取文件所有内容
List<String> lines = Files.readAllLines(path);
/* 一次写所有文件内容 */
// 写一个字符串到文件
Files.write(path, content.getBytes(charset));
// 追加字符串到文件
Files.write(path, content.getBytes(charset),StandardOpenOption.APPEND);
// 写一个行的集合到文件
Files.write(path, lines);
- 大文件
要处理大文件和二进制文件,需要用到输入流/输出流,或者使用读入器/写入器。
InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream();
Reader reader = Files.newBufferedReader(path, charset);
Writer writer = Writer.newBufferedWriter(path, charset);
上面这些方法较单纯使用FileInputStream, FileOutputStream, BufferedReader, BufferedWriter更为简便。
例如,如果使用FileInputStream和FileOutputStream,需要这样使用
// read data from stream
try(DataInputStream in = new DataInputStream(new FileInputStream("filename"))) {
...
}
// write data to stream
try(DataOutputStream out = new DataOutputStream(new FileOutputStream("filename"))) {
...
}
简单来说,就是少了专门new的语句;对于Reader/Writer,还少了包装的语句。
复制文件
从一个位置复制到另外一个位置
Files.copy(fromPath, toPath); // fromPath和toPath都是Path对象, 如果目标路径已存在文件, 复制失败
Files.copy(fromPath, toPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); // 选项REPLACE_EXISTING表示想覆盖原有目标路径, COPY_ATTRIBUTES表示复制所有文件属性
Files.copy(inputStream, toPath); // 从输入流复制到目标路径
Files.copy(fromPath, outputStream); // 从源路径复制到输出流
移动文件
从一个位置移动到另外一个位置
Files.move(fromPath, toPath); // fromPath和toPath都是Path对象
Files.move(fromPath, toPath, StandardCopyOption.ATOMIC_MOVE); // ATOMIC_MOVE表示该操作是原子性的(要么成功移动到目标路径, 要么失败文件还在原来的位置)
// move操作无法从输入流到目标路径, 或者从源路径到输出流
删除文件
删除指定路径文件
Files.delete(path); // 如果指定路径不存在, 报异常NoSuchFileException
boolean deleted = Files.deleteIfExist(path); // 如果文件存在, 才会删除, 不会报异常. 可以用来删除空目录
获取文件信息
常用操作
boolean exists(path) // 文件存在?
boolean isHidden(path) // 文件隐藏?
boolean isReadable(path) // 文件可读?
boolean isWritable(path) // 文件可写?
boolean isExecutable(path) // 可执行?
boolean isRegularFile(path) // 是普通文件? 等价于!isSymbolicLink() && !isDirectory() && !isOther()
boolean isDirectory(path) // 是目录?
boolean isSymbolicLink(path) // 是符号链接?
long fileSize = Files.size(path); // 获取文件字节数
获取基本文件属性集
基本文件属性集主要包括:
- 创建文件、最后一次访问以及最后一次修改时间;
- 文件是常规文件、目录,还是符号链接;
- 文件尺寸;
- 文件主键,具体所属类与文件系统相关,有可能是文件唯一标识符,有可能不是;
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
PosixiFileAttributes posixAttributes = Files.readAttributes(path, PosixiFileAttributes.class); // 如果文件系统兼容POSIX, 才能获取到PosixiFileAttributes 实例
访问目录各项
- 遍历指定目录下各项
Files.list会返回Stream,而且是惰性读取,处理目录具有大量项时高效。不过,list不会进入子目录,进入子目录使用walk。使用示例
try(Stream<Path> entries = Files.list(dirPath)) { // 读取目录涉及需要关闭系统资源, 使用try块. 不进入子目录
entries.forEach(System.out::println); // 打印每个entries项, 也就是打印每个path
}
try(Stream<Path> entries = Files.walk(dirPath)) { // 会进入子目录
entries.forEach(System.out.println);
}
- 遍历并删除指定目录下各项
使用Files.walk遍历 得到Stream,再利用Stream的forEach方法对各项进行处理
/* 示例将当前目录rss下所有文件(包括目录)及子文件, 都复制到目录rss2下 */
Path source = Paths.get("rss"); // 根据实际情况设置字节的source路径
Path target = Paths.get("rss2");
try(Stream<Path> entries = Files.walk(source)) {
entries.forEach( p-> {
try{
Path q = target.resolve(source.relative(p)); // 取得p相对于source的相对路径后, 再拼接到target路径下. 相当于是说, 将每个文件相对路径都由source转移到target下
if(!Files.exists(q)) {
if(Files.isDirectory(q)) Files.createDirectory(q); // 如果是目录, 在target路径下, 根据相对路径创建对应目录
else Files.copy(p, q); // 如果是文件, 从source路径复制到target下
}
} catch(IOException e) {
e.printStackTrace();
}
});
}
目录流
使用Files.walk有一个缺陷:无法方便地删除目录,因为要删除父目录,必须先删除子目录。否则,会抛出异常。
使用File.newDirectoryStream对象,产生一个DirectoryStream,对遍历过程可以进行更细粒度控制。DirectoryStream不是Stream,而是专门用于目录遍历的接口。它是Iterable的子接口,可以用Iterable的迭代和增强forEach方法。
还可以搭配glob模式来过滤文件,示例是过滤出dir目录下 后缀名为 .java的文件:
try(DirectoryStream<Path> entries = Files.newDirectoryStream(dir, "*.java")){
for (Path entry: entries) {
Process entry
}
}
glob模式
模式 | 描述 | 示例 |
---|---|---|
* | 匹配路径组成部分中0个或多个字符串 | *.java 匹配当前目录中的所有java文件 |
** | 匹配跨目录边界的0个或多个字符串 | **.java 匹配在所有子目录中的java文件 |
? | 匹配一个字符 | ????.java 匹配所有4个字符的java文件(不含扩展名) |
[...] | 匹配一个字符集合, 可以使用连线字符[0-9]和取反字符[!0-9] | Test[0-9A-F].java 匹配Testx.java, 其中x是一个十六进制数 |
匹配由逗号隔开的多个可选项之一 | *.{java,class} 匹配所有的java文件和类class文件 | |
\ | 转义任意模式中的字符以及\字符 | *\** 匹配所有文件名中包含*的文件 |
注意:如果使用Windows,必须对glob的反斜杠转义两次:一次是glob语法转义,另外一次是java字符串转义:Files.newDirectoryStream(dir, "C:\\\\") // 相当于C:\\
访问目录所有子孙
如果想要访问某个目录下所有子孙,可以使用walkFileTree(),并向其传递一个FileVisitor对象。这个方法并非简单遍历,而是在遇到文件或目录时,目录被处理前后,访问文件错误时,FileVisitor会收到通知,然后指定执行方式:跳过该文件、跳过目录、跳过兄弟文件、终止访问。
// walkFileTree得到的通知:
FileVisitResult visitFile() // 遇到文件或目录时
FileVisitResult preVisitDirectory() // 一个目录被处理前
FileVisitResult postVisitDirectory() // 一个目录被处理后
FileVisitResult visitFileFailed() // 试图访问文件失败, 或目录发生错误时
// 收到通知后, 可以设置指定的操作
FileVisitResult.CONTINURE // 继续访问下一个文件
FileVisitResult.SKIP_SUBTREE // 继续访问, 但不再访问这个目录下任何文件
FileVisitResult.SKIP_SIBLINGS // 继续访问, 但不再访问这个文件的兄弟文件(同一个目录下的文件)
FileVisitResult.TERMINATE // 终止访问
便捷类SimpleFileVisitor + Files.walkFileTree()可以实现对目录的细粒度访问,并在在收到相关通知时,有机会进行相应处理。默认SimpleFileVisitor类实现FileVisitor接口,除visitFileFailed() 外(抛出异常并终止访问),其余方法都是直接继续访问,而不做任何处理。
注意:preVisitDirectory()和postVisitDirectory()通常需要覆盖,否则,访问时遇到不允许打开的目录或者不允许访问的文件时立即失败,进而直接跳转到visitFileFailed()
示例,展示如何打印给定目录下的所有子目录:
Files.walkFileTree(Paths.get("F:\\test"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println(dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("postVisitDirectory " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.SKIP_SUBTREE;
}
});
执行结果:
F:\test
F:\test\dir1
F:\test\dir1\subdir1
postVisitDirectory F:\test\dir1\subdir1
postVisitDirectory F:\test\dir1
F:\test\dir2
postVisitDirectory F:\test\dir2
F:\test\dir3
postVisitDirectory F:\test\dir3
postVisitDirectory F:\test
示例2,删除目录树(包括其中的文件)
利用walkFileTree访问到对应路径目录时,利用便捷类SimpleFileVisitor
Files.walkFileTree(Paths.get("F:\\test"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println(dir);
// 删除dir路径下所有文件(不包含子目录)
Files.list(dir).forEach(p->{
try {
if (!Files.isDirectory(p))
Files.delete(p);
} catch (IOException e) {
e.printStackTrace();
}
});
return FileVisitResult.CONTINUE;
}
// 删除目录
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("postVisitDirectory " + dir);
if (null != exc) throw exc;
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
} );
File类
创建文件
通过路径字符串创建
File file = new File("filePath"); // 在当前路径创建名为"filePath"的文件, 此时磁盘上还没有创建对应文件或目录
// 磁盘上创建文件或目录
File tempFile = File.createTempFile(prefix, suffix, directory); // 在指定目录或当前目录(directory缺省),创建以prefix/suffix为前缀/后缀的临时文件
file.createNewFile(); // 以file所代表路径, 创建新文件
file.mkdir(); // 以file所代表路径, 创建新目录, 注意mkdir()和createNewFile() 不能同时用同一个file所代表路径创建同名文件/目录对象, 否则后者无法创建
读写文件
单纯File类是没有办法直接读写文件的,这点跟Files是一个明显区别,File类需要借助输入流/输出流(FileInputStream/FileOutputStream),或者读入器/写出器(Reader/Writer)来实现读写功能。
示例是用DataInputStream和DataOutoutStream进行文件读写包装,支持字符本文以及二进制数据。使用Reader和Writer的方式,这里省略。感兴趣可参考系统学习 Java IO (十三)----字符读写 Reader/Writer 及其常用子类
// 写数据到文件file
try(DataOutputStream out = new DataOutputStream(new FileOutputStream((file)))) {
out.writeInt(1);
out.writeChar('a');
}
//从文件file读数据
try(DataInputStream in = new DataInputStream(new FileInputStream(file))) {
// read数据类型的个数和顺序一定要和write一致
int a = in.readInt();
char c = in.readChar();
System.out.println(a);
System.out.println(c);
}
复制和移动
复制和移动文件,File类没有专门的API,需要自行实现。这里只举一个复制的例子,移动可以看成是 复制+删除源文件。
/**
* 利用File类复制文件(包括目录)
* @note 要求文件只能是普通文件或者目录
*/
public static File copyFile(String dest, String src) throws IOException {
File destFile = new File(dest);
File srcFile = new File(src);
// 源文件不存在, 无法复制
if (!srcFile.exists()) {
System.out.println("源文件不存在, 路径: " + srcFile);
return null;
}
// 根据源文件类型, 在目标路径新建文件或目录
if (srcFile.isDirectory()) {
destFile.mkdir();
return destFile;
}
else srcFile.createNewFile();
// 处理源文件为文件(非目录)的情形
try(Scanner scanner = new Scanner(new FileInputStream(srcFile))) {
try(PrintWriter writer = new PrintWriter(new FileOutputStream(destFile))){
while (scanner.hasNext()) {
String line = scanner.nextLine();
writer.println(line);
}
}
}
return destFile;
}
删除文件
删除文件包括删除一般文件,还有目录。文件比较容易,直接调用file.delete()
即可,目录的情况较为复杂,因为涉及到目录不为空的情况。与Files.delete()
删除目录类似,必须确保带删除目录为空目录,也就是说需要遍历目录及其子目录,先删除目录下的内容,才能删除对应目录,这里不再遨述,后续有机会再补充完整这块示例。
获取文件信息
直接调用File对象接口,即可以查询到文件对应信息,主要包括
String getName() // 获取文件名称
String getParent() // 获取所在目录名称
File getParentFile() // 获取文件路径
boolean canRead() // 获取文件是否可读
boolean canWrite() // 获取文件是否可写
boolean exists() // 获取文件是否存在
boolean isDirectory() // 表示是否是一个目录
boolean isFile() // 表示是否是一个标注文件
long lastModified() // 最近一次修改
long length() // 文件长度
String[] list() // 路径名所表示的目录中的文件名列表
String[] list(FilenameFilter filter) // // 路径名所表示的目录中的文件名列表, 文件名经由filter过滤
String[] listFiles() // 路径名所表示的目录中的文件路列表
boolean setReadOnly() // 设置文件为只读