Java文件操作

182 阅读4分钟

本文内容来自《On Java基础卷》相关章节学习总结

1 文件和目录路径

Path对象表示一个文件或目录的路径。

java.nio.file.Paths类包含了重载的static get()方法,可以接收一个String序列,或者一个统一资源标识符(URI),将其转化为一个Path对象。

public class PathInfo {
  static void show(String id, Object p) {
    System.out.println(id + p);
  }
  static void info(Path p) {
    show("toString:\n ", p);
    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("files","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:\n" + u);
    Path puri = Paths.get(u);
    System.out.println(Files.exists(puri));
    File f = ap.toFile(); // Don't be fooled
  }
}

1.1 选择Path的片段

public class PartsOfPaths {
  public static void main(String[] args) {
    System.out.println(System.getProperty("os.name"));
    Path p =
      Paths.get("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) {
      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()));
  }
}

获取Path对象路径的各个部分时,可以使用getNameCount()方法拿到路径元素的数量,结合索引和getName()得到,也可以使用增强for循环来遍历(Path继承了Iterable接口)。

1.2 分析Path

Files工具类提供了一系列用于检查Path各种信息的方法。

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("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));
    }
}

1.3 添加或删除路径片段

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("files", "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"));
    }
}

Path提供了在路径中添加和删除某些路径片段来构建新Path对象的方法。

上述代码,创建了一个base基准路径,然后使用relativize()方法,从所有输入Path中删除了基准路径,通过resolve在一个Path对象后面添加路径片段。

2 目录

Files工具类包含了操作目录和文件所需的大部分操作。

2.1 删除多级目录

Files只提供了删除某个文件或空目录的方法,没有提供直接删除目录树的功能(类似rm -rf命令),需要自己实现:

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;
        }
    });
}

Files.walkFileTree()可以遍历每个子目录和文件,通过SimpleFileVisitor匿名内部类,可以对每个遍历的对象执行动作:

  • visitFile():在这个目录下的每个文件上运行
  • postVisitDirectory:先进入当前目录下的文件和目录,最后在当前目录上运行

3 文件系统

通过FileSystems工具,可以获得默认的文件系统。

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());
  }
}

4 监听Path

通过FileSystem可以得到一个WatchService,它使我们能够设置一个进程,对某个目录中的变化做出反应。

public class PathWatcher {
    static Path test = Paths.get("test");

    /**
     * 遍历目录树,删除文件名后缀为.txt的所有文件
     */
    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);
                        }
                    });//删除test目录下所有以.txt结尾的文件
        } 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();//生成WatchService
        test.register(watcher, ENTRY_DELETE);//将WatchService注册到Path路径,监听DELETE事件,注意,只监听test目录下的事件
        Executors.newSingleThreadScheduledExecutor()
                .schedule(
                        PathWatcher::delTxtFiles,
                        250, TimeUnit.MILLISECONDS);//执行删除文件操作
        WatchKey key = watcher.take();//阻塞等待目标事件发生,返回WatchKey
        for (WatchEvent evt : key.pollEvents()) {
            System.out.println(
                    "evt.context(): " + evt.context() +
                            "\nevt.count(): " + evt.count() +
                            "\nevt.kind(): " + evt.kind());
            System.exit(0);
        }
    }
}

上述代码中,将WatchService和感兴趣的DELETE事件一起注册到Path路径上,从而对该路径下的目标事件进行监听。调用的watcher.take()方法,主线程阻塞等待相应事件发生,返回一个包含WatchEvent的WatchKey。

监听事件类型包括ENTRY_CREATE、ENTRY_DELETE和ENTRY_MODIFY。

注意:

WatchService注册到Path路径上,只会监听这个目录,不会监听整个目录树。要想监听整个目录树,需要在整个树的每个子目录上设置一个WatchService,如下所示:

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("test"))
                .filter(Files::isDirectory)
                .forEach(TreeWatcher::watchDir);
        PathWatcher.delTxtFiles();
    }
}

5 查找文件

通过在FileSystem对象上调用getPathMatcher()方法,并传入glob或者regex表达式,来获得一个PathMatcher对象。调用该对象的match方法,可以对Path进行匹配。

public class Find {
    public static void main(String[] args) throws Exception {
        Path test = Paths.get("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}");//使用glob表达式匹配所有文件名以.tmp和.txt结尾的Path
        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);
    }
}

6 读写文件

java.nio.file.Files类包含了方便读写文本文件和二进制文件的工具函数。

Files.readAllLines()方法可以一次性读入整个文件,生成并返回一个List<String>

public class ListOfLines {
    public static void main(String[] args) throws Exception {
        Files.readAllLines(
                        Paths.get("files","../streams/Cheese.dat"))
                .stream()
                .filter(line -> !line.startsWith("//"))
                .map(line ->
                        line.substring(0, line.length() / 2))
                .forEach(System.out::println);
    }
}

Files提供了write重载方法,可以将byte数组或者实现了Iterable<? extends CharSequence>接口的对象写入文件。

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("bytes.dat"), bytes);//将byte数组写入文件
        System.out.println("bytes.dat: " +
                Files.size(Paths.get("bytes.dat")));

        // Write an iterable to a file:
        List<String> lines = Files.readAllLines(
                Paths.get("files","../streams/Cheese.dat"));
        Files.write(Paths.get("Cheese.txt"), lines);//将List对象写入文件
        System.out.println("Cheese.txt: " +
                Files.size(Paths.get("Cheese.txt")));
    }
}

注意:上述读取文件的方式,对文件的大小有要求:

  1. 如果文件非常大,那么一次性读取整个文件将占用大量内存;
  2. 如果只需要获取文件中的部分内容,那么读取整个文件是不必要的;

Files.lines()方法可以将一个文件变为由一个行组成的Stream。

Files.lines(Paths.get("PathInfo.java"))
  .skip(13)
  .findFirst()
  .ifPresent(System.out::println);

在一个流中完成文件读取、处理和文件写入:

try(
  Stream<String> input =
    Files.lines(Paths.get("StreamInAndOut.java"));
  PrintWriter output =
    new PrintWriter("StreamInAndOut.txt")
) {
  input
    .map(String::toUpperCase)
    .forEachOrdered(output::println);
} catch(Exception e) {
  throw new RuntimeException(e);
}