Javaの入出力関連 まとめ
はじめに
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
クラスを継承したもの
を使うという認識で大丈夫そう。
バイナリを扱うときもInputStream
、OutputStream
へ変換するぐらいで、これを継承したクラスを利用してゴニョゴニョするって状況も少ない気がする。
混乱の原因(愚痴)
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.Files
でInputStream
/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-8
やUTF-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
の引数を指定すればよい
※ ラムダ式に慣れている人であれば、こちらの方が使いやすかと思う。
以下のような処理もシンプルに書きやすい
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
List<Path> fileList = Files.list(Paths.get(dirPath))
.filter(file -> {
return file.getFileName().toString().lastIndexOf(".txt") != -1;
}).collect(Collectors.toList());
更新日でfilter
// 取得範囲の日付作成
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();
}
ファイル削除
try {
Files.delete(Paths.get(filepath));
} catch (IOException e) {
e.printStackTrace();
}
try {
Files.deleteIfExists(Paths.get(filepath));
} catch (IOException e) {
e.printStackTrace();
}
Discussion