🦔

Javaの入出力関連 まとめ

2022/03/03に公開

はじめに

Javaの入出力関連のクラス/インターフェースって、似たような名前で複数あって混乱する。
検索しても、古い書き方とかいろいろな書き方が同時に見つかって、どれ使えばいいか迷う。

最適解ではないかもしれないけど、これ使っとけばいいんじゃねっていうのを今更だが備考録として残しておく。

いろいろ書いてあるが、そんなのどうでもいいという感じの人は、最後らへんのサンプル集だけみればいい感じで…

参考サイト

今更ながらJavaのI/Oストリームを整理する
Javaのファイル入出力関係のクラス/インタフェースについて整理する

クラス分類

入出力系

インターフェース 継承しているクラス例 説明
java.io.InputStream FileInputStream
ByteArrayInputStream
バイト列の読込用クラス
java.io.OutputStream FileOutputStream
ByteArrayOutputStream
バイト列の書込用クラス
java.io.Reader InputStreamReader
BufferedReader
文字列の読込用クラス
java.io.Writer OutputStreamWriter
BufferedWriter
文字列の書込用クラス

↑の4つの系統のクラスがある。

入出力操作は基本的に文字列の読み書きがメインなので…

  • 読込はReaderクラスを継承したもの
  • 書込みはWriterクラスを継承したもの

を使うという認識で大丈夫そう。

バイナリを扱うときもInputStreamOutputStreamへ変換するぐらいで、これを継承したクラスを利用してゴニョゴニョするって状況も少ない気がする。

混乱の原因(愚痴)

InputStreamクラスと、Readerを継承したInputStreamReaderクラスがあったり…名前から見れば、継承関係がありそうではないか…

さらに、FilterInputStreamを継承したBufferedInputStreamクラスなんてものもあり、BufferedReaderと混同してしまうは…紛らわしい。

基本的に~Reader~Writerってついているのを利用するって覚えとけばいい感じかな?
Files.new~のメソッドで作成できるものを基準に考えれば、混乱は少ないような気がする。

ユーティリティ系

クラス 説明
java.io.File ファイルパスを表すクラス(古いため、ほぼ使わない)
java.nio.file.Path ファイルパスを表すクラス
java.nio.charset.Charset 文字コードを表すクラス
java.nio.file.Files ファイル操作のユーティリティクラス

結局どうすればよいの?

基本的には、以下のような感じの基本パターンを覚えとけば、迷うことはなさそう。
いちいち変数に格納せず1文で掛ける箇所も、説明のため1行ずつ記載しています。

java.nio.file.Filesを使ってBufferedReader/BufferedWriterのインスタンスを作成すれば、シンプルにスッキリかけていい感じ。
文字列書込みの場合は、PrintWriterへラップして使うほうが使い勝手がよさそう

バイナリデータを扱いたい場合も、java.nio.file.FilesInputStream/OutputStreamのインスタンスを作成すれば同じようにできそう。

  • 読み込み
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class IoSample {
    public static void main(String[] args) {
        String outFilePath = "/tmp/sample.txt";

        // ファイルパス文字列から、Pathオブジェクト作成
        Path path = Paths.get(outFilePath);
        // Filesを使ってBufferedReaderの取得
        try (BufferedReader bw = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
            String str;
            // 1行ずつ読み込んで処理を行う
            while ((str = bw.readLine()) != null) {
                System.out.println(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

  • 書込み
package sample;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class IoSample {
    public static void main(String[] args) {
        String outFilePath = "/tmp/output.txt";

        // ファイルパス文字列から、Pathオブジェクト作成
        Path path = Paths.get(outFilePath);
        // Filesを使ってBufferedWriterの取得
        try (BufferedWriter bw = Files.newBufferedWriter(path, StandardCharsets.UTF_8);
                PrintWriter pw = new PrintWriter(bw, true)) {
            pw.println("ファイル書き込み");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Files.newBufferedWriterに渡す引数によって、新規作成 or 追記 などモードを変更できる

try-with-resources (ファイルは開けたら閉める)

ファイルをオープンして、読み書きして、終わったらクローズするのは一連の流れ。

いにしえの時代は、openしたら、closeを明示的に呼び出して…例外発生時にもしっかりcloseするようにして。とやってた。
これを実装すると、close処理でぐちゃぐちゃして嫌いだった。closeの入れ忘れもたまにあって、なんだかんだで面倒だった。

昔の書き方は、↓。本題ではないので、折りたたんでおく

いにしえの時代のclose処理
package sample;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

public class IoSample {
    public static void main(String[] args) {
        String outFilePath = "xxxxx";

        // ここで定義しないといけない…
        OutputStream os = null;
        OutputStreamWriter osw = null;
        BufferedWriter bw = null;

        try {
            os = Files.newOutputStream(Paths.get(outFilePath));
            osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
            bw = new BufferedWriter(osw);

            // いろいろ出力処理
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // closeの前に、nullチェックが必要 
	    // + closeメソッドのIOExceptionをcatchする必要がある
            if (bw != null) {
                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (osw != null) {
                try {
                    osw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

}

try-with-resourcesを使った書き方を行えば、closeメソッドを明示的に呼び出す必要がないため、閉じ忘れも防げる。

package sample;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

public class IoSample {
    public static void main(String[] args) {
        String outFilePath = "xxxxx";

        try (OutputStream os = Files.newOutputStream(Paths.get(outFilePath));
                OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);) {

            // いろいろ出力処理
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

注意点

tryの中で例外が発生すると、closeしてくれない場合がある。
上記のサンプルで

try (OutputStream os = Files.newOutputStream(Paths.get(outFilePath));
     OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
     BufferedWriter bw = new BufferedWriter(osw);)

の部分は、下記のように1文で書ける。

try (BufferedWriter bw = new BufferedWriter(
        new OutputStreamWriter(
	     Files.newOutputStream(Paths.get(outFilePath)), StandardCharsets.UTF_8));)

この場合、try()の中で例外が発生してしまうと、しっかりと閉じてくれないので1文ずつ書いたほうが良い。

↑は事象のためのサンプルでなので、

try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(outFilePath), StandardCharsets.UTF_8))

本来なら、こう書いたほうがシンプル

参考:try-with-resourcesでリソース解放されないパターン

サンプル集

1行ずつ文字列を読み込む

try (BufferedReader bw = Files.newBufferedReader(Paths.get("/tmp/sample.txt"))) {
    String str = "";
    while ((str = bw.readLine()) != null) {
        // 読込処理
    }

} catch (IOException e) {
    e.printStackTrace();
}

文字コードを指定して読み込む

  • 引数に何も指定しない場合は、UTF-8で読み込みます
Files.newBufferedReader(Paths.get("/tmp/sample.txt"))
  • 明示的に指定する場合は、StandardCharsetsの定義を指定します。(UTF-8UTF-16)
Files.newBufferedReader(Paths.get("/tmp/sample.txt"), StandardCharsets.UTF_8))
  • Shift-JIS(StandardCharsetsで定義されていない文字コード等)の場合は、Charsetを使う
// MS932は、WindowsのShif-JISを拡張した文字列らしい
Files.newBufferedReader(Paths.get("/tmp/sample.txt"), Charset.forName("MS932"))

1行ずつ文字列を読み込む(Stream版)

BufferedReaderで読み込む方法よりは、1行ずつreadするwhileが消えたりと、若干シンプルに書ける。

try (Stream<String> lines = Files.lines(Paths.get("/tmp/sample.txt"))) {
    lines.forEachOrdered(line -> {
        // 読込処理
    });
} catch (IOException e) {
    e.printStackTrace();
}

※ 文字コードの指定については、上記と同じようにFiles.linesの引数を指定すればよい

※ ラムダ式に慣れている人であれば、こちらの方が使いやすかと思う。
以下のような処理もシンプルに書きやすい

例)特定文字列が含まれている行はSkipする
lines.filter(line -> {
    return line.indexOf('hoge') == -1;
}).forEachOrdered(line -> {
    // 読込処理
});

文字列をファイルへ出力

try (BufferedWriter bw = Files.newBufferedWriter(Paths.get("/tmp/sample.txt"));
        PrintWriter pw = new PrintWriter(bw, true)) {

    pw.println("ファイル書き込み");

} catch (IOException e) {
    e.printStackTrace();
}

文字コードを指定して書き込む

文字コードの指定については、上記と同じようにnewBufferedWriterの引数を指定すればよい

Files.newBufferedWriter(Paths.get("/tmp/sample.txt"), Charset.forName("MS932"));

モードの指定

StandardOpenOptionで指定できる。いろいろとモードが存在するが、ファイルの新規作成 or 追記ぐらい覚えとけばよさそう

新規作成(既存ファイルがある場合は、新規ファイルで上書き
Files.newBufferedWriter(Paths.get("/tmp/sample.txt"));
新規作成(既存ファイルがある場合は、例外発生)
Files.newBufferedWriter(Paths.get("/tmp/sample.txt")
                      , StandardOpenOption.CREATE_NEW);
追記(ファイルがない場合は、新規作成)
Files.newBufferedWriter(Paths.get("/tmp/sample.txt")
                      , StandardOpenOption.CREATE, StandardOpenOption.APPEND);

ファイル/ディレクトリ存在確認

下の例以外にも、実行ファイルか、書き込み可能か、シンボリックリンクかなどいろいろあるのでFiles.is~のメソッドで利用できそうなのを調べてみるといいかも

ファイルパスが存在するか、ファイルか、ディレクトリかなどのチェック
if (Files.exists(Paths.get(path))) {
    System.out.println("対象のパスが存在します");
} else {
    System.out.println("対象のパスが存在しません");
}

if (Files.isRegularFile(Paths.get(path))) {
    System.out.println("対象のパスはファイルです");
} else {
    System.out.println("対象のパスはファイルではありません");
}

if (Files.isDirectory(Paths.get(path))) {
    System.out.println("対象のパスはディレクトリです");
} else {
    System.out.println("対象のパスはディレクトリではありません");
}

ファイル/ディレクトリ一覧取得

単純に一覧取得(ファイル、ディレクトリ混在)

対象のディレクトリ直下のファイル/ディレクトリ一覧を取得
try {
    List<Path> fileList = Files.list(Paths.get(dirPath)).collect(Collectors.toList());

    // 取得ファイル確認
    fileList.forEach(file -> {
        System.out.println(file.getFileName());
    });

} catch (IOException e) {
    e.printStackTrace();
}

ファイルタイプでfilter

フォルダを除外して取得
List<Path> fileList = Files.list(Paths.get(dirPath))
        .filter(file -> {
            return !Files.isDirectory(file);
        }).collect(Collectors.toList());

ファイル名でfilter

拡張子が.txtのファイルだけ取得
List<Path> fileList = Files.list(Paths.get(dirPath))
        .filter(file -> {
            return file.getFileName().toString().lastIndexOf(".txt") != -1;
        }).collect(Collectors.toList());

更新日でfilter

最終更新日が1日以内のファイルを取得
// 取得範囲の日付作成
LocalDateTime targetDate = LocalDateTime.now().minusDays(1);
List<Path> fileList = Files.list(Paths.get(dirPath))
        .filter(file -> {
            try {
		// 各ファイルの最終更新日を取得
                LocalDateTime lastModified = LocalDateTime.ofInstant(
                        Files.getLastModifiedTime(file).toInstant(),
                        ZoneId.systemDefault());

                // ターゲット日付より未来の日付のファイルを抽出
                return lastModified.isAfter(targetDate);

            } catch (IOException e) {
                e.printStackTrace();
            }
            return false;
        }).collect(Collectors.toList());

ファイルコピー

ファイルコピー
try {
    Files.copy(Paths.get(inPath), Paths.get(outPath));
} catch (IOException e) {
    e.printStackTrace();
}

ファイル削除

ファイル削除(ファイルがない場合NoSuchFileExceptionが発生する)
try {
    Files.delete(Paths.get(filepath));    
} catch (IOException e) {
    e.printStackTrace();
}
ファイル削除(ファイルがなくても例外にならない)
try {
    Files.deleteIfExists(Paths.get(filepath));
    
} catch (IOException e) {
    e.printStackTrace();
}

Discussion