学校では教えてくれない System.out.println()

実際に学校で教えてくれるかは別として System.out.println()について考えてみよう。System.out.println()は、Javaを勉強しようとする人が初期の段階から利用するメソッドである。が、しかし、実は謎だらけなのである。

まずは、System.out.println()を分解して考えてみる。

  • Systemはjava.lang.Systemである。
  • outはjava.lang.Systemのフィールドである。
  • Systemクラスのjavadocを眺めると、outの型はPrintStreamであることがわかる。
  • PrintStreamのjavadocを眺めると、printlnメソッドが存在する。

つまり、System.outにPrintStreamのオブジェクトが代入されていて、そのオブジェクトのprintlnメソッドを呼んでいるだけでる。このメソッドを実行することで、標準出力へ文字列が出力できる仕組みとなっている。

これくらいは、まぁ、Javaに慣れてくると問題なく分かると思う。が、outフィールドへ代入されたPrintStreamオブジェクトが、いつ、どこで作成され代入されるのかと考えた瞬間、暗黒面に突入するのである。

さて、実際にJavaソースコードを見てみよう。Javaソースコードは公開されているので、JDKソースコードも簡単に見ることが出来る。以下openjdk7のソースコードを引用していく。

java.lang.Systemは以下の通りである。
早速、outフィールドを見てみよう。え、finalでnullで初期化されてるじゃん!はい、暗黒面突入です。

public final class System {
...中略...
    public final static PrintStream out = null;
...中略...
    public static void setOut(PrintStream out) {
        checkIO();
        setOut0(out);
    }
...中略...
    private static native void setOut0(PrintStream out);
...中略...
    private static void checkIO() {
        SecurityManager sm = getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("setIO"));
        }
    }
...中略...
   /**
     * Initialize the system class.  Called after thread initialization.
     */
    private static void initializeSystemClass() {
...中略...
        FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
...中略...
        setOut0(new PrintStream(new BufferedOutputStream(fdOut, 128), true));

とりあえず、nullの件は置いておこう。

さて、よく見るとoutはpublic staticなフィールドなのである。まぁ、そうだよね。System.outって使い方するんだから、privateな訳がない。まてよ、System.outって書けるって事は、自分で作ったクラスなどからoutに値を代入するコードを書いてしまえば、自由に書き換えられるやん。って、そうはいかなくて、finalなのですよ、final。残念でした。まぁ、そうだよね。じゃあ、初期化しかできないので、そこでPrintStreamのオブジェクト作って入れてしまえば終了のはずなんだけど、実際はnullなのである。

なんでnullなのか、そこはもう完全に暗黒の世界、JVMの世界である。JVMが起動したときに、様々な処理が走り出すわけだけど、java.なクラスもロードされるわけだけど、java.lang.Systemがロードされて初期化しようとしたときには、まぁ、ぜんぜん準備が完了していなくて何もできないのよね。とりあえず、ある程度終わってからでないと出来ませんと、そういうことです。だから、とりあえず、nullいれてます。あとで、JVMの方から書き換えたらええやんって事かと。あ、JVMの初期化すべて見ていないので推測です。で、その部分がinitializeSystemClassメソッドなわけです。このメソッドはJVMの初期化ルーチンの中のThread周りの初期化が終わった時点で呼び出されます。

で、まぁ、initializeSystemClassメソッドを呼んで、後から書き換えるのはいいとして、まだ問題あるのですよ。そう、outフィールドはfinalなんですよ、final。はい、書き換えられません。で、どうするかというと、秘技JNIです。setOut0ネイティブメソッドってのがあります。やっていることは、引数で渡されたPrintStreamオブジェクトをoutフィールドへ代入しているだけです。ソースコードは以下の通り。

/*
 * The following three functions implement setter methods for
 * java.lang.System.{in, out, err}. They are natively implemented
 * because they violate the semantics of the language (i.e. set final
 * variable).
 */
...中略...
JNIEXPORT void JNICALL
Java_java_lang_System_setOut0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"out","Ljava/io/PrintStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}

単純ですね。本当に代入しているだけです。

ちなみに、setOutっていうpublicメソッドってのもあって、これを使うと、自分で作ったPrintStreamオブジェクトをoutフィールドへ代入できてしまいます。単純にはできなくてcheckIO()ってメソッドが挟まっているので、セキュリティマネージャのパーミッションがないと実行できないんですけどね。

あと、JVMの初期化部分を以下に掲載。JVMのcppで書かれており、結構リーディングするのが大変でございます。
まぁ、気が向いたらこのへんの話もかくかもしれません。ブートストラップクラスローダやいろんなスレッドの話。

jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
...中略...
      call_initializeSystemClass(CHECK_0);
static void call_initializeSystemClass(TRAPS) {
  klassOop k =  SystemDictionary::resolve_or_fail(vmSymbols::java_lang_System(),
 true, CHECK);
  instanceKlassHandle klass (THREAD, k);

  JavaValue result(T_VOID);
  JavaCalls::call_static(&result, klass, vmSymbols::initializeSystemClass_name()
,
                                         vmSymbols::void_method_signature(), CHE
CK);
}