Статьи

Подводный камень Java 8 — Остерегайтесь Files.lines ()

В Java8 есть действительно хорошая новая функция, которая позволяет вам получить поток строк из файла в один слой.

1
List lines = Files.lines(path).collect(Collectors.toList());

Вы можете манипулировать потоком так же, как и любым другим потоком, например, вы можете захотеть отфильтровать () или map () или limit () или skip () и т. Д. Я начал использовать это во всем коде, пока не столкнулся с этим исключением ,

01
02
03
04
05
06
07
08
09
10
11
12
Caused by: java.nio.file.FileSystemException: /tmp/date.txt: Too many open files in system
 at sun.nio.fs.UnixException.translateToIOException(UnixException.java:91)
 at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:102)
 at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:107)
 at sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:214)
 at java.nio.file.Files.newByteChannel(Files.java:361)
 at java.nio.file.Files.newByteChannel(Files.java:407)
 at java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:384)
 at java.nio.file.Files.newInputStream(Files.java:152)
 at java.nio.file.Files.newBufferedReader(Files.java:2784)
 at java.nio.file.Files.lines(Files.java:3744)
 at java.nio.file.Files.lines(Files.java:3785)

По какой-то причине у меня было слишком много открытых файлов! Странно, разве Files.lines () не закрывает файл?

Посмотрите код ниже ( run3() ), где я создал, воспроизвел проблему:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package utility;
 
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.stream.Stream;
 
public class Test2 {
    public static void main(String[] args) throws IOException{
        int times = 100_000;
 
        Path path = Paths.get("/tmp", "date.txt");
        Test2 t2 = new Test2();
        t2.setDate(path);
 
        for (int i = 0; i < times; i++) {
            t2.run1(path);
        }
        for (int i = 0; i < times; i++) {
            t2.run2(path);
        }
        for (int i = 0; i < times; i++) {
            t2.run3(path);  //throws exception too many files open
        }
        System.out.println("finished");
    }
 
    public String run1(Path path){
        try(BufferedReader br = new BufferedReader(new FileReader(path.toFile()))){
            return br.readLine();
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }
 
    public String run2(Path path){
        try(Stream<String> stream = Files.lines(path)) {
            return stream.findFirst().get();
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }
 
    public String run3(Path path) throws IOException{
        return Files.lines(path).findFirst().get();
    }
 
    public void setDate(Path path) {
        try (FileWriter writer = new FileWriter(path.toFile())){
            writer.write(new Date().toString());
            writer.flush();
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }
}

Мой код выглядел примерно так, как run3() которая run3() исключение. Я доказал это, выполнив команду Unix lsof (выводит список открытых файлов) и заметил множество примеров открытия date.txt. Чтобы убедиться, что проблема действительно в Files.lines() я убедился, что код run1() с run1() с использованием BufferedReader , что он и сделал. Прочитав исходный код для Files я понял, что поток должен быть создан в автоматическом закрытии . Когда я реализовал это в run2() код снова run2() нормально.

На мой взгляд, я не думаю, что это не особенно интуитивно понятно. Это действительно портит один лайнер, когда вы должны использовать автоматическое закрытие. Я предполагаю, что коду нужен сигнал о том, когда закрывать файл, но было бы неплохо, если бы он был скрыт от нас. По крайней мере, это должно быть выделено в JavaDoc, что не является 🙂