当前位置: 首页 > 工具软件 > Streams > 使用案例 >

文件与streams

莘睿
2023-12-01

Java 7 大大简化了文件读写的基本操作,添加的新元素放在 java.nio.file 包下面,大家通常把 nio 中的 n 理解为 new,即新的io,现在更应该当成是 non-blocking :非阻塞 io(io 就是 input/output)。 java.nio.file 库终于将 Java 文件操作带到与其他编程语言相同的水平。
最重要的是 :Java 8 新增的 streams 与文件结合使得文件操作编程变得更加优雅。
先来了解文件操作的两个基本组件:

  1. 文件或者目录的路径;
  2. 文件本身。

1、文件和目录路径

一个 Path(路径) 对象表示一个文件或者目录的路径,是一个跨操作系统和文件系统的抽象,目的是在构造路径时不必关注底层操作系统,代码可以在不进行修改的情况下运行在不同的操作系统上
java.nio.file.Paths 类包含一个重载方法 static get() ,该方法接受一系列 String 字符串或者一个统一资源标识符(URI) 作为参数,并且进行转换返回一个 Path 对象:

package com.gui.demo.thingInJava.Files;

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;

/**
 * @Classname PathInfo
 * @Description
 * @Date 2021/8/30 11:00
 * @Created by gt136
 */
public class PathInfo {
    static void show(String id, Object path) {
        System.out.println(id + ": " + path);
    }
    static void info(Path path) {
        show("toString", path);
        show("Exists", Files.exists(path));
        show("RegularFile", Files.isRegularFile(path));
        show("Directory", Files.isDirectory(path));
        show("Absolute", path.isAbsolute());
        show("FileName", path.getFileName());
        show("Parent", path.getParent());
        show("Root", path.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\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\PathInfo.java");
//        Path p = Paths.get("C:","Users\\gt136\\Downloads\\demo\\src\\main\\java\\com\\gui\\demo\\thingInJava\\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 uri = p.toUri();
        System.out.println("URI: " + uri);
        Path puri = Paths.get(uri);
        System.out.println(Files.exists(puri));
        File f = ap.toFile();
    }
}
/*
outputs:
Windows 10
toString: C:\path\to\nowhere\NoFile.txt
Exists: true
RegularFile: true
Directory: false
Absolute: true
FileName: NoFile.txt
Parent: C:\path\to\nowhere
Root: C:\
*****************************
toString: src\main\java\com\gui\demo\thingInJava\Files\PathInfo.java
Exists: true
RegularFile: true
Directory: false
Absolute: false
FileName: PathInfo.java
Parent: src\main\java\com\gui\demo\thingInJava\Files
Root: null
*****************************
toString: C:\Users\gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\Files\PathInfo.java
Exists: true
RegularFile: true
Directory: false
Absolute: true
FileName: PathInfo.java
Parent: C:\Users\gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\Files
Root: C:\
*****************************
toString: C:\Users\gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\Files
Exists: true
RegularFile: false
Directory: true
Absolute: true
FileName: Files
Parent: C:\Users\gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava
Root: C:\
*****************************
toString: C:\Users\gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\Files\PathInfo.java
Exists: true
RegularFile: true
Directory: false
Absolute: true
FileName: PathInfo.java
Parent: C:\Users\gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\Files
Root: C:\
*****************************
URI: file:///C:/Users/gt136/Downloads/demo/src/main/java/com/gui/demo/thingInJava/Files/PathInfo.java
true
 */

main()的第一行用于展示操作系统的名称。
toString()用于生成完整形式的路径,你可以看到 getFileName()总是返回当前文件名。
通过使用 Files 工具类,可以测试一个文件是否存在,测试一个“普通”(RegularFile)文件还是一个目录等。
这里你会看到URI 看起来只能用于描述文件,实际上 URI 可以用于描述很多东西;我们之后将URI 成功地转为一个 Path 对象。
最后你会在 Path 中看到调用 toFile()方法会生成一个 File 对象。似乎可以得到一个类似文件的对象,但是这个方法的存在仅仅是为了向后兼容。虽然看上去应该被称为“路径”,实际上却应该表示目录或者文件本身。因为其疑惑性,我们将完全可以不用它,java.nio.file 就足够我们用了。

1.1 选取路径部分片段

Path 对象可以非常容易的生成路径的某一部分:

public class PartsOfPaths {
    public static void main(String[] args) {
        System.out.println(System.getProperty("os.name"));
        Path p = Paths.get("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()));
    }
}
/*
outputs:
Windows 10
Users
gt136
Downloads
demo
PartsOfPaths.java
ends with '.java': false
Users: false : false
gt136: false : false
Downloads: false : false
demo: false : false
PartsOfPaths.java: false : true
Starts with C:\ true
 */

可以通过getName()来索引 Path 的各个部分,直到达到上限 getNameCount()。
Path 也实现了 Iterator 接口,因此我们可以通过增强的 foreach 进行遍历。
请注意:即使路径以 .java 结尾,使用 endsWith() 方法也会返回 false。这是因为使用 endsWith() 比较的是整个路径部分,而不会包含文件路径的后缀
通过使用 startWith() 和 endsWith() 也可以完成路径的遍历。但是我们可以看到,遍历 Path 对象并不包含根路径,只有使用 startWith() 检测跟路径时才会返回 true。

1.2 路径分析 Files 工具类

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) {
        System.out.println(System.getProperty("os.name"));
        Path p = Paths.get("src\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\PathAnalysis.java").toAbsolutePath();
        say("Exists", Files.exists(p));
        say("Dirctory", Files.isDirectory(p));
        say("Executable", Files.isExecutable(p));
        say("Readable", Files.isReadable(p));
        say("RegularFile", Files.isRegularFile(p));
        say("Writeble", Files.isWritable(p));
        say("notExists", Files.notExists(p));
        try {
            say("Hidden", Files.isHidden(p));
            say("size", Files.size(p));
            say("FileStone", Files.getFileStore(p));
            say("LastModified: ", Files.getLastModifiedTime(p));
            say("Owner", Files.getOwner(p));
            say("ContentType",Files.probeContentType(p));

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        say("SymbolicLink", Files.isSymbolicLink(p));
        if (Files.isSymbolicLink(p)) {
            try {
                say("SymbolicLink", Files.readSymbolicLink(p));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
            try {
                say("PosixFilePermissions", Files.getPosixFilePermissions(p));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    }
}
/*
outputs:
Windows 10
Exists: true
Dirctory: false
Executable: true
Readable: true
RegularFile: true
Writeble: true
notExists: false
Hidden: false
size: 2072
FileStone: Windows (C:)
LastModified: : 2021-08-31T10:17:46.047597Z
Owner: LAPTOP-BCBU1OB4\gt136 (User)
ContentType: text/plain
SymbolicLink: false
 */

在调用最后一个 测试方法 getPosixFilePermissions()之前我们需要确认一下当前文件系统是否支持 Posix 接口,否则会抛出运行时异常。

1.3 Paths 的增减修改

我们必须能通过对 Path 对象增加或者删除一部分来构造一个新的 Path 对象。我们使用 relativize()移除 Path 的根路径,使用 resolve()添加 Path 的尾路径。

public class AddAndSubtractPaths {
    static Path basePath = Paths.get("..", "..", "..").toAbsolutePath().normalize();

    static void show(int id, Path result) {
        if (result.isAbsolute()) {
            System.out.println("(" + id + ")r " + basePath.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"));
        //输出根路径:C:\Users
        System.out.println(basePath);
        Path p = Paths.get("src\\main\\java\\com\\gui\\demo\\thingInJava\\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"));
    }
}
/*
outputs:
Windows 10
C:\Users
(1)r gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\Files\AddAndSubtractPaths.java
RealPath: C:\Users\gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\Files\AddAndSubtractPaths.java
(2)r gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\strings\..\Files
RealPath: C:\Users\gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\Files
(3)r gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\Files
RealPath: C:\Users\gt136\Downloads\demo\src\main\java\com\gui\demo\thingInJava\Files
(4)..\..
RealPath: C:\Users\gt136
(5)..\..
RealPath: C:\Users\gt136
(6)r gt136
RealPath: C:\Users\gt136
(7)r gt136\Downloads\demo\.\..\..
RealPath: C:\Users\gt136
(8)r gt136
RealPath: C:\Users\gt136
(9)r gt136\Downloads\demo
RealPath: C:\Users\gt136\Downloads\demo
(10)r gt136\Downloads\strings
java.nio.file.NoSuchFileException: C:\Users\gt136\Downloads\strings
(11)nonexistent
java.nio.file.NoSuchFileException: C:\Users\gt136\Downloads\demo\nonexistent
 */

我们使用 relativize()移除 Path 的根路径,使用 resolve()添加 Path 的尾路径。

2、目录:新增

Files 工具类包含了大部分我们需要的目录操作和文件操作方法,但是,没有包含删除目录树相关的方法,因此我们手动实现一个:

public class RmDir {
    public static void rmdir(Path directory) throws IOException {
        Files.walkFileTree(directory,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(),“walking”目录树意味着遍历每个子目录和文件。Visitor 设计模式提供了一种标准机制来访问集合中的每个对象,然后你需要提供在每个对象上执行的操作。此操作的定义取决于实现的 FileVisitor 的四个抽象方法,包括:

  1. preVisitDirectory():目录的条目被访问之前调用
  2. visitFile():调用目录中的某个文件。
  3. visitFileFailed():调用某个不能被访问的文件。
  4. postVisitDirectory():在目录中的条目及所有后代都被访问后调用目录。

为了简化,java.nio.file.SimpleFileVisitor 提供了所有方法的默认实现。
这样,上面的匿名内部类只需重写非标准行为的方法,两者都应该返回标志位并决定是否继续访问(直到找到所需要的)。
作为探索目录操作的一部分,现在我们可以有条件地删除已存在的目录。

public class Directories {
    //Path.get() 会返回 FileSystems.getDefault().getPath(first, more);
    static Path test = Paths.get("test");
    //FileSystems.getDefault()会返回抽象默认的文件系统
    static String sep = FileSystems.getDefault().getSeparator();
    //
    static List<String> parts = Arrays.asList("foo", "bar", "baz", "bag");

    static Path makeVariant() {
        //将容器中的 元素按照distance重新在容器中放置
        Collections.rotate(parts, 1);
        //join 方法将sep字符插入到容器的中间(不包括开头和结尾)
        return Paths.get("test", String.join(sep, parts));
    }

    static void refreshTestDir() throws IOException {
        if (Files.exists(test)) {
            //
            RmDir.rmdir(test);
        }
        if (!Files.exists(test)) {
            //
            Files.createDirectory(test);
        }
    }

    static void populateTestDir() throws IOException {
        for (int i = 0; i < parts.size(); i++) {
            Path variant = makeVariant();
            if (!Files.exists(variant)) {
                Files.createDirectories(variant);
                Files.copy(Paths.get("src\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\Directories.java"), variant.resolve("File.txt"));
                Files.createTempFile(variant, null, null);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        refreshTestDir();

        Files.createFile(test.resolve("Hello.txt"));
        Path variant = makeVariant();
        try {
            Files.createDirectory(variant);
        } catch (IOException 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);
    }
}
/*
outputs:
Nope, that doesn't work.
test\bag
test\bar
test\baz
test\DIR_1992295150508866935
test\foo
test\Hello.txt
**********************
test
test\bag
test\bag\foo
test\bag\foo\bar
test\bag\foo\bar\baz
test\bag\foo\bar\baz\3481383038818368384.tmp
test\bag\foo\bar\baz\File.txt
test\bar
test\bar\baz
test\bar\baz\bag
test\bar\baz\bag\foo
test\bar\baz\bag\foo\8713177723820086212.tmp
test\bar\baz\bag\foo\File.txt
test\baz
test\baz\bag
test\baz\bag\foo
test\baz\bag\foo\bar
test\baz\bag\foo\bar\5063471877667292040.tmp
test\baz\bag\foo\bar\File.txt
test\DIR_1992295150508866935
test\DIR_1992295150508866935\pre6897899870957080456.non
test\foo
test\foo\bar
test\foo\bar\baz
test\foo\bar\baz\bag
test\foo\bar\baz\bag\2502025422016235731.tmp
test\foo\bar\baz\bag\File.txt
test\Hello.txt
 */

makeVariant()接受基本目录测试,并通过旋转(通过Collections.rotate方法)部件列表生成不同的子目录路径。这些旋转与路径分隔符 sep 使用 String.join() 贴在一起,然后返回一个 Path 对象。
首先,refreshTestDir()用于检测 test 目录是否已经存在。若存在,则使用我们写的新的工具类 rmdir()删除整个目录。其实检查是否 exists 是多余的,因为如果对于已经存在的目录调用 createDirectory()将会抛出异常。createFile() 使用参数 Path 创建一个空文件,resolve()将文件名添加到 test Path 的末尾。
尝试使用 createDirectory()来创建多级路径,但是会报异常(这里显示的就是结果的第一行),因为这个方法只能创建单级路径。所以这里将populateTestDir()作为一个单独的方法,对于每一个变量 variant ,我们都能使用 createDirectories()创建完整的目录路径,然后使用此文件的副本以不同的目标名称填充该终端目录。然后使用createTempFile()生成一个临时文件。
在调用populateTestDir()后,test 目录下创建好了一个临时目录。请注意:createDirectory()只有名称的前缀选项。与createTempFile()不同,我们再次使用它将临时文件放入新的临时目录中。如果没有指定后缀,它将默认使用“.tmp”作为后缀。
为了展示结果,先使用了newDirectoryStream(),但是这个方法只是返回了test 目录内容的 Stream 流,并没有更多内容。要获取目录树的全部内容的流,请使用 File.walk()

文件系统

为了完整起见,我们需要一种方法查找文件系统相关的其他信息。在下面的代码中,我们使用静态的 FileSystem 工具类获取了默认的文件系统,但同样的你也可以在 Path 对象上调用 getFileSystem()以获取创建该 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"));
        //Returns:the default file system
        FileSystem fsys = FileSystems.getDefault();
        System.out.println(fsys);
        //遍历文件系统中的磁盘
        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());
    }
}
/*
outputs:
Windows 10
File Store: Windows (C:)
File Store: software (D:)
File Store: files (E:)
root directory: C:\
root directory: D:\
root directory: E:\
Separator: \
UserPrincipalLookupService: sun.nio.fs.WindowsFileSystem$LookupService$1@3feba861
isOpen: true
isReadOnly: false
FileSystemProvider: sun.nio.fs.WindowsFileSystemProvider@5b480cf9
File Attribute Views: [owner, dos, acl, basic, user]
 */

一个 FileSystem 对象也能生成 WatchService 和 PathMatcher 对象,接下来会细讲。

路径监听:WatchService

通过 WatchService 可以设置一个进程对目录中的更改作出响应。在下面的例子中,delTxtFiles()作为一个单独的任务执行,该任务将遍历整个目录并删除以 .txt 结尾的所有文件,而 WatchService 会对文件删除操作作出反应

import static java.nio.file.StandardWatchEventKinds.*;

/**
 * @Classname PathWatcher
 * @Description TODO
 * @Date 2021/9/2 11:26
 * @Created by gt136
 */
public class PathWatcher {
    static Path path = Paths.get("test");

    static void delTxtFiles() {
        try {
            Files.walk(path)
                    .filter(f->f.toString().endsWith(".txt"))//筛选出以.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) {
        try {
			//此处调用了Directories 类中的静态方法
            //如果路径存在则删除,否则新建新的
            Directories.refreshTestDir();
            //创建路径和写入文件内容
            Directories.populateTestDir();

            Files.createFile(path.resolve("Hello.txt"));
           //获取到监听器
            WatchService watcher = FileSystems.getDefault().newWatchService();
            //将监听器注册到 Path 路径上;第二个参数是指监听什么事件,这里指删除事件
            path.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);
            }
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }

    }
}
/*
outputs:
deleting test\bag\foo\bar\baz\File.txt
deleting test\bar\baz\bag\foo\File.txt
deleting test\baz\bag\foo\bar\File.txt
deleting test\foo\bar\baz\bag\File.txt
deleting test\Hello.txt
evt.context(): Hello.txt
evt.count(): 1
evt.kind(): ENTRY_DELETE
 */

delTxtFiles()中的 try 代码块看起来有些多余,因为它们捕获的是同一种类型的异常,外部的 try 语句似乎就够用了。但是,java 却要求必须这样写,这可能是一个 bug。还要注意的是filter()中,我们必须显式的使用 f.toString() 转为字符串,否则我们调用endWith()将会与整个 Path 对象进行比较
一旦我们从 FileSystem 中得到了 WatchService 对象,我们将其注册到 test 路径中,第二个参数是指监听的事件,可以选择 ENTRY_CREATE,ENTRY_DELETE或ENTRY_MODIFY(创建和删除不属于修改事件)。
因为接下来的对 watcher.take() 的调用会发生在某些事情之前停止所有操作,所以我们希望delTxtFiles()能够并行运行以便执行我们想要执行的操作。
为了实现这个目的,通过调用 Executors.newSingleThreadScheduledExecutor() 产生一个 ScheduledExecutorService 对象,然后调用schedule()方法传递函数所需的方法引用,并且设置运行之前应该等待的时间。
此时, watcher.take() 将等待并阻塞在这里。当目标事件发生时,会返回一个包含 WatchEvent 的 WatchKey 对象。这里展示的这三种方法是能对 WatchEvent 执行的所有操作。
查看输出的具体内容,会发现即使删除以 .txt 结尾的文件,但是 Hello.txt 也不会出发WatchEvent 操作。这是因为监听器只能监听给定的目录,而不是包含目录下的所有内容。如果要监听整个树目录,则必须在整个树的每个子目录上都放置一个 WatchEvent 。

import static java.nio.file.StandardWatchEventKinds.*;

/**
 * @Classname TreeWatcher
 * @Description 给路径下的每一级文件夹都设置监听器,来监听事件
 * @Date 2021/9/3 18:26
 * @Created by gt136
 */
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 IOException {
        Directories.refreshTestDir();
        Directories.populateTestDir();
        Files.walk(Paths.get("test"))
                .filter(Files::isDirectory)
                .forEach(TreeWatcher::watchDir);
        PathWatcher.delTxtFiles();
    }
}
/*
outputs:
deleting test\bag\foo\bar\baz\File.txt
evt.context(): File.txt
evt.count(): 1
evt.kind(): ENTRY_DELETE
deleting test\bar\baz\bag\foo\File.txt
 */

watchDir()方法中给 WatchService 提供参数 ENTRY_DELETE,并启动一个独立的线程来监控该 WatchService。这里没有使用 schedule()进行启动,而是使用 submit()启动,并将 watchDir()应用于每个子目录。现在,当我们运行 deltxtfiles()时,其中一个 WatchService 会检测每一个文件删除。

文件查找:PathMatcher

之前,我们为了找到文件,我们一直使用相当粗糙的方法,在 path 上调用toString(),然后使用 string 操作查看结果。
但是,java.nio.file 有更好的解决方案:通过在 FileSystem 对象上调用getPathMatcher()获得一个 PathMatcher ,然后传入你感兴趣的模式。模式有两个选项:glob 和 regex。
glob 比较简单,但是功能强大,因此你可以使用 glob 解决许多问题。如果你的问题更复杂,可以使用 regex。
在这里,我们使用 glob 查找以 .tmp 或 .txt 结尾的所有 Path:

public class Find {
    public static void main(String[] args) throws IOException {
        Path test = Paths.get("test");
        //如果路径存在则删除,否则新建新的
        Directories.refreshTestDir();
        //创建路径和写入文件内容
        Directories.populateTestDir();
        //在test后面新增文件夹
        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)
                .filter(Files::isRegularFile)
                .map(Path::getFileName)
                .filter(matcher2::matches)
                .forEach(System.out::println);
    }
}
/*
outputs:
test\bag\foo\bar\baz\4092140275367986776.tmp
test\bag\foo\bar\baz\File.txt
test\bar\baz\bag\foo\8648666806475045915.tmp
test\bar\baz\bag\foo\File.txt
test\baz\bag\foo\bar\469752048322624680.tmp
test\baz\bag\foo\bar\File.txt
test\dir.tmp
test\foo\bar\baz\bag\8022215776105811097.tmp
test\foo\bar\baz\bag\File.txt
*****************************
4092140275367986776.tmp
8648666806475045915.tmp
469752048322624680.tmp
dir.tmp
8022215776105811097.tmp
**********************************
4092140275367986776.tmp
8648666806475045915.tmp
469752048322624680.tmp
8022215776105811097.tmp
 */

在 matcher 中,glob 表达式开头的**/* 表示“当前目录及所有子目录”,这在当你不仅仅要匹配当前目录下特定结尾的 Path 时非常有用。一个 * 表示“任何东西”,然后是一个点,然后大括号表示一系列的可能性,如以.tmp或.txt 结尾的东西。
matcher2 只使用 *.tmp通常不匹配任何内容,但是添加map()操作会将完整路径减少到末尾的名称。
注意:在这两种情况下,输出中都会出现 dir.tmp,即使它是一个目录而不是一个文件。要查找文件,必须像在最后 file.walk()中那样对其进行筛选。

文件读写

如果一个文件很小,也就是说“它运行得足够快且占用内存很小”,那么 java.nio.file.Files 类中的使用程序将帮助你轻松读写文本和二进制文件

public class ListOfLines {
    public static void main(String[] args) throws IOException {
        Files.readAllLines(Paths.get("C:\\Users\\gt136\\Downloads\\Documents\\cheese.txt"))
                .stream()
                .filter(line -> !line.startsWith("//"))//去掉第一行
                .map(line -> line.substring(0, line.length() / 2))
                .forEach(System.out::println);
    }
}
/*
outputs:
Not much of a cheese
Finest in the
And what leads you
Well, it's
It's certainly uncon
 */

Files.readAllLines()一次读取整个文件(因此,小文件很有必要),产生一个 List<String>
代码过滤掉了注释行,其余的内容只打印一半。
你只需要将 Path 传递给 readAllLines()(以前很复杂)。 readAllLines()有一个重载版本,包含一个 Charset 参数来存储文件的 Unicode 编码。
Files.write() 被重载以写入 byte 数组或任何 Iterable 对象:

public class Writing {
    static Random rand = new Random(47);
    static final int SIZE = 1000;

    public static void main(String[] args) throws IOException {
        //写字节byte数据到文件中
        byte[] bytes = new byte[SIZE];
        //产生随机字节数组
        rand.nextBytes(bytes);
        //将 bytes 写入文件
        Files.write(Paths.get("bytes.dat"), bytes);
        System.out.println("bytes.dat: " + Files.size(Paths.get("bytes.dat")));

        //在文件中写入迭代
        List<String> lines = Files.readAllLines(Paths.get("C:\\Users\\gt136\\Downloads\\Documents\\cheese.txt"));
        //将list写入文件,任何 Iterator 对象也可以这么做
        Files.write(Paths.get("Cheese.txt"), lines);
        System.out.println("Cheese.txt: " + Files.size(Paths.get("Cheese.txt")));
    }
}
/*
outputs:
bytes.dat: 1000
Cheese.txt: 199
 */

我们使用 Random 来创建一个随机的 byte 数组,你可以看到生成的文件大小是 1000。
但是如果文件大小有问题怎么办?比如说:1. 文件太大,如果你一次性读完整个文件,你可能会耗尽内存。2. 你只需要获取文件的中间数据,读取整个文件会很浪费时间。
这时候就可以使用Files.lines()方便的将文件转化为行的流:

public class ReadLineStream {
    public static void main(String[] args) throws IOException {
        Files.lines(Paths.get("src\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\PathInfo.java"))
                .skip(22)
                .findFirst()
                .ifPresent(System.out::println);
    }
}
/*
outputs:
        show("RegularFile", Files.isRegularFile(path));
 */

它对 PathInfo.java 做了流式处理,跳过了前22行,直接读取的第23行,并将其打印出来。
Files.lines()对于把文件处理行的处理非常有用,但是如果你现在想要在 Stream 中读取、处理或写入该怎么办呢?这就要写稍微复杂的代码:

public class StreamInAndOut {
    public static void main(String[] args) {
        //构建了输入输出流
        try (Stream<String> input = Files.lines(Paths.get("src\\main\\java\\com\\gui\\demo\\thingInJava\\Files\\StreamInAndOut.java"));
        PrintWriter output = new PrintWriter("StreamInAndOut.txt"))
        {
            //将输入流中的字符串全部大写
            input.map(String::toUpperCase)
                    .forEachOrdered(output::println);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

PrintWriter 是一个老的 java.io 类,允许你将数据“打印”到一个文件。

 类似资料: