在非常难用的文件I/O编程存在多年之后,Java终于简化了文件读写的基本操作
本文将研究操作文件的连个基本组件:
- [文件或目录]的路径
- 文件本身
1、文件或目录路径
Path对象代表一个文件或目录的路径,它是不同操作系统和文件系统之上的抽象。
java.nio.file.Paths类包含了重载的static.get()方法,可以接受一个String序列或URI,然后将其转化为Path对象。
import java.nio.file.*;
import java.net.URI;
import java.io.File;
import java.io.IOException;
public class PathInfo {
static void show(String id, Object p) {
System.out.println(id + p);
}
static void info(Path p) {
show("toString:\n ", p);
//Files工具类
show("Exists: ", Files.exists(p));
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("src/files/PathInfo.java");
info(p);
Path ap = p.toAbsolutePath();
info(ap);
info(ap.getParent());
try {
//对于不区分大小写的文件系统,将返回原始大小写Path
info(p.toRealPath());
} catch(IOException e) {
System.out.println(e);
}
URI u = p.toUri();
System.out.println("URI:\n" + u);
Path puri = Paths.get(u);
System.out.println(Files.exists(puri));
//实际上也是路径(类似于Path),向后兼容的妥协
File f = ap.toFile(); // Don't be fooled
}
}
/* Output:
Windows 10
toString:
C:\path\to\nowhere\NoFile.txt
Exists: false
RegularFile: false
Directory: false
Absolute: true
FileName: NoFile.txt
Parent: C:\path\to\nowhere
Root: C:\
******************
toString:
src\files\PathInfo.java
Exists: true
RegularFile: true
Directory: false
Absolute: false
FileName: PathInfo.java
Parent: src\files
Root: null
******************
toString:
F:\IdeaProjects\OnJava\src\files\PathInfo.java
Exists: true
RegularFile: true
Directory: false
Absolute: true
FileName: PathInfo.java
Parent: F:\IdeaProjects\OnJava\src\files
Root: F:\
******************
toString:
F:\IdeaProjects\OnJava\src\files
Exists: true
RegularFile: false
Directory: true
Absolute: true
FileName: files
Parent: F:\IdeaProjects\OnJava\src
Root: F:\
******************
toString:
F:\IdeaProjects\OnJava\src\files\PathInfo.java
Exists: true
RegularFile: true
Directory: false
Absolute: true
FileName: PathInfo.java
Parent: F:\IdeaProjects\OnJava\src\files
Root: F:\
******************
URI:
file:///F:/IdeaProjects/OnJava/src/files/PathInfo.java
true
*/
1.1、选择Path的片段
import java.nio.file.*;
public class PartsOfPaths {
public static void main(String[] args) {
System.out.println(System.getProperty("os.name"));
Path p =
Paths.get("src/files/PartsOfPaths.java").toAbsolutePath();
for(int i = 0; i < p.getNameCount(); i++)
System.out.println(p.getName(i));
System.out.println("ends with '.java': " +
p.endsWith(".java"));//比较的是完整的路径组件
for(Path pp : p) {//或者结合getNameCount()与getName(n)
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
IdeaProjects
OnJava
src
files
PartsOfPaths.java
ends with '.java': false
IdeaProjects: false : false
OnJava: false : false
src: false : false
files: false : false
PartsOfPaths.java: false : true
Starts with F:\ true
*/
1.2、分析Path
Files工具类用于检查Path的各种信息
import java.nio.file.*;
import java.io.IOException;
public class PathAnalysis {
static void say(String id, Object result) {
System.out.print(id + ": ");
System.out.println(result);
}
public static void
main(String[] args) throws IOException {
System.out.println(System.getProperty("os.name"));
Path p =
Paths.get("src/files/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));
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: 1443
FileStore: DevelopData (F:)
LastModified: : 2023-03-29T03:24:28.7453995Z
Owner: DESKTOP-M8M3I1T\Hampix (User)
ContentType: text/plain
SymbolicLink: false
*/
1.3、添加删除路径片段
relativize()用于去掉Path的基准路径(此例中为base),只有Path是绝对路径是生效。
resolve()用于在Path后面增加路径片段。
import java.nio.file.*;
import java.io.IOException;
public class AddAndSubtractPaths {
static Path base = Paths.get("..", "..", "..")
.toAbsolutePath()
.normalize();
static void show(int id, Path result) {
if(result.isAbsolute())
System.out.println("(" + id + ")r " +
base.relativize(result));
else
System.out.println("(" + id + ") " + result);
try {
System.out.println("RealPath: "
+ result.toRealPath());
} catch(IOException e) {
System.out.println(e);
}
}
public static void main(String[] args) {
System.out.println(System.getProperty("os.name"));
System.out.println(base);
Path p = Paths.get("AddAndSubtractPaths.java")
.toAbsolutePath();
show(1, p);
Path convoluted = p.getParent().getParent()
.resolve("strings")
.resolve("..")
.resolve(p.getParent().getFileName());
show(2, convoluted);
show(3, convoluted.normalize());
Path p2 = Paths.get("..", "..");
show(4, p2);
show(5, p2.normalize());
show(6, p2.toAbsolutePath().normalize());
Path p3 = Paths.get(".").toAbsolutePath();
Path p4 = p3.resolve(p2);
show(7, p4);
show(8, p4.normalize());
Path p5 = Paths.get("").toAbsolutePath();
show(9, p5);
show(10, p5.resolveSibling("strings"));
show(11, Paths.get("nonexistent"));
}
}
/* Output:
Windows 10
C:\Git
(1)r OnJava8\ExtractedExamples\files\AddAndSubtractPaths.java
RealPath:
C:\Git\OnJava8\ExtractedExamples\files\AddAndSubtractPaths.java
(2)r OnJava8\ExtractedExamples\files
RealPath: C:\Git\OnJava8\ExtractedExamples\files
(3)r OnJava8\ExtractedExamples\files
RealPath: C:\Git\OnJava8\ExtractedExamples\files
(4) ..\..
RealPath: C:\Git\OnJava8
(5) ..\..
RealPath: C:\Git\OnJava8
(6)r OnJava8
RealPath: C:\Git\OnJava8
(7)r OnJava8
RealPath: C:\Git\OnJava8
(8)r OnJava8
RealPath: C:\Git\OnJava8
(9)r OnJava8\ExtractedExamples\files
RealPath: C:\Git\OnJava8\ExtractedExamples\files
(10)r OnJava8\ExtractedExamples\strings
RealPath: C:\Git\OnJava8\ExtractedExamples\strings
(11) nonexistent
java.nio.file.NoSuchFileException:
C:\Git\OnJava8\ExtractedExamples\files\nonexistent
*/
2、目录
Files工具类包含了操作目录和文件的大部分操作,但是没有删除目录树的工具,咱们创建一个。
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
public class RmDir {
public static void rmdir(Path dir)
throws IOException {
Files.walkFileTree(dir,
new SimpleFileVisitor<Path>() {
@Override public FileVisitResult
visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult
postVisitDirectory(Path dir, IOException exc)
throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
}
下面探索创建和填充目录
package files;
import java.util.*;
import java.nio.file.*;
import onjava.RmDir;
public class Directories {
static Path test = Paths.get("src/files/test");
static String sep =
FileSystems.getDefault().getSeparator();
static List<String> parts =
Arrays.asList("foo", "bar", "baz", "bag");
static Path makeVariant() {
Collections.rotate(parts, 1);
return Paths.get("src/files/test", String.join(sep, parts));
}
static void refreshTestDir() throws Exception {
if(Files.exists(test))
RmDir.rmdir(test);
if(!Files.exists(test))
//如果目录存在将抛出异常
Files.createDirectory(test);
}
public static void
main(String[] args) throws Exception {
refreshTestDir();
Files.createFile(test.resolve("Hello.txt"));
Path variant = makeVariant();
// Throws exception (too many levels):
try {
//只能用于创建单层目录,所以抛出异常。
//populateTestDir()的Files.createDirectories(variant)才能正常创建多层目录
Files.createDirectory(variant);
} catch(Exception e) {
System.out.println("Nope, that doesn't work.");
}
populateTestDir();
Path tempdir =
Files.createTempDirectory(test, "DIR_");
Files.createTempFile(tempdir, "pre", ".non");
//只显示一层目录
Files.newDirectoryStream(test)
.forEach(System.out::println);
System.out.println("*********");
Files.walk(test).forEach(System.out::println);
}
static void populateTestDir() throws Exception {
for(int i = 0; i < parts.size(); i++) {
Path variant = makeVariant();
if(!Files.exists(variant)) {
Files.createDirectories(variant);
Files.copy(Paths.get("src/files/Directories.java"),
variant.resolve("File.txt"));
Files.createTempFile(variant, null, null);
}
}
}
}
/*
Nope, that doesn't work.
src\files\test\bag
src\files\test\bar
src\files\test\baz
src\files\test\DIR_16351143387970971436
src\files\test\foo
src\files\test\Hello.txt
*********
src\files\test
src\files\test\bag
src\files\test\bag\foo
src\files\test\bag\foo\bar
src\files\test\bag\foo\bar\baz
src\files\test\bag\foo\bar\baz\2288129667382909257.tmp
src\files\test\bag\foo\bar\baz\File.txt
src\files\test\bar
src\files\test\bar\baz
src\files\test\bar\baz\bag
src\files\test\bar\baz\bag\foo
src\files\test\bar\baz\bag\foo\10024225869691822436.tmp
src\files\test\bar\baz\bag\foo\File.txt
src\files\test\baz
src\files\test\baz\bag
src\files\test\baz\bag\foo
src\files\test\baz\bag\foo\bar
src\files\test\baz\bag\foo\bar\12798335110278345190.tmp
src\files\test\baz\bag\foo\bar\File.txt
src\files\test\DIR_16351143387970971436
src\files\test\DIR_16351143387970971436\pre3449890123475405278.non
src\files\test\foo
src\files\test\foo\bar
src\files\test\foo\bar\baz
src\files\test\foo\bar\baz\bag
src\files\test\foo\bar\baz\bag\9307929951700391831.tmp
src\files\test\foo\bar\baz\bag\File.txt
src\files\test\Hello.txt
Process finished with exit code 0
*/
3、文件系统
import java.nio.file.*;
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:)
File Store: FunData (E:)
File Store: DevelopData (F:)
File Store: LearnData (G:)
File Store: Backup Plus (H:)
Root Directory: C:\
Root Directory: D:\
Root Directory: E:\
Root Directory: F:\
Root Directory: G:\
Root Directory: H:\
Separator: \
UserPrincipalLookupService: sun.nio.fs.WindowsFileSystem$LookupService$1@7b23ec81
isOpen: true
isReadOnly: false
FileSystemProvider: sun.nio.fs.WindowsFileSystemProvider@6acbcfc0
File Attribute Views: [owner, dos, acl, basic, user]
Process finished with exit code 0
*/
4、监听Path
WatchService能开一个进程,对某一目录的变化做出反应
package files;
import java.io.IOException;
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.util.concurrent.*;
public class PathWatcher {
static Path test = Paths.get("src/files/test");
static void delTxtFiles() {
try {
Files.walk(test)
.filter(f ->
f.toString().endsWith(".txt"))
.forEach(f -> {
try {
System.out.println("deleting " + f);
Files.delete(f);
} catch(IOException e) {
throw new RuntimeException(e);
}
});
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public static void
main(String[] args) throws Exception {
Directories.refreshTestDir();
Directories.populateTestDir();
Files.createFile(test.resolve("Hello.txt"));
WatchService watcher =
FileSystems.getDefault().newWatchService();
//只监听此目录,而不是目录树
test.register(watcher, ENTRY_DELETE);
Executors.newSingleThreadScheduledExecutor()
.schedule(
PathWatcher::delTxtFiles,
250, TimeUnit.MILLISECONDS);
//阻塞等待事件
WatchKey key = watcher.take();
for(WatchEvent evt : key.pollEvents()) {
System.out.println(
"evt.context(): " + evt.context() +
"\nevt.count(): " + evt.count() +
"\nevt.kind(): " + evt.kind());
System.exit(0);
}
}
}
/*
deleting src\files\test\bag\foo\bar\baz\File.txt
deleting src\files\test\bar\baz\bag\foo\File.txt
deleting src\files\test\baz\bag\foo\bar\File.txt
deleting src\files\test\foo\bar\baz\bag\File.txt
deleting src\files\test\Hello.txt
evt.context(): Hello.txt
evt.count(): 1
evt.kind(): ENTRY_DELETE
Process finished with exit code 0
*/
import java.io.IOException;
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.util.concurrent.*;
public class TreeWatcher {
static void watchDir(Path dir) {
try {
WatchService watcher =
FileSystems.getDefault().newWatchService();
dir.register(watcher, ENTRY_DELETE);
Executors.newSingleThreadExecutor().submit(() -> {
try {
WatchKey key = watcher.take();
for(WatchEvent evt : key.pollEvents()) {
System.out.println(
"evt.context(): " + evt.context() +
"\nevt.count(): " + evt.count() +
"\nevt.kind(): " + evt.kind());
System.exit(0);
}
} catch(InterruptedException e) {
return;
}
});
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public static void
main(String[] args) throws Exception {
Directories.refreshTestDir();
Directories.populateTestDir();
Files.walk(Paths.get("src/files/test"))
.filter(Files::isDirectory)
.forEach(TreeWatcher::watchDir);
PathWatcher.delTxtFiles();
}
}
/*
deleting src\files\test\bag\foo\bar\baz\File.txt
deleting src\files\test\bar\baz\bag\foo\File.txt
deleting src\files\test\baz\bag\foo\bar\File.txt
deleting src\files\test\foo\bar\baz\bag\File.txt
evt.context(): File.txt
evt.count(): 1
evt.kind(): ENTRY_DELETE
*/
5、查找文件
上面都是通过toString(),然后用String的各种操作查看结果。可以使用PathMatcher,通过FileSystem对象上调用getPathMatcher()获得,有glob和regex两种模式。
import java.nio.file.*;
public class Find {
public static void
main(String[] args) throws Exception {
Path test = Paths.get("src/files/test");
Directories.refreshTestDir();
Directories.populateTestDir();
// Creating a *directory*, not a file:
Files.createDirectory(test.resolve("dir.tmp"));
// "**/“表示所有子目录,*表示任意,{}表示一些列可能性
PathMatcher matcher = FileSystems.getDefault()
.getPathMatcher("glob:**/*.{tmp,txt}");
Files.walk(test)
.filter(matcher::matches)
.forEach(System.out::println);
System.out.println("***************");
PathMatcher matcher2 = FileSystems.getDefault()
.getPathMatcher("glob:*.tmp");
Files.walk(test)
.map(Path::getFileName)
.filter(matcher2::matches)
.forEach(System.out::println);
System.out.println("***************");
Files.walk(test) // Only look for files,不包含目录
.filter(Files::isRegularFile)
.map(Path::getFileName)
.filter(matcher2::matches)
.forEach(System.out::println);
}
}
/*
src\files\test\bag\foo\bar\baz\1371568932388083606.tmp
src\files\test\bag\foo\bar\baz\File.txt
src\files\test\bar\baz\bag\foo\146766178072502416.tmp
src\files\test\bar\baz\bag\foo\File.txt
src\files\test\baz\bag\foo\bar\15935029429709749912.tmp
src\files\test\baz\bag\foo\bar\File.txt
src\files\test\dir.tmp
src\files\test\foo\bar\baz\bag\15790184490602227067.tmp
src\files\test\foo\bar\baz\bag\File.txt
***************
1371568932388083606.tmp
146766178072502416.tmp
15935029429709749912.tmp
dir.tmp
15790184490602227067.tmp
***************
1371568932388083606.tmp
146766178072502416.tmp
15935029429709749912.tmp
15790184490602227067.tmp
Process finished with exit code 0
*/
6、读写文件
Files.readAllLinews()一次性读取整个文件,返回一个List<String>。
小文件。
import java.util.*;
import java.nio.file.*;
public class ListOfLines {
public static void
main(String[] args) throws Exception {
Files.readAllLines(
Paths.get("src/streams/Cheese.dat"))
.stream()
//跳过注释行
.filter(line -> !line.startsWith("//"))
.map(line ->
line.substring(0, line.length()/2))
.forEach(System.out::println);
}
}
/* Output:
Not much of a cheese
Finest in the
And what leads you
Well, it's
It's certainly uncon
*/
Files.write()
import java.util.*;
import java.nio.file.*;
public class Writing {
static Random rand = new Random(47);
static final int SIZE = 1000;
public static void
main(String[] args) throws Exception {
// Write bytes to a file:
byte[] bytes = new byte[SIZE];
rand.nextBytes(bytes);
Files.write(Paths.get("src/files/bytes.dat"), bytes);
System.out.println("bytes.dat: " +
Files.size(Paths.get("src/files/bytes.dat")));
// Write an iterable to a file:
List<String> lines = Files.readAllLines(
Paths.get("src/streams/Cheese.dat"));
Files.write(Paths.get("src/files/Cheese.txt"), lines);
System.out.println("Cheese.txt: " +
Files.size(Paths.get("src/files/Cheese.txt")));
}
}
/* Output:
bytes.dat: 1000
Cheese.txt: 199
*/
Files.lines()
大文件
import java.nio.file.*;
public class ReadLineStream {
public static void
main(String[] args) throws Exception {
Files.lines(Paths.get("src/files/PathInfo.java"))
.skip(13)
.findFirst()
.ifPresent(System.out::println);
}
}
/*
//Files工具类
*/
在一个流中完成读取、处理、写入
import java.io.*;
import java.nio.file.*;
import java.util.stream.*;
public class StreamInAndOut {
public static void main(String[] args) {
try(
Stream<String> input =
Files.lines(Paths.get("src/files/StreamInAndOut.java"));
PrintWriter output =
new PrintWriter("src/files/StreamInAndOut.txt")
) {
input
.map(String::toUpperCase)
.forEachOrdered(output::println);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}