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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?