Java8 Stream文件,如何控制文件的关闭?

时间:2017-04-19 19:05:02

标签: java file java-8 java-stream

假设我有一个Java8 Stream<FileReader>并且我将该流用于map等等,我该如何控制流中使用的FileReader的关闭?

请注意,我可能无法访问单个FileReader,例如:

filenames.map(File::new)
    .filter(File::exists)
    .map(f->{
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(f));
        } catch(Exception e) {}
            return Optional.ofNullable(br);
        })
    .filter(Optional::isPresent)
    .map(Optional::get)
    .flatMap(...something that reads the file contents...) // From here, the Stream doesn't content something that gives access to the FileReaders

在做了其他一些映射之后,我终于在续集中失去了FileReader

我首先想到垃圾收集器能够在需要时执行此操作,但是当filenames为长Stream时,我遇到操作系统描述符耗尽。

5 个答案:

答案 0 :(得分:5)

关于FileReader使用的一般说明:FileReader在内部使用了一个覆盖finalize()的FileInputStream,因此discouraged to use因为它对garbarge集合的影响,特别是在处理大量文件时。 / p>

除非你在Java 7之前使用Java版本,否则你应该使用java.nio.files API,用

创建一个BufferedReader
 Path path = Paths.get(filename);
 BufferedReader br = Files.newBufferedReader(path);

因此,您的流管道的开头看起来应该更像

 filenames.map(Paths::get)
          .filter(Files::exists)
          .map(p -> {
        try {
            return Optional.of(Files.newBufferedReader(p));
        } catch (IOException e) {
            return Optional.empty();
        }
    }) 

现在问题:

选项1

保留原始Reader的一种方法是使用元组。元组(或其任何n元变体)通常是处理函数应用程序的多个结果的好方法,因为它在流管道中完成:

class ReaderTuple<T> {
   final Reader first;
   final T second;
   ReaderTuple(Reader r, T s){
     first = r;
     second = s;
   }
}

现在您可以将FileReader映射到一个元组,第二个项目是您当前的流项目:

 filenames.map(Paths::get)
  .filter(Files::exists)
  .map(p -> {
        try {
            return Optional.of(Files.newBufferedReader(p));
        } catch (IOException e) {
            return Optional.empty();
        }
    }) 
  .filter(Optional::isPresent)
  .map(Optional::get)
  .flatMap(r -> new ReaderTuple(r, yourOtherItem))
  ....
  .peek(rt -> {
    try { 
      rt.first.close()  //close the reader or use a try-with-resources
    } catch(Exception e){}
   })
  ... 

这种方法的问题是,无论何时在流执行期间发生未经检查的异常发生在flatMap和peek之间,读者可能都不会被关闭。

选项2

使用元组的另一种方法是将需要阅读器的代码放在try-with-resources块中。这种方法的优势在于您可以控制关​​闭所有读者。

示例1:

 filenames.map(Paths::get)
  .filter(Files::exists)
  .map(p -> {
        try (Reader r = new BufferedReader(new FileReader(p))){

            Stream.of(r)
            .... //put here your stream code that uses the stream

        } catch (IOException e) {
            return Optional.empty();
        }
    }) //reader is implicitly closed here
 .... //terminal operation here

示例2:

filenames.map(Paths::get)
  .filter(Files::exists)
  .map(p -> {
        try {
            return Optional.of(Files.newBufferedReader(p));
        } catch (IOException e) {
            return Optional.empty();
        }
    }) 
 .filter(Optional::isPresent)
 .map(Optional::get)
 .flatMap(reader -> {
   try(Reader r = reader) {

      //read from your reader here and return the items to flatten

   } //reader is implicitly closed here
  }) 

示例1具有读者肯定关闭的优点。示例2是安全的,除非您在创建读取器和可能失败的try-with-resources块之间添加更多内容。

我个人会选择示例1,并将访问阅读器的代码放在一个单独的函数中,以便代码更易读。

答案 1 :(得分:3)

或许更好的解决方案是使用Consumer<FileReader>来使用流中的每个元素。

如果有大量文件,您可能遇到的另一个问题是文件将同时打开。一旦完成,最好关闭每一个。

假设您将上面的代码更改为采用Consumer<BufferedReader>

的方法

我可能不会为此使用流,但我们可以使用一个来显示如何使用它。

public void readAllFiles( Consumer<BufferedReader> consumer){
    Objects.requireNonNull(consumer);

    filenames.map(File::new)
             .filter(File::exists)
             .forEach(f->{

                 try(BufferedReader br = new BufferedReader(new FileReader(f))){
                     consumer.accept(br);
                 } catch(Exception e) {
                     //handle exception
                 }
             });
}

通过这种方式,我们确保关闭每个读者,并且仍然可以支持用户想要的任何内容。

例如,这仍然有用

 readAllFiles( br -> System.out.println( br.lines().count()));

答案 2 :(得分:1)

因此,如果您只有非二进制文件,则可以使用以下内容:

List<String> fileNames = Arrays.asList(
            "C:\\Users\\wowse\\hallo.txt",
            "C:\\Users\\wowse\\bye.txt");

fileNames.stream()
            .map(Paths::get)
            .filter(Files::exists)
            .flatMap(path -> {
                try {
                    return Files.lines(path);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            })
            .forEach(System.out::println);

如果您有可以在内存中保存的二进制文件,则可以尝试以下方法。

fileNames.stream()
            .map(Paths::get)
            .filter(Files::exists)
            .map(path -> {
                try {
                    return Files.readAllBytes(path);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            })
            .filter(Objects::nonNull)
            .map(String::new)
            .forEach(System.out::println);

除此之外,我认为您必须使用一些包装类,我可以在javafx中建议Map.EntryPair,这样您就不必使用任何外部库。< / p>

答案 3 :(得分:0)

我知道这是一个古老的问题,但是有一个非常不错的解决方案,我发现here,但我认为这不是那么简单。

java.nio.file提供的方法可让您轻松进行此操作:

filenames.map(Paths::get)
    // Nicer alternative to File::exists
    .filter(Files::exists)
    // This will automatically close the stream after each file is done reading
    .flatMap(path -> {
        try {
            return Files.lines(path);
        } catch (IOException e) {
            // Seamlessly handles error opening file, no need for filtering
            return Stream.empty();
        }
    })
    .map(/* Do something with each line... */)

答案 4 :(得分:-1)

只是为了争论(虽然我同意路易斯的意见): 您可以传递原始Reader / InputStream(或任何对象,但您提供的案例实际上是有缺陷的编程,因为您可以传递FileReader而不是使用{{1}封装它使用common-lang3 BufferedReader类。 Jool也是一个提供Pair类的有效库。

示例:

Tuple*