学校では教えてくれない 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); }