Javaにおける文字コードまわりの話(4)

Javaにおける文字コードまわりの話(3) - あしのあしあと」の続き。今回は、ストリームを用いて、文字をエンコードおよびデコードする。
Javaの開発において、文字のエンコード、デコードが必要な箇所は、だいたいストリームを用いているのではないだろうか(String#getBytesかもしれないが)。この場合、バイトストリームであるInputStream、OutputStream、および、文字ストリームであるReader、Writerを用いる。

今回は、検証のためなので、次のストリームを用いることにした。

  • 典型的な文字ストリーム
    • InputStreamReader
    • OutputStreamWriter
  • メモリ上に保持するだけの、バイトストリーム
    • ByteArrayInputStream
    • ByteArrayOutputStream

バイト配列のデコード、文字配列のエンコードのプログラムは、次のようになる。
検証用ということもあって、バイト配列、文字配列は全てメモリ上に展開している。配列が大きいと、相応のメモリを使うので注意。

    /**
     * 指定されたバイト配列をデコードし、文字配列を取得する。
     * @param bytes デコード対象のバイト配列
     * @param encordingsetName バイト列をデコードするエンコーディングセット名
     * @return 指定されたバイト配列をデコードした文字配列
     */
    public static char[] getChars(byte[] bytes, String encordingsetName) {
        ArrayList<Character> resultChars = new ArrayList<Character>();

        Reader reader = null;
        try {
            reader = new InputStreamReader(
                    new ByteArrayInputStream(bytes), encordingsetName);

            char[] cbuf = new char[2048];
            for (int len = 0; len != -1; len = reader.read(cbuf, 0, cbuf.length)) {
                for (int i = 0; i < len; i++) {
                    resultChars.add(Character.valueOf(cbuf[i]));
                }
            }
        } catch (IOException ioe) {
            handleIOException(ioe);
        } finally {
            closeStreams(reader);
        }
        return toCharArray(resultChars);
    }
    private static char[] toCharArray(List<Character> charList) {
        char[] charArray = new char[charList.size()];
        int i = 0;
        for (Character ch : charList) {
            charArray[i++] = ch.charValue();
        }
        return charArray;
    }

    /**
     * 指定された文字配列をエンコードし、バイト配列を取得する。
     * @param chars エンコード対象の文字配列
     * @param encordingsetName 文字をエンコードするエンコーディングセット名
     * @return 指定された文字配列をエンコードしたバイト配列
     */
    public static byte[] getBytes(char[] chars, String encordingsetName) {
        ByteArrayOutputStream os = new ByteArrayOutputStream();

        Writer writer = null;
        try {
            writer = new OutputStreamWriter(os, encordingsetName);
            writer.write(chars);
            writer.flush();
        } catch (IOException ioe) {
            handleIOException(ioe);
        } finally {
            closeStreams(writer);
        }
        return os.toByteArray();
    }

ここで、本質的ではないコードを少なくするために、次の便利メソッドを用いた。

    /**
     * @param streams クローズするストリーム
     */
    private static void closeStreams(Closeable... streams) {
        for (Closeable stream : streams) {
            try {
                if (stream != null) stream.close();
            } catch (IOException e) {
                handleIOException(e);
            }
        }
    }
    /**
     * @param ioe 処理対象のI/O例外
     */
    private static void handleIOException(IOException ioe) {
        // FIXME
        ioe.printStackTrace();
    }

ここでは、(あえて)エンコーディングセットを指定するようにしている。実際には、エンコーディングセット名を指定しないで用いる場合が多いかもしれない。その場合には、デフォルトのエンコーディングセットが用いられる。
デフォルトのエンコーディングセットは、システムプロパティのfile.encordingに設定されている。デフォルトのエンコーディングセットを指定するには、次の2つの方法がある(次は、Windows-31Jを指定している例)。


(1) Javaの起動オプションに指定する

    -Dfile.encording=windows-31j

(2) プログラム中で指定する

    System.setProperty("file.encording", "windows-31j");


このようにしておくことで、文字ストリームは(コンストラクタの第2引数を指定しなかった場合に)、デフォルトとして指定されているエンコーディングセットを用いる。デフォルトとして指定されているエンコーディングセットを表示させる方法は、「Javaにおける文字コードまわりの話(2) - あしのあしあと」を参照のこと。
今回はこれだけ*1にしたい。最後に、指定したエンコーディングセットにより、ファイルに書きこんだり、ファイルから読み込んだりする場合のコードをメモしておく。FileReaderを用いると、デフォルトのエンコーディングセットが用いられる(エンコーディングセットを指定できない)ため、ここではあえて使わない。

    /**
     * @param fileIn 読み込むファイル
     * @param fileOut 書き出すファイル
     * @param encordingsetNameIn 読み込み時のエンコーディングセット名
     * @param encordingsetNameOut 書き出し時のエンコーディングセット名
     */
    public static void readAndWriteFileInGivenEncording(
            File fileIn, File fileOut, String encordingsetNameIn, String encordingsetNameOut) {
        Reader reader = null;
        Writer writer = null;
        try {
            reader = new InputStreamReader(new FileInputStream(fileIn), encordingsetNameIn);
            writer = new OutputStreamWriter(new FileOutputStream(fileOut), encordingsetNameOut);

            char[] cbuf = new char[2048];
            for (int len = 0; len != -1; len = reader.read(cbuf)) {
                // ここで、読み込んだ文字に対して処理を行う。

                writer.write(cbuf);
            }
        } catch (FileNotFoundException fnfe) {
            handleIOException(fnfe);
        } catch (IOException ioe) {
            handleIOException(ioe);
        } finally {
            closeStreams(reader, writer);
        }
    }

実際の検証においては、読み込みだけ、書き出しだけの場合を使うことが多いと思う。

2010/07/24追記

Javaにおける文字コードまわりの話(5) - あしのあしあと」に、(整理し切れてはいないのだが)簡単なまとめを書いた。

*1:時間がなくて、なかなかできない。DQMJ2もその一因w 強いモンスターよりも、好きなモンスターを育ててしまうあたりがダメなんだよなぁ。ビジネス的にはNG、、果たして割り切れるかどうか。