Reader/Writer/InputStream/OutputStream

少し話題に出たのでファイル読み書きなどでよく使う感じのアレをアレしたいと思います。

まず、

  • InputStream / OutputStream はバイナリデータのストリームです。 byte[] で読み書きします。
  • Reader / Writer はテキストデータのストリームです。 char[] で読み書きします。
  • ストリームというのは byte[]char[] を使って少しずつデータを読み込んだり書き出したりするためのものです。

というのが基本になります。

ファイルの読み書き

テキストファイルを読み込む

Path path = Paths.get("path/to/file");
try (BufferedReader in = Files.newBufferedReader(path)) {
    //読み込み処理
}

ファイルはUTF-8で読み込まれます。

UTF-8以外のファイルを読み込む場合は第二引数に Charset を渡します。

try (BufferedReader in = Files.newBufferedReader(path, Charset.forName("Windows-31J"))) {
    String line;
    while(null != (line = in.readLine())) {
        //読み込み処理
    }
}

バイナリファイルを読み込む

try (InputStream in = new BufferedInputStream(Files.newInputStream(path))) {
    byte[] b = new byte[1000];
    int i;
    while (-1 != (i = in.read(b))) {
        //読み込み処理
    }
}

Files.newInputStream はバッファリングされないので巨大なファイルやたくさんファイルを扱う処理だと遅いと思います。 基本的には BufferedInputStream でラップする方が良いかとー。

テキストファイルを書き出す

try (BufferedWriter out = Files.newBufferedWriter(path)) {
    //書き出し処理
}

バイナリファイルを書き出す

try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(path))) {
    //書き出し処理
}

Files.newInputStream と同じく Files.newOutputStream もバッファリングされません。

オンメモリで扱う

Writer を渡したらそこにもろもろ書き出してくれるライブラリがあるんだけど わざわざファイルに書き出すんじゃなくてオンメモリで処理して String で結果を取りたいんや! というような場合には StringWriter を使います。

StringWriter out = new StringWriter();
library.writeTo(out);
String result = out.toString();

Reader / InputStream / OutputStream にもそれぞれオンメモリで使用するためのクラスがあります。

  • StringReaderString を読み込める Reader
  • StringWriterString へ書き出せる Writer
  • ByteArrayInputStreambyte[] を読み込める InputStream
  • ByteArrayOutputStreambyte[] へ書き出せる OutputStream

InputStreamをReaderへ/OutputStreamをWriterへ変換する

それぞれ InputStreamReaderOutputStreamWriter を使って変換できます。

InputStream in = ...
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);

OutputStream out = ...
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);

第二引数に Charset を渡していますが、何も渡さない場合はデフォルトエンコーディングが使用されるので注意が必要です。 デフォルトエンコーディングとはシステムプロパティ file.encoding で取得できるものです。 変更したい場合は次のようにJava起動時にオプションを設定します。

java -Dfile.encoding=UTF-8 com.example.MainClass

ZIPファイルを読み込む/書き出す

ZIPファイルの読み書きには ZipInputStreamZipOutputStream が使えます。

InputStream in = ...
try(ZipInputStream zin = new ZipInputStream(in, StandardCharsets.UTF_8)) {
    ZipEntry zipEntry;
    while (null != (zipEntry = zin.getNextEntry())) {
        byte[] b = new byte[1000];
        int i;
        while (-1 != (i = zin.read(b))) {
            //読み込み処理
        }
    }
}
OutputStream out = ...
try(ZipOutputStream zout = new ZipOutputStream(out, StandardCharsets.UTF_8)) {
    ZipEntry zipEntry = new ZipEntry("hoge.txt");
    zout.putNextEntry(zipEntry);
    byte[] b = ...
    zout.write(b);
    zout.closeEntry();
}

ファイルのコピー、移動をする

Files を使います。

Path src = ...
Path dest = ...

Files.copy(src, dest);

Files.move(src, dest);

Channel

Reader / Writer / InputStream / OutputStream の他に Channel というものもありますが Channel が必要になるライブラリには ほぼ出会った事がないので覚えなくても生きて行けると思います。

おまけ

テキストファイルの読み込みには Files.newBufferedReader を使うと書きましたが Java 6までは FileReader を使って次のようにファイル読み込みをしていました。

File file = new File("path/to/file");
Reader in = new FileReader(file);
try {
    //読み込み処理
} finally {
    in.close();
}

Charset を渡さずに FileReader をインスタンス化していますが、 この場合はデフォルトエンコーディングが使われていました。

しかも FileReader には Charset を受け取るコンストラクタは用意されていません。 ではデフォルトエンコーディング以外でファイルを読み込みたい場合はどうするのか?

その場合は、

  1. FileInputStream でファイルを開いて
  2. InputStreamReaderCharset を指定しつつラップする

という方法をとっていました。

File file = new File("path/to/file");
Reader in = new InputStreamReader(new FileInputStream(file), Charset.forName("iso-2022-jp"));
try {
    //読み込み処理
} finally {
    in.close();
}

そういう訳で java.ioCharset を受け取らない場合はデフォルトエンコーディング、 java.nio.fileCharset を受け取らない場合はUTF-8が使われる、という感じです。

デフォルトエンコーディングは環境によって変わるので java.nio.file を使っておくのが安全だと思います。

JUnit 4.12から入ったTestRuleを軽く見てみる

DisableOnDebug

DisableOnDebug 他の TestRule をラップして、 デバッグ実行されているときのみラップした TestRule を適用します。

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;

public class HogeTest {

    @Rule
    public TestRule timeout = new DisableOnDebug(Timeout.seconds(1)); //1秒以上かかったら失敗とみなす

    @Test
    public void testHoge() throws Exception {
        //test code
    }
}

こんな感じで Timeout と組み合わせる事が多い気がします。

コマンドライン引数に次のいずれかが含まれていればデバッグ実行されていると判断するようです。

  • -Xdebug
  • -agentlib:jdwp

デバッグ実行かどうかの判断は DisableOnDebug.isDebugging メソッドをオーバーライドすればカスタマイズできます。

Stopwatch

Stopwatch はテスト実行にかかった時間を System.nanoTime メソッドで計測します。

import java.util.logging.Logger;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Stopwatch;
import org.junit.runner.Description;

public class FugaTest {

    private static Logger logger = Logger.getLogger(FugaTest.class.getName());

    @Rule
    public Stopwatch stopwatch = new Stopwatch() {
        @Override
        protected void succeeded(long nanos, Description description) {
            logger.info(() -> String.format("テストの実行に%,dナノ秒かかった", nanos));
        }
    };

    @Test
    public void test() throws Exception {
        //test code
    }
}

ロギング目的に使うのが多そうです。