Java8 教程

Java8 文件增强

Java 8 为使用流读取文件行及访问目录项提供了一些简便的方法。

Files.lines 方法

为了延迟读取一个文件中的行,你可以使用 Files.lines 方法。它会产生一个包含字符串的流,每个字符串就是文件的一行,方法定义如下:

  • static Stream<String> lines(Path path)

  • static Stream<String> lines(Path path, Charset cs)

lines 方法将以流形式读取文件中的所有行,与 readAllLines 不同的是,该方法不会将所有行读取到 List 中,而是随着流的消耗而缓慢填充。

文件中的字节将使用指定的字符集解码为字符,并支持与 readAllLines 所指定的相同的行结束符。

此方法返回后,在从文件读取数据时发生的任何后续 I/O 异常,或在读取畸形或不可应用的字节序列时发生的任何 I/O 异常,都将被封装为一个 UncheckedIOException 异常,该异常将从导致读取数据的 Stream 方法中抛出。如果在关闭文件时出现 IOException,它也会被封装为 UncheckedIOException。

返回的流封装了一个 Reader。如果需要及时处理文件系统资源,应使用 try-with-resources 结构确保在流操作完成后调用流的关闭方法。

注意:Files 类只包含对文件、目录或其他类型文件进行操作的静态方法。在大多数情况下,这里定义的方法将委托相关文件系统提供程序执行文件操作。

示例:读取一个文件,过滤含有“love”关键字的行,并输出。假如在 classpath 下面存在 lyric.txt 文件,内容如下:

《My Heart Will Go On》(我心永恒)—— Celine Dion
Every night in my dreams 每一个寂静夜晚的梦里
I see you, I feel you 我都能看见你,触摸你
That is how I know you go on 因此而确信你仍然在守候
Far across the distance 穿越那久远的时空距离
And spaces between us 你轻轻地回到我的身边
You have come to show you go on 告诉我,你仍然痴心如昨
Near, far, wherever you are 无论远近亦或身处何方
I believe that the heart does go on 我从未怀疑过心的执著
Once more you open the door 当你再一次推开那扇门
And you're here in my heart 清晰地伫立在我的心中
And my heart will go on and on 我心永恒,我心永恒
Love can touch us one time 爱曾经在刹那间被点燃
And last for a lifetime 并且延续了一生的传说
And never let go till we're gone 直到我们紧紧地融为一体
Love was when I loved you 爱曾经是我心中的浪花
One true time I hold to 我握住了它涌起的瞬间
In my life we'll always go on 我的生命,从此不再孤单

客户端代码:

package com.hxstrive.jdk8.file;

import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * File 增强
 * @author hxstrive.com
 */
public class FileDemo1 {

    public static void main(String[] args) throws Exception {
        URL url = FileDemo1.class.getClassLoader().getResource("lyric.txt");
        if(Objects.isNull(url)) {
            System.err.println("文件不存在");
            return;
        }

        try(Stream<String> stream = Files.lines(Paths.get(url.toURI()))) {
            stream.filter(r -> r.contains("love"))
                    .collect(Collectors.toList()).forEach(System.out::println);
            // 结果:
            // Love was when I loved you 爱曾经是我心中的浪花
        } catch (Exception e) {
            System.out.println("读取文件出错");
            e.printStackTrace();
        }
    }

}

上面介绍了 Files.lines 方法不会将所有行读取到 List 中,而是随着流的消耗而缓慢填充。例如:

URL url = FileDemo2.class.getClassLoader().getResource("lyric.txt");
if(Objects.isNull(url)) {
    System.err.println("文件不存在");
    return;
}

try(Stream<String> stream = Files.lines(Paths.get(url.toURI()))) {
    stream.filter(r -> {
                System.out.println("查找:" + r);
                return r.contains("night");
            })
            .findFirst()
            .ifPresent(System.out::println);
    // 结果:
    //查找:《My Heart Will Go On》(我心永恒)—— Celine Dion
    //查找:Every night in my dreams 每一个寂静夜晚的梦里
    //Every night in my dreams 每一个寂静夜晚的梦里
} catch (Exception e) {
    System.out.println("读取文件出错");
    e.printStackTrace();
}

上面示例,一旦找到包含“night”关键字的第一行,那么文件中的其他行就不会再被读取。

注意:与 FileReader 类不同,Files.lines 方法会默认以 UTF-8 字符编码打开文件,而 FileReader 会以本地字符编码打开文件,从而导致可移植性问题。你也可以通过提供一个 Charset 参数来指定其他的编码。

你也许会希望关闭底层的文件,但前面中已经知道流不需要关闭任何资源(因为 Stream 接口继承了 AutoClosable 类)。但是 Files.lines 方法会产生一个需要调用 close 方法来关闭文件的流。确保文件关闭的最简单方法是使用 Java7 的 try-with-resources 语法,例如:

try(Stream<String> stream = Files.lines(Paths.get(url.toURI()))) {
    //...
} catch (Exception e) {
    System.out.println("读取文件出错");
    e.printStackTrace();
}
// 这里首先会关闭流,然后紧接着关闭文件

当一个流调用其他流时,close 方法也会被递归调用。因此,你还可以编写如下代码:

try(Stream<String> stream = Files.lines(Paths.get(url.toURI()))) {
    // 这里有意产生了一个新的流
    Stream<String> newStream = stream.filter(Objects::nonNull);
    newStream.filter(r -> {
                System.out.println("查找:" + r);
                return r.contains("night");
            })
            .findFirst()
            .ifPresent(System.out::println);
} catch (Exception e) {
    System.out.println("读取文件出错");
    e.printStackTrace();
}

当 stream 流被关闭时,它会关闭底层的流,从而关闭底层的文件。

注意:如果你希望当流被关闭时收到通知,你可以附加一个 onClose 方法。下面代码可以用来确定 stream 流关闭时的确关闭了底层的流:

try(Stream<String> stream = Files.lines(Paths.get(url.toURI()))
        .onClose(() -> System.out.println("stream 被关闭"))) {
    // 这里有意产生了一个新的流
    Stream<String> newStream = stream.filter(Objects::nonNull)
            .onClose(() -> System.out.println("newStream 被关闭"));
    newStream.filter(r -> {
                System.out.println("查找:" + r);
                return r.contains("night");
            })
            .findFirst()
            .ifPresent(System.out::println);
    // 结果:
    //查找:《My Heart Will Go On》(我心永恒)—— Celine Dion
    //查找:Every night in my dreams 每一个寂静夜晚的梦里
    //Every night in my dreams 每一个寂静夜晚的梦里
    //stream 被关闭
    //newStream 被关闭
} catch (Exception e) {
    System.out.println("读取文件出错");
    e.printStackTrace();
}

如果当流读取文件行时发生了一个 IOException, 那么该异常会被包装为一个 UncheckedIOException 异常,并被流操作抛出。

如果你希望按行读取文件以外的其他来源,可以考虑使用 BufferedReader.lines方法。

try(BufferedReader reader = new BufferedReader(new InputStreamReader(ur1.openStream()))){
    Stream<String> lines = reader.lines();
    ...
}

通过使用该方法,你可以只关闭流,而不关闭 reader。出于该原因,你必须将 BufferedReader 对象 (而非 Stream对象) 放在 try 语句的头部。

注意:大概在十年前,Java5 引入了 Scanner 类来代替笨重的 BufferedReader 类。不幸的是,Java8 的API 设计者们决定将 lines 方法添加到 BufferedReader 类中,而没有添加到 Scanner 类。

Files.list 方法

静态方法 Files.list 可以返回一个读取指定目录中项目的 Stream<Path> 对象。由于会延迟读取目录,所以可以高效地处理拥有大量项目的目录。方法定义如下:

  • static Stream<Path> list(Path dir)  返回一个懒散填充的流,其中的元素是目录中的条目。

由于读取目录会涉及关闭一个系统资源,所以应该使用 try 语句。假如项目下面存在如下目录:

6be3fac5df215ac2b77cbff1ddc68ee9_1722094331277-ebd99d5b-c4da-4b72-950f-f37b1d7982db_x-oss-process=image%2Fformat%2Cwebp.png

此时我们使用 Files.list 方法获取该目录的下面文件和目录名称:

try(Stream<Path> stream = Files.list(Paths.get("D:\\demo_jdk8\\src\\main\\resources"))
        .onClose(() -> System.out.println("stream 被关闭"))) {
    stream.forEach(p -> {
        System.out.println(p.getFileName());
    });
    //结果:
    //demo.txt
    //demo2.js
    //demo4.js
    //demo6.js
    //lyric.txt
    //sub
    //stream 被关闭
} catch (Exception e) {
    System.out.println("读取文件出错");
    e.printStackTrace();
}

注意:在实现底层,流会使用一个 DirectoryStream 对象,它是 Java7 中为了高效遍历大目录所引入的。该接口与 Java8 的流没有任何关系,但是由于它实现了 Iterable 接口,所以可以用于增强的 for 循环中。

try(DirectoryStream<Path> stream = Files.newDirectoryStream(
        Paths.get("D:\\demo_jdk8\\src\\main\\resources"))) {
    for(Path path : stream) {
        System.out.println(path.getFileName());
    }
    //结果:
    //demo.txt
    //demo2.js
    //demo4.js
    //demo6.js
    //lyric.txt
    //sub
} catch (Exception e) {
    System.out.println("读取文件出错");
    e.printStackTrace();
}

注意,Files.list 方法不会进入子目录。要处理目录下的所有子目录,应该使用 Files.walk 方法,方法定义如下:

  • static Stream<Path> walk(Path start, FileVisitOption... options)  通过以给定起始文件为根对文件树进行遍历,返回一个用 Path 缓慢填充的流。

  • static Stream<Path> walk(Path start, int maxDepth, FileVisitOption... options)  通过以给定起始文件为根对文件树进行遍历,仅遍历指定的 maxDepth 深度,返回一个用 Path 缓慢填充的流。

例如:

try(Stream<Path> stream = Files.walk(
    Paths.get("D:\\$work_demo\\jdk8\\demo_jdk8\\src\\main\\resources"),3)) {
    stream.forEach(System.out::println);
    //结果:
    //D:\demo_jdk8\src\main\resources
    //D:\demo_jdk8\src\main\resources\demo.txt
    //D:\demo_jdk8\src\main\resources\demo2.js
    //D:\demo_jdk8\src\main\resources\demo4.js
    //D:\demo_jdk8\src\main\resources\demo6.js
    //D:\demo_jdk8\src\main\resources\lyric.txt
    //D:\demo_jdk8\src\main\resources\sub
    //D:\demo_jdk8\src\main\resources\sub\sub1.txt
    //D:\demo_jdk8\src\main\resources\sub\sub2.txt
} catch (Exception e) {
    System.out.println("读取文件出错");
    e.printStackTrace();
}

你可以通过 Files.walk(pathToRoot, depth) 来限制要访问的目录树的深度。这两个 walk 方法都可以接受一个 FileVisitoption 类型的可变参数,但是目前只有一个选项可供选择:按照符号链接读取的 FOLLOW_LINKS

注意:如果你打算对 walk 返回的路径进行过滤,并且过滤条件中包含了目录中存储的文件属性,例如大小、创建时间或者类型 (文件、目录、符号链接), 那么请使用 find 方法来代替 walk 方法。在调用 find 方法时需要传递一个 Predicate 函数,该函数的参数分别为一个路径及一个 BasicFileAttributes 对象。使用find 方法的唯—好处就是效率。反正都是在读取目录,所以你可以直接使用这些属性。

说说我的看法
全部评论(
没有评论
关于
本网站属于个人的非赢利性网站,转载的文章遵循原作者的版权声明,如果原文没有版权声明,请来信告知:hxstrive@outlook.com
公众号