mike-neckのブログ

Java or Groovy or Swift or Golang

StreamはAutoCloseableであると認識していないとアレな件

こんにちわ、みけです。

表題、何言ってるかわかりませんね。

ファイルを読み込んでStream<String>を扱うときに、

try-with-resourcesでStream<String>を宣言しておかないと、

リソースリークがあるかもしれないということです。

具体的には

    return Files.lines(path, StandardCharsets.UTF_8)
            .filter(line -> line.startsWith("ERROR")
            .map(LogText::new)
            .collect(Collectors.toList());

とやっていると、ファイルの閉じ忘れが発生するので、

        try(Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8) {
            return stream.filter(line -> line.startsWith("ERROR"))
                    .map(LogText::new)
                    .collect(Collectors.toList());
        }

とやらないとリソースリークが発生する恐れがあります。


コードを読む

Files#lines(Path, Charset)ソースコードは次のようになっています。

    public static Stream<String> lines(Path path, Charset cs) throws IOException {
        BufferedReader br = Files.newBufferedReader(path, cs);
        try {
            return br.lines().onClose(asUncheckedRunnable(br));
        } catch (Error|RuntimeException e) {
            try {
                br.close();
            } catch (IOException ex) {
                try {
                    e.addSuppressed(ex);
                } catch (Throwable ignore) {}
            }
            throw e;
        }
    }

see

BufferedReaderはtry-with-resourcesで囲まれていません。

したがって、ユーザー側が意識的にcloseする必要があります。

そして、終端操作(ここではcollect)ではこのようになっています。

    public final <R, A> R collect(Collector<? super P_OUT, A, R> collector) {
        A container;
        if (isParallel()
                && (collector.characteristics().contains(Collector.Characteristics.CONCURRENT))
                && (!isOrdered() || collector.characteristics().contains(Collector.Characteristics.UNORDERED))) {
            container = collector.supplier().get();
            BiConsumer<A, ? super P_OUT> accumulator = collector.accumulator();
            forEach(u -> accumulator.accept(container, u));
        }
        else {
            container = evaluate(ReduceOps.makeRef(collector));
        }
        return collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)
               ? (R) container
               : collector.finisher().apply(container);
    }

see

Streamの終端操作ではcloseが呼ばれることはないようです。

テスト

ここで、試しに僕が書いたテストを実行してみます。

    private static final String REGEX = "[\\P{L}]+";

    private String text;

    private void doNothing(String arg) {}

    private Comparator<String> comparator = (s1, s2) -> s1.length() - s2.length();

    @Test
    public void testStreamIsClosedOnTerminalOperation() {
        final AtomicInteger count = new AtomicInteger(0);
        List<Consumer<Stream<String>>> list = Arrays.asList(
                st -> st.forEach(this::doNothing),
                st -> st.forEachOrdered(this::doNothing),
                st -> st.allMatch(String::isEmpty),
                st -> st.anyMatch(String::isEmpty),
                (Consumer<Stream<String>>) Stream::count,
                Stream::findAny,
                Stream::findFirst,
                st -> st.max(comparator),
                st -> st.min(comparator),
                st -> st.collect(Collectors.toList()),
                st -> st.reduce(0, (total, s) -> total + s.length(), (left, right) -> left + right)
        );
        Iterator<Consumer<Stream<String>>> iterator = list.iterator();
        final Pattern pattern = Pattern.compile(REGEX);
        Stream.generate(() -> pattern.splitAsStream(text).onClose(count::incrementAndGet))
                .limit(list.size())
                .forEach(stream -> iterator.next().accept(stream));
        assertThat(count.get(), is(list.size()));
    }

終端操作全部用意して、終端操作の数だけStream<String>を生成して、

closedが呼ばれた時に実行される処理にカウントをインクリメントする処理を

追加しておきます。

もし、終端操作でcloseが呼ばれれば、

カウントは終端操作の数と等しくなるはずです。

実行してみます。

f:id:mike_neck:20141012132553p:plain

見事、真っ赤です。

結論

Files#linesStream<String>を受け取るときはtry-with-resourcesの中で取りましょう。

Special Thanks

ソースコードをいろいろと読んで教えてくれた

ツイッターでしゃべっている間にわかりました。

また、『Java SE8 実践プログラミング』(Cay S.Horstmann著、柴田芳樹訳、インプレス出版2014)を読んでいる間にわかりました。


以上