argius note

プログラミング関連

System.in, System.out, System.errを再初期化する

またかなり間が空いてしまいましてすみません。

今さら感あふれるネタですが、知らなかったのでメモ。


結論を先に書きます。

下記ページで知りました。


System.in, System.out, System.errの再初期化は、下記のようにします。
FileDescriptorを使うのがポイントです。

// import java.io.*;

System.setIn(new BufferedInputStream(new FileInputStream(FileDescriptor.in)));
System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));
System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.err)));


ただし、これだけでは完全ではありません。理由は後述します。


元々入っているのを退避しておいて、後で戻す方法でも、もちろんOKです。
これについても後述します。



どういう時に使う?

使う場面は限られていて、単体テスト時や、ツール内で標準出力をリダイレクトする場合などに使用します。IDEのランチャーや、Webアプリケーションコンテナーなどで(たぶん)使われています。通常のアプリケーションではまず使用しません。


単体テストの場合については、JUnitで戻り値がvoidメソッドをテストしたい、という要望はわりとあって、System.out.println(String)の結果をassertさせる方法として、

import static org.junit.Assert.*;
import java.io.*;
import org.junit.*;

public final class HelloTest {

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
        // TODO ここでSystem.outを元に戻したい
    }

    @Test
    public void testPrintMessage() {
        // デフォルトを退避
        PrintStream defaultSystemOut = System.out;

        // 差し替える
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(bos);
        System.setOut(ps);

        // assert
        Hello.printMessage();
        assertEquals("Hello.", bos.toString());

        // 元に戻す
        System.setOut(defaultSystemOut);
    }

}

のようにしてSystem.outを書き換えたりすることがあります。


問題は、これを元に戻す方法です。

JUnitの場合なら戻さなくても問題になることはあまり無いとは思いますが、レアなケースで元に戻す必要がある時にどうすれば良いのかを考えました。

退避する方法は確実ではありますが、毎回退避するのも、どこに退避しておくかを考えるのも、面倒です。

今回の方法は、要するに「グローバル変数を使って再初期化ができるよ!」ということです。


Systemクラス初期化時はどうなっているのか?

前述の方法で上手く行っているようですが、この方法で本当にSystemクラス初期化時と同じになっているのでしょうか。

OpenJDK 8u40によれば、

java.lang.System(L.1188) - GrepCode

  • (引用)
FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
setIn0(new BufferedInputStream(fdIn));
setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));


となっています。
setIn0, setOut0, setErr0は、セキュリティーマネージャーのチェックの有無を除けばsetIn, setOut, setErrと同じですので問題ないでしょう。

newPrintStreamシステムプロパティーのところを追加する必要がありそうです。


実装する

以上を踏まえて、それぞれの再初期化を実装し、ユーティリティークラスにまとめました。

  • System.in, System.out, System.errを再初期化するメソッドの実装例

import static java.lang.System.*;
import java.io.*;

public final class UnofficialSystemUtils {

    private UnofficialSystemUtils() {
    }

    public static void resetIn() {
        setIn(new BufferedInputStream(new FileInputStream(FileDescriptor.in)));
    }

    public static void resetOut() {
        setOut(newPrintStream(new FileOutputStream(FileDescriptor.out), getProperty("sun.stdout.encoding")));
    }

    public static void resetErr() {
        setErr(newPrintStream(new FileOutputStream(FileDescriptor.err), getProperty("sun.stderr.encoding")));
    }

    private static PrintStream newPrintStream(FileOutputStream fos, String enc) {
        if (enc != null) {
            try {
                return new PrintStream(new BufferedOutputStream(fos, 128), true, enc);
            } catch (UnsupportedEncodingException e) {
            }
        }
        return new PrintStream(new BufferedOutputStream(fos, 128), true);
    }

}




これで初期状態と同じになるはずです。



(おわり)