こんにちわ、みけです。
表題、何言ってるかわかりませんね。
ファイルを読み込んで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; } }
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); }
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
が呼ばれれば、
カウントは終端操作の数と等しくなるはずです。
実行してみます。
見事、真っ赤です。
結論
Files#lines
でStream<String>
を受け取るときはtry-with-resourcesの中で取りましょう。
Special Thanks
ソースコードをいろいろと読んで教えてくれた
- @backpaper0さん
- @uehajさん
- @bitter_foxさん
- @megascusさん
とツイッターでしゃべっている間にわかりました。
また、『Java SE8 実践プログラミング』(Cay S.Horstmann著、柴田芳樹訳、インプレス出版2014)を読んでいる間にわかりました。
StreamがAutoClosableであることを今更知る事案
— もちだって人らしい (@mike_neck) 2014, 10月 12
try(Stream<String> lines = Files.lines(path)) {
//do something
}
みたいなのできるらしい
— もちだって人らしい (@mike_neck) 2014, 10月 12
以上