- JAVA
Java BufferedReaderの使い方|ファイルを1行ずつ読む
BufferedReaderとは、文字をまとめて読み込んでメモリにためておき、そこから1行ずつ取り出せるようにするJavaの標準クラスです。 ファイルでも標準入力でも、readLine() を呼ぶたびに1行ぶんの文字列が返り、最後まで読むと null が返ります。いま書くなら Files.newBufferedReader(Path, Charset) を try-with-resources で開く形 が、短くて文字コードの取り違えにも気づきやすい書き方です。
この記事では、BufferedReader の作り方4通りを同じファイルで動かして比べ、Shift_JIS のファイルを UTF-8 として読んだときに何が起きるか、標準入力の読み方、BOM 付き UTF-8 の先頭文字、Scanner との速度差までを、すべて実行結果つきで紹介します。コードはそのまま貼り付けて java ファイル名.java で動く完成形にしています。
確認環境:Ubuntu 24.04 / OpenJDK 21.0.10(ロケールは C.UTF-8)。掲載している出力はすべてこの環境で実行したものです。
BufferedReaderの早見表
まず、やりたいことから書き方を引ける表です。
| やりたいこと | 書き方 | ポイント |
|---|---|---|
| ファイルを1行ずつ読む(おすすめ) | Files.newBufferedReader(path, StandardCharsets.UTF_8) |
文字コードが合わないと例外で知らせてくれる |
| Shift_JIS のファイルを読む | Files.newBufferedReader(path, Charset.forName("Shift_JIS")) |
Windows の機種依存文字を含むなら "MS932" |
| 標準入力を1行ずつ読む | new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)) |
文字コードを明示する |
| 行を Stream で処理する | br.lines() |
filter・map がそのまま使える |
| 最後まで読んだか判定する | while ((line = br.readLine()) != null) |
終端は null、空行は "" |
| 全行をリストで受け取る | Files.readAllLines(path) |
小さなファイル向け |
| 閉じ忘れを防ぐ | try (BufferedReader br = ...) { } |
ブロックを抜けると自動で close |
作り方4通りの違いは次のとおりです。
| 作り方 | 文字コードの指定 | 指定しないときの文字コード | 合わない文字コードで読むと | 使える JDK |
|---|---|---|---|---|
new BufferedReader(new FileReader(名前)) |
引数を足せば可(JDK 11 以降) | デフォルト文字セット(JDK 18 以降は UTF-8) | 文字が「�」に置き換わる | 1.1 以降 |
new BufferedReader(new InputStreamReader(入力, 文字コード)) |
可 | デフォルト文字セット | 文字が「�」に置き換わる | 1.1 以降 |
Files.newBufferedReader(path, 文字コード) |
可 | 常に UTF-8 | MalformedInputException で止まる | 7 以降(文字コード省略は 8 以降) |
br.lines()(上のどれかに続けて呼ぶ) |
元の作り方に従う | 元の作り方に従う | 元の作り方に従う | 8 以降 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java BufferedReaderの使い方|ファイルを1行ずつ読む」
BufferedReaderが何をしているか
Java でテキストを読むクラスは、役割ごとに重ねて使う作りになっています。
| クラス | 役割 |
|---|---|
FileInputStream |
ファイルからバイト列を読む |
InputStreamReader |
バイト列を、文字コードに従って文字に変換する |
FileReader |
上の2つをまとめた、ファイル専用の文字読み込み |
BufferedReader |
文字をまとめて読んでためておき、readLine() で1行ずつ返す |
FileReader だけでも read() で1文字ずつ読めますが、「1行ぶん」を取り出すメソッドは持っていません。readLine() と lines() を使えること、そしてまとめて読むので速いことが、BufferedReader で包む理由です。速さの差は後半の計測で数字を示します。
公式の API ドキュメント(Java SE 21)は、BufferedReader を「文字、配列、行を効率よく読めるように文字をバッファリングする」クラスと説明しており、バッファの大きさは指定もできるが、既定の大きさでほとんどの用途に足りるとしています。
ファイルを1行ずつ読む4通りの書き方
以降のサンプルは、次の3行が入った items.txt(UTF-8)を同じフォルダに置いて実行しています。
備品A,10
備品B,25
備品C,7
書き方1:FileReaderを包む
入門書でもよく見かける、もっとも基本の形です。
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Read1 {
public static void main(String[] args) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("items.txt"))) {
String line;
int no = 1;
while ((line = br.readLine()) != null) {
System.out.println(no++ + ": " + line);
}
}
}
}
$ java Read1.java
1: 備品A,10
2: 備品B,25
3: 備品C,7
while ((line = br.readLine()) != null) は、「1行読んで line に入れ、それが null でなければ続ける」という意味です。代入と比較を1つの式にまとめた定番の書き方なので、形ごと覚えてしまうと便利です。
try ( ... ) { } は try-with-resources という構文で、ブロックを抜けるときに br.close() を自動で呼んでくれます。BufferedReader を閉じると、内側の FileReader も一緒に閉じられます。
この書き方は文字コードを書いていないので、Java のデフォルト文字セットでファイルを読みます。JDK 18 以降はそれが UTF-8 です(詳しくは後述)。
書き方2:InputStreamReaderで文字コードを指定する
バイト列を文字に変える係の InputStreamReader に、文字コードを渡す形です。ファイル以外(標準入力、ネットワーク、ZIP の中身など)でも同じ形で使えるのが強みです。
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class Read2 {
public static void main(String[] args) throws IOException {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("items.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
String[] cols = line.split(",");
System.out.println(cols[0] + " の数量は " + cols[1]);
}
}
}
}
$ java Read2.java
備品A の数量は 10
備品B の数量は 25
備品C の数量は 7
書き方3:Files.newBufferedReaderを使う(おすすめ)
java.nio.file.Files のメソッドを使うと、3つのクラスを重ねる部分が1行になります。
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class Read3 {
public static void main(String[] args) throws IOException {
Path path = Path.of("items.txt");
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
int total = 0;
while ((line = br.readLine()) != null) {
total += Integer.parseInt(line.split(",")[1]);
}
System.out.println("合計: " + total);
}
}
}
$ java Read3.java
合計: 42
文字コードの引数を省いた Files.newBufferedReader(path) は、JDK のバージョンや OS に関係なく常に UTF-8 で読みます。また、文字コードが合わないバイト列に出会うと例外で止まるので、文字化けしたデータを気づかないまま処理し続けることがありません。この2点が、書き方1・2との大きな違いです。
Path.of(...) は JDK 11 で入ったメソッドです。JDK 8 では Paths.get("items.txt") と書きます。
書き方4:lines()でStreamとして処理する
br.lines() は、各行を要素とする Stream を返します。絞り込みや変換を続けて書けるので、集計や抽出がすっきりします。
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class Read4 {
public static void main(String[] args) throws IOException {
try (BufferedReader br = Files.newBufferedReader(Path.of("items.txt"))) {
List<String> names = br.lines()
.map(line -> line.split(","))
.filter(cols -> Integer.parseInt(cols[1]) >= 10)
.map(cols -> cols[0])
.toList();
System.out.println(names);
}
}
}
$ java Read4.java
[備品A, 備品B]
lines() は必要になったぶんだけ読み進める作りなので、大きなファイルでも全体をメモリに載せずに処理できます。toList() は JDK 16 以降のメソッドで、それより前の版では collect(Collectors.toList()) と書きます。
4通りの選び方
| 状況 | 選ぶ書き方 | 理由 |
|---|---|---|
| これから新しく書く | 書き方3(Files.newBufferedReader) |
短く、文字コードの取り違えを例外で知らせてくれる |
| 絞り込み・集計をしたい | 書き方3+書き方4(lines()) |
Stream の操作をそのまま使える |
| 標準入力やソケットなど、ファイル以外を読む | 書き方2(InputStreamReader) |
どんな入力ストリームにも使える |
既存コードが FileReader で書かれている |
書き方1のまま、文字コードの引数を足す | 変更が最小で済む |
readLine()の戻り値を正しく扱う
改行文字は含まれない
readLine() が返す文字列には、行末の改行文字が含まれません。公式ドキュメントでは、行の終わりを「ラインフィード(\n)、キャリッジリターン(\r)、キャリッジリターンの直後にラインフィードが続くもの、またはファイルの終端」のいずれかと定めています。つまり Windows 形式(CRLF)でも Unix 形式(LF)でも、同じコードで読めます。
改行の種類を混ぜた文字列で確かめます。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
public class Newline {
public static void main(String[] args) throws IOException {
String text = "LF\nCRLF\r\nCR\r\n最後の行";
try (BufferedReader br = new BufferedReader(new StringReader(text))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println("[" + line + "] 長さ=" + line.length()
+ " 空行=" + line.isEmpty());
}
System.out.println("読み終わったあとの readLine(): " + br.readLine());
}
}
}
$ java Newline.java
[LF] 長さ=2 空行=false
[CRLF] 長さ=4 空行=false
[CR] 長さ=2 空行=false
[最後の行] 長さ=4 空行=false
読み終わったあとの readLine(): null
どの行も、長さは文字の数だけで、改行のぶんは数えられていません。最後の行は改行で終わっていなくても1行として返ります。この例のように、BufferedReader はファイル専用ではなく、StringReader など Reader であれば何でも包めます。テストで読み込み処理を確かめるときに便利です。
nullと空文字の違い
readLine() の戻り値 |
意味 | 判定の書き方 |
|---|---|---|
null |
もう読む行がない(終端に達した) | line == null |
""(空文字) |
何も書かれていない行(空行) | line.isEmpty() |
| 空白だけの文字列 | スペースやタブだけの行 | line.isBlank()(JDK 11 以降) |
空行でループを終えたいときに line != null だけで判定していると、空行もそのまま処理に流れます。空行を読み飛ばしたいなら、ループの先頭に if (line.isEmpty()) continue; を入れます。
元のファイルの改行をそのまま保ちたい場合(改行コードを変えずに写したいなど)は、readLine() ではなく read(char[]) でまとめて読むか、Files.readString(path) でファイル全体を1つの文字列として受け取ります。
文字コードを指定して読む
Shift_JISのファイルをUTF-8として読むとどうなるか
Windows のアプリケーションが出力した CSV には、Shift_JIS で書かれたものが今もあります。これを UTF-8 として読んだときの挙動は、作り方によって2通りに分かれます。
まず、検証用に Shift_JIS のファイル sjis.txt を作ります。
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
public class MakeSjis {
public static void main(String[] args) throws IOException {
Files.writeString(Path.of("sjis.txt"), "備品A,10\n備品B,25\n", Charset.forName("Shift_JIS"));
}
}
od -c で中身を見ると、「備品」が UTF-8 とは違うバイト列になっていることがわかります。
$ java MakeSjis.java
$ od -c sjis.txt
0000000 224 365 225 i A , 1 0 \n 224 365 225 i B , 2
0000020 5 \n
0000022
パターンA:FileReader で読む(文字が置き換わる)
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.Charset;
public class Sjis1 {
public static void main(String[] args) throws IOException {
System.out.println("デフォルト: " + Charset.defaultCharset());
try (BufferedReader br = new BufferedReader(new FileReader("sjis.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
}
$ java Sjis1.java
デフォルト: UTF-8
���iA,10
���iB,25
例外は出ず、UTF-8 として解釈できなかったバイトが「�」(U+FFFD、置換文字)に置き換わりました。プログラムは最後まで動くので、文字化けしたまま後続の処理へ進む点に注意が必要です。
パターンB:Files.newBufferedReader で読む(例外で止まる)
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Sjis2 {
public static void main(String[] args) throws IOException {
try (BufferedReader br = Files.newBufferedReader(Path.of("sjis.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
}
$ java Sjis2.java
Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 1
at java.base/java.nio.charset.CoderResult.throwException(CoderResult.java:279)
at java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:405)
(中略)
at java.base/java.io.BufferedReader.readLine(BufferedReader.java:436)
at Sjis2.main(Sjis2.java:10)
こちらは MalformedInputException: Input length = 1 で止まります。「指定された文字コードとしては正しくないバイトが1つあった」という意味です。このエラーを見たら、まずファイルの文字コードと、読み込み側の指定が合っているかを確かめます。
lines() を使っている場合、同じ原因のエラーは UncheckedIOException に包まれて届きます。
書き方4の br.lines().forEach(System.out::println) を catch (UncheckedIOException e) で受け、e.getCause() を出力した結果が次です。
lines() の中で発生: java.nio.charset.MalformedInputException: Input length = 1
直し方:正しい文字コードを渡す
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
public class Sjis3 {
public static void main(String[] args) throws IOException {
Charset sjis = Charset.forName("Shift_JIS");
try (BufferedReader br = Files.newBufferedReader(Path.of("sjis.txt"), sjis)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
}
$ java Sjis3.java
備品A,10
備品B,25
既存の FileReader を活かすなら、JDK 11 で追加された文字コードつきのコンストラクタ new FileReader("sjis.txt", sjis) が使えます。JDK 21 で同じ結果になることを確認しました。なお、このコンストラクタを含むコードを javac --release 8 でコンパイルすると次のエラーになり、JDK 8 向けには使えないことがわかります。
Fr11.java:9: error: no suitable constructor found for FileReader(String,Charset)
try (BufferedReader br = new BufferedReader(new FileReader("sjis.txt", sjis))) {
^
JDK 8 でも動かす必要があるコードは、書き方2(InputStreamReader に文字コードを渡す)か書き方3を使います。
2つのパターンをまとめると次のようになります。
| 観点 | FileReader / InputStreamReader | Files.newBufferedReader |
|---|---|---|
| 合わないバイトに出会ったとき | 「�」に置き換えて読み進める | MalformedInputException を投げる |
| 気づきやすさ | 出力を目で見るまで気づきにくい | その場で止まるのですぐわかる |
| 向いている場面 | 多少の欠けを許して最後まで読みたいとき | データを正確に取り込みたいとき |
JDK 18以降はデフォルトがUTF-8になった
JDK 18 で導入された JEP 400「UTF-8 by Default」により、標準 API のデフォルト文字セットが UTF-8 に統一されました。JEP 400 には、デフォルト文字セットに依存する API が、実装・OS・ロケール・設定の違いによらず一貫して動くようになる、と書かれています。影響を受ける API として挙げられているのは、InputStreamReader、FileReader、OutputStreamWriter、FileWriter、PrintStream、Formatter、Scanner、URLEncoder、URLDecoder です。一方、java.nio.file.Files の読み書きメソッドは、以前から文字コードの引数がなければ常に UTF-8 を使う仕様です。
現在の設定は、次のプログラムで確かめられます。
import java.nio.charset.Charset;
public class Enc {
public static void main(String[] args) {
System.out.println("java.version = " + System.getProperty("java.version"));
System.out.println("defaultCharset = " + Charset.defaultCharset());
System.out.println("file.encoding = " + System.getProperty("file.encoding"));
System.out.println("native.encoding = " + System.getProperty("native.encoding"));
System.out.println("stdout.encoding = " + System.getProperty("stdout.encoding"));
}
}
$ java Enc.java
java.version = 21.0.10
defaultCharset = UTF-8
file.encoding = UTF-8
native.encoding = UTF-8
stdout.encoding = UTF-8
native.encoding は JDK 17 で追加されたシステムプロパティで、OS やロケールから決まる文字コードを表します。同じプログラムを、ロケールを POSIX にして実行すると次のようになりました。
$ LC_ALL=POSIX java Enc.java
java.version = 21.0.10
defaultCharset = UTF-8
file.encoding = UTF-8
native.encoding = ANSI_X3.4-1968
stdout.encoding = ANSI_X3.4-1968
OS 側の文字コードが ASCII(ANSI_X3.4-1968)でも、デフォルト文字セットは UTF-8 のままです。JDK 17 までの「OS やロケールに合わせる」挙動に戻したいときは、JEP 400 が用意している起動オプション -Dfile.encoding=COMPAT を付けます。
$ LC_ALL=POSIX java -Dfile.encoding=COMPAT Enc.java
java.version = 21.0.10
defaultCharset = US-ASCII
file.encoding = ANSI_X3.4-1968
native.encoding = ANSI_X3.4-1968
stdout.encoding = ANSI_X3.4-1968
この変更で影響を受けるのは、文字コードを書いていない new FileReader(名前) です。 JEP 400 は、主な影響はアジア圏のロケールで Windows を使っている利用者に及ぶと述べ、windows-31j(Shift_JIS 系)を例に挙げています。デフォルトが windows-31j の状態を、検証用に起動オプションで作って Sjis1 を動かしてみます。
$ javac -d out Sjis1.java
$ java -Dfile.encoding=MS932 -cp out Sjis1
デフォルト: windows-31j
備品A,10
備品B,25
先ほど「�」になった同じプログラムが、デフォルトが windows-31j なら正しく読めています。つまり、文字コードを書いていない読み込みコードは、デフォルト文字セットが変わるだけで結果が変わります。JDK 17 以前から 18 以降へ上げるときは、次の順で手当てすると安心です。
| 手順 | 内容 |
|---|---|
| 1 | new FileReader(名前)、new InputStreamReader(入力)、new Scanner(ファイル) など文字コードなしの箇所を探す |
| 2 | 読むファイルの実際の文字コードを確かめ、引数で明示する |
| 3 | すぐに直せない場合は、当面 -Dfile.encoding=COMPAT で以前の挙動に合わせる |
| 4 | 新しく書くコードは、最初から文字コードを引数で渡す |
画面に「?」と表示されるときは出力側を確かめる
読み込みは正しいのに、表示だけが「?」になることがあります。ロケールを POSIX にして Read1 を実行した結果です。
$ LC_ALL=POSIX java Read1.java
1: ??A,10
2: ??B,25
3: ??C,7
ファイルは UTF-8 で正しく読めていますが、System.out の文字コード(stdout.encoding)が ASCII になっているため、日本語を出力できず「?」になっています。JEP 400 でも、System.out と System.err の文字コードはデフォルト文字セットとは別に決まると説明されています。出力側を指定すると表示されます。
$ LC_ALL=POSIX java -Dstdout.encoding=UTF-8 Read1.java
1: 備品A,10
2: 備品B,25
3: 備品C,7
| 症状 | 見直す場所 |
|---|---|
| 「�」が混じる | 読み込み側の文字コード(ファイルと指定が合っていない) |
| MalformedInputException で止まる | 読み込み側の文字コード(同上) |
| 「?」になる | 出力側の文字コード(stdout.encoding、端末の設定) |
| 1列目だけ一致しない | BOM(次の節) |
BOM付きUTF-8は先頭に見えない1文字が入る
表計算ソフトなどが出力した UTF-8 の CSV には、先頭に BOM(バイト順マーク、EF BB BF の3バイト)が付いていることがあります。Java の UTF-8 デコーダーは BOM を取り除かないので、1行目の先頭に U+FEFF という見えない文字が入ります。
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class Bom {
public static void main(String[] args) throws IOException {
Path path = Path.of("bom.csv");
// 先頭に BOM(EF BB BF)を付けた UTF-8 のファイルを用意
byte[] bom = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
byte[] body = "id,name\n1,備品A\n".getBytes(StandardCharsets.UTF_8);
byte[] all = new byte[bom.length + body.length];
System.arraycopy(bom, 0, all, 0, bom.length);
System.arraycopy(body, 0, all, bom.length, body.length);
Files.write(path, all);
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String header = br.readLine();
String first = header.split(",")[0];
System.out.println("1列目: [" + first + "] 長さ=" + first.length());
System.out.printf("先頭文字: U+%04X%n", (int) header.charAt(0));
System.out.println("\"id\" と一致: " + first.equals("id"));
// 対策: 先頭が U+FEFF なら取り除く
if (!header.isEmpty() && header.charAt(0) == 0xFEFF) {
header = header.substring(1);
}
first = header.split(",")[0];
System.out.println("除去後: [" + first + "] 長さ=" + first.length()
+ " 一致=" + first.equals("id"));
}
}
}
$ java Bom.java
1列目: [id] 長さ=3
先頭文字: U+FEFF
"id" と一致: false
除去後: [id] 長さ=2 一致=true
画面上は id に見えるのに長さが 3 で、equals("id") が false になっています。「CSV の1列目だけ見出しが一致しない」「Map から1列目の値だけ取れない」といった現象の多くはこれが原因です。1行目を読んだ直後に、先頭の文字が U+FEFF かを確かめて取り除くのが、手軽で確実な対処です。
標準入力(System.in)を1行ずつ読む
キーボードやパイプからの入力も、BufferedReader で1行ずつ読めます。System.in はバイト列の入力なので、書き方2と同じく InputStreamReader を挟みます。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class Stdin1 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
int n = Integer.parseInt(br.readLine().trim());
int sum = 0;
for (int i = 0; i < n; i++) {
String[] cols = br.readLine().split(" ");
System.out.println(cols[0] + " さんの点数は " + cols[1]);
sum += Integer.parseInt(cols[1]);
}
System.out.println("平均: " + (double) sum / n);
}
}
入力ファイル input.txt:
3
佐藤 80
鈴木 65
高橋 92
$ java Stdin1.java < input.txt
佐藤 さんの点数は 80
鈴木 さんの点数は 65
高橋 さんの点数は 92
平均: 79.0
1行目で件数を読み、その回数だけ readLine() を呼ぶ形は、プログラミングの練習問題でもよく使われます。readLine() が返すのは文字列なので、数値として使うときは Integer.parseInt() で変換します。前後に空白が紛れ込んでも動くよう、trim() を挟んでおくと扱いやすくなります。
入力の終わりと数字以外の入力に備える
対話形式で入力を受けるときは、「入力がないまま終わった(null)」と「数字でない文字が来た」の2つに備えておくと、利用者にわかりやすいメッセージを返せます。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class Prompt {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
System.out.print("数量を入力してください: ");
String line = br.readLine();
if (line == null) {
System.out.println("入力がありませんでした");
return;
}
try {
int qty = Integer.parseInt(line.trim());
System.out.println("2倍すると " + qty * 2);
} catch (NumberFormatException e) {
System.out.println("数字ではありません: " + e.getMessage());
}
}
}
$ echo " 12 " | java Prompt.java
数量を入力してください: 2倍すると 24
$ echo "abc" | java Prompt.java
数量を入力してください: 数字ではありません: For input string: "abc"
$ java Prompt.java < /dev/null
数量を入力してください: 入力がありませんでした
件数が決まっていない入力は、ファイルと同じく null が返るまで while ((line = br.readLine()) != null) で読みます。相手の文字コードが決まっているなら、Stdin1 のように InputStreamReader へ明示しておくと、環境が変わっても同じ結果になります。
System.in を包んだ BufferedReader は、プログラムの終わりまで使い続けるのが普通です。try-with-resources で閉じると System.in 自体も閉じられ、以降は標準入力を読めなくなるため、標準入力の場合は閉じずに使い回す書き方が一般的です。
Scannerとの違いと速度を比べる
テキストを読むクラスには java.util.Scanner もあります。役割の違いは次のとおりです。
| 観点 | BufferedReader | Scanner |
|---|---|---|
| 得意なこと | 行単位で速く読む | 空白区切りの値を型つきで取り出す |
| 主なメソッド | readLine()、lines() |
nextLine()、nextInt()、next() |
| 数値の取り出し | Integer.parseInt() で自分で変換 |
nextInt() などで直接取得 |
| 例外 | IOException(検査例外) |
入力の形が合わないと InputMismatchException |
| 向いている入力 | 大きなファイル、ログ、CSV | 小さな対話入力、形式が決まった短い入力 |
100万行(23 MB)のファイルを作り、行数を数えるだけの処理で時間を比べました。あわせて、バッファなしで FileReader.read() を1文字ずつ呼んだ場合も計測しています。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Scanner;
public class Speed {
public static void main(String[] args) throws IOException {
Path path = Path.of("big.txt");
try (BufferedWriter bw = Files.newBufferedWriter(path)) {
for (int i = 0; i < 1_000_000; i++) {
bw.write("line-" + i + ",備品,12345");
bw.newLine();
}
}
System.out.println("サイズ: " + Files.size(path) / 1024 / 1024 + " MB");
for (int round = 1; round <= 5; round++) {
long t0 = System.nanoTime();
long n1 = 0;
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
while (br.readLine() != null) n1++;
}
long t1 = System.nanoTime();
long n2 = 0;
try (Scanner sc = new Scanner(path, StandardCharsets.UTF_8)) {
while (sc.hasNextLine()) { sc.nextLine(); n2++; }
}
long t2 = System.nanoTime();
long n3 = 0;
try (FileReader fr = new FileReader(path.toFile(), StandardCharsets.UTF_8)) {
int c;
while ((c = fr.read()) != -1) { if (c == '\n') n3++; }
}
long t3 = System.nanoTime();
long n4;
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
n4 = br.lines().count();
}
long t4 = System.nanoTime();
System.out.printf("%d回目 readLine: %d ms / lines(): %d ms / Scanner: %d ms / FileReader 1文字ずつ: %d ms%n",
round, (t1 - t0) / 1_000_000, (t4 - t3) / 1_000_000,
(t2 - t1) / 1_000_000, (t3 - t2) / 1_000_000);
}
Files.delete(path);
}
}
5回の計測結果を表にまとめます(4つとも 1,000,000 行を数えたことを確認しています)。
| 回 | BufferedReader.readLine() | BufferedReader.lines() | Scanner.nextLine() | FileReader.read() を1文字ずつ |
|---|---|---|---|---|
| 1回目 | 244 ms | 329 ms | 1157 ms | 1872 ms |
| 2回目 | 244 ms | 290 ms | 1155 ms | 2192 ms |
| 3回目 | 419 ms | 240 ms | 940 ms | 1741 ms |
| 4回目 | 201 ms | 640 ms | 686 ms | 2516 ms |
| 5回目 | 298 ms | 276 ms | 973 ms | 2358 ms |
| 中央値 | 244 ms | 290 ms | 973 ms | 2192 ms |
中央値で比べると、BufferedReader は Scanner のおよそ4分の1、バッファなしの1文字読みのおよそ9分の1の時間で読み終えました。回ごとのばらつきはありますが、5回とも BufferedReader が Scanner より速いという順序は変わりませんでした。readLine() と lines() はほぼ同じ速さです。数値は計測した環境での値で、マシンやファイルの内容によって変わります。
Scanner は入力を正規表現で区切りながら読むぶん仕事が多く、そのかわりに nextInt() のような便利なメソッドを備えています。数十行程度の入力ならどちらでも体感の差はないので、行単位でたくさん読むなら BufferedReader、少量の値を手軽に取り出すなら Scanner と使い分けるのがよいでしょう。
つまずきやすいエラーと直し方
ファイルが見つからないとき
作り方によって、投げられる例外のクラスが違います。
存在しない nothing.txt を2通りの作り方で開き、受け取った例外を出力した結果です。
java.nio.file.NoSuchFileException: nothing.txt
java.io.FileNotFoundException: nothing.txt (No such file or directory)
| 作り方 | ファイルがないときの例外 |
|---|---|
Files.newBufferedReader |
java.nio.file.NoSuchFileException |
new FileReader |
java.io.FileNotFoundException |
どちらも IOException の子クラスなので、catch (IOException e) でまとめて受けられます。相対パスで指定したファイルが見つからないときは、プログラムを起動したフォルダ(カレントディレクトリ)が想定と違っていることが多いので、Path.of("items.txt").toAbsolutePath() を出力して、実際に探している場所を確かめると解決が早まります。
閉じたあとに読むと「Stream closed」
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Closed {
public static void main(String[] args) throws IOException {
BufferedReader br = Files.newBufferedReader(Path.of("items.txt"));
try (br) {
System.out.println(br.readLine());
}
System.out.println(br.readLine()); // close 後に読む
}
}
$ java Closed.java
備品A,10
Exception in thread "main" java.io.IOException: Stream closed
at java.base/java.io.BufferedReader.ensureOpen(BufferedReader.java:121)
try-with-resources のブロックを抜けた時点で BufferedReader は閉じられています。読み込みはブロックの中で済ませ、外で使いたいデータはリストや変数に入れて持ち出します。 lines() で得た Stream も同じで、ブロックの外へ Stream のまま返すと、使う時点ではすでに閉じられています。toList() などでリストにしてから返しましょう。
エラーの逆引き表
| 出たエラー・症状 | よくある原因 | 直し方 |
|---|---|---|
MalformedInputException: Input length = 1 |
ファイルの文字コードと指定が合っていない | 正しい文字コードを第2引数に渡す |
UncheckedIOException(lines() 使用時) |
Stream の処理中に入出力エラーが起きた | getCause() で元の例外を確かめる |
| 「�」が表示される | FileReader などが合わない文字コードで読んだ | 文字コードを明示する |
| 「?」が表示される | 出力側の文字コードが日本語を扱えない | -Dstdout.encoding=UTF-8 や端末設定を見直す |
NoSuchFileException / FileNotFoundException |
パスが違う、起動フォルダが想定と違う | toAbsolutePath() で探している場所を確かめる |
IOException: Stream closed |
close 後に読んでいる | 読み込みを try ブロックの中にまとめる |
NullPointerException(line.split などで) |
終端の null をそのまま使った |
line != null を確かめてから使う |
NumberFormatException |
数字以外や空白つきの文字列を変換した | trim() を挟み、例外を受けて案内を出す |
| 1列目の見出しだけ一致しない | BOM が先頭に付いている | 1行目の先頭の U+FEFF を取り除く |
ファイルを読むほかの方法との使い分け
Files クラスには、BufferedReader を自分で扱わなくて済むメソッドもあります。
items.txt に対して3つのメソッドを呼んだ結果です。
readAllLines: [備品A,10, 備品B,25, 備品C,7]
Files.lines : 3 行
readString : 20 文字
| 方法 | 返すもの | メモリの使い方 | 向いている場面 |
|---|---|---|---|
BufferedReader.readLine() |
1行ずつの文字列 | 1行ぶんずつ | 大きなファイル、行ごとに条件分岐したい処理 |
BufferedReader.lines() / Files.lines() |
行の Stream | 必要なぶんずつ | 絞り込み・集計 |
Files.readAllLines() |
全行のリスト | 全体を載せる | 設定ファイルなど小さなファイル |
Files.readString()(JDK 11 以降) |
ファイル全体の文字列 | 全体を載せる | テンプレートや JSON を丸ごと読む |
Scanner |
値を1つずつ | 少しずつ | 空白区切りの数値を手軽に読む |
Files.lines() もファイルを開いたままにするので、上の例のように try-with-resources で閉じます。ファイルの大きさが読めないときは、1行ずつ処理する BufferedReader を選んでおくと、データが増えても同じコードで対応できます。
よくある質問
Q. BufferedReader を close すると、内側の FileReader も閉じられますか? A. はい。外側の BufferedReader を閉じれば、包んでいる Reader も一緒に閉じられます。try-with-resources の丸かっこには、いちばん外側の BufferedReader を1つ書けば足ります。
Q. readLine() で読んだ行に改行を付けて書き出したいときは?
A. readLine() の戻り値には改行が含まれないので、書き出す側で BufferedWriter.newLine() や System.lineSeparator() を使って足します。元の改行コードをそのまま保ちたい場合は、Files.readString() などファイル全体を読む方法を選びます。
Q. Shift_JIS と MS932 はどちらを指定すればよいですか?
A. Windows で作られたファイルなら、機種依存文字(丸付き数字など)まで扱える Charset.forName("MS932")(正式名は windows-31j)が合うことが多いです。本記事の検証で使った文字は、どちらでも同じ結果になります。
Q. 文字コードがわからないファイルはどう読めばよいですか?
A. まず Files.newBufferedReader(path) で UTF-8 として読み、MalformedInputException が出たら Shift_JIS 系を試す、という順が手軽です。FileReader と違って合わないときに例外で知らせてくれるので、判定に使えます。
まとめ
- BufferedReader は文字をまとめて読み、
readLine()で1行ずつ返すクラス。終端はnull、空行は空文字で返り、改行文字は含まれない - 新しく書くなら
Files.newBufferedReader(path, 文字コード)を try-with-resources で。文字コードを省いても常に UTF-8 で読む - 合わない文字コードで読むと、FileReader は「�」に置き換え、Files.newBufferedReader は MalformedInputException で止まる
- JDK 18 以降はデフォルト文字セットが UTF-8。文字コードを書いていない
new FileReader(名前)は、JDK を上げると結果が変わることがある - 標準入力は
new InputStreamReader(System.in, 文字コード)を包んで読む。nullと数字以外の入力に備える - BOM 付き UTF-8 は1行目の先頭に U+FEFF が入るので、読んだ直後に取り除く
- 100万行の読み込みでは、BufferedReader は Scanner のおよそ4分の1の時間。行単位で大量に読むなら BufferedReader が向いている
参考・出典
- Java SE 21 API ドキュメント:
java.io.BufferedReader(Oracle) - Java SE 21 API ドキュメント:
java.nio.file.Files(Oracle) - JEP 400: UTF-8 by Default(OpenJDK)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java BufferedReaderの使い方|ファイルを1行ずつ読む」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。