BufferedReader と Stream API
1 GB のログを読もうとして、プログラムが落ちる
前回の Files.readAllLines は、ファイル全体を List に詰めて返します。数 KB の設定ファイルなら申し分ありませんが、1 GB のアクセスログに同じことをすると、全行がメモリに載った時点で OutOfMemoryError になります。行を 1 つ見るたびに前の行は用済みなのに、最後まで全部抱え込んでしまうのが原因です。
Files.lines は、同じファイルを Stream<String> として返します。読むのは必要になった行だけで、処理が終わった行はそのまま捨てられます。手元に残るのは常に 1 行分だけなので、ファイルがどれだけ大きくてもメモリは増えません。
Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class Demo {
public static void main(String[] args) throws Exception {
Path p = Path.of("access.log");
try (Stream<String> lines = Files.lines(p)) {
long errors = lines.filter(line -> line.contains("ERROR")).count();
System.out.println(errors);
}
}
}1 行が Stream の 1 要素になるので、これまで使ってきた filter や map、count がそのまま通用します。改行が \n か \r\n かの判定は内側で済んでいて、こちら側で意識することはありません。
閉じ忘れると、あるとき突然 Too many open files で死ぬ
Stream というと、ただの値の流れに見えて閉じるものだと思いにくいのですが、Files.lines が返す Stream は裏でファイルを開いたまま握っています。放置すると OS のファイルディスクリプタが 1 つずつ残り、何百回か呼んだところで上限に達して Too many open files で落ちます。しかも落ちるのは、開きっぱなしにした場所ではなく、そのあとの何でもない処理です。
そのため Files.lines は必ず try の丸括弧の中で開きます。ブロックを抜けるときに自動で閉じられるので、途中で例外が出ても、return で抜けても取りこぼしがありません。
閉じる必要があるのは、このように外の世界とつながっている Stream だけです。メモリ上のデータから作った Stream は握っているものが無いので、丸括弧に入れる必要はありません。
Java
try (Stream<String> lines = Files.lines(p)) { // ファイルを握っている
// ...
}
names.stream().filter(s -> !s.isEmpty()).count(); // 握っていないので閉じない空行を数値に変換しようとして落ちる
Stream の途中に例外が出る処理を挟むときは、渡ってくる行を疑ってください。数値が 1 行に 1 つ並んだファイルでも、間に空行が 1 つ混ざっていれば Integer.parseInt("") が呼ばれ、NumberFormatException で止まります。手で編集されたファイルや、他のシステムが吐いたファイルでは十分ありえます。
対策は単純で、変換の前に filter で空行を落としておくことです。s.isEmpty() が true の行を通さないようにすれば、あとの処理は数字だけを相手にできます。
Stream<String>から数値の合計を出したいときは、mapToIntを通してIntStreamに変えます。IntStreamにはsumaveragemaxが最初から用意されているので、自分で合計用の変数を持ち回る必要がありません。
やってみよう
Solution.sumOfNumbers(String content) を完成させます。1 行に 1 つ整数が書かれた文字列を受け取り、その合計を返します。
- 一時ファイルを作り、受け取った
contentを書き込む - そのファイルを
Files.linesで開く。tryの丸括弧の中で開き、抜けるときに閉じること - 空行を除いてから整数に変換し、合計を
intでreturnする
sumOfNumbers("1\n2\n3") は 6、sumOfNumbers("") は 0 です。空文字列のときは行が 1 つも流れてこないので、合計は自然に 0 になります。
要件
Files.createTempFileとFiles.writeStringでcontentを一時ファイルに書き出すことtry-with-resourcesでFiles.lines(path)を開き、抜けるときにStreamを自動で閉じることfilterで空行を除外し、mapToInt(Integer::parseInt)とsum()を使ってintの合計を返すこと
入出力例
sumOfNumbers("1
2
3") → 6
sumOfNumbers("10") → 10
sumOfNumbers("") → 0
sumOfNumbers("100
200") → 300
sumOfNumbers("1
2
3
") → 6
sumOfNumbers("5
7
8") → 20