java中的标準輸入輸出以及錯誤有時候會讓人迷惑,這裡通過檢視源碼來對他們深入了解一下。
定義
- 标準輸入 System.in
- 标準輸出 System.out
- 标準錯誤 System.err
初始化分析
檢視 java API或者源碼可以看到定義(java.lang.System類):
public final static InputStream in = null;
public final static PrintStream out = null;
public final static PrintStream err = null;
繼續檢視源碼我們看in、out、err是如何初始化的呢?
public final class System {
/* register the natives via the static initializer.
*
* VM will invoke the initializeSystemClass method to complete
* the initialization for this class separated from clinit.
* Note that to use properties set by the VM, see the constraints
* described in the initializeSystemClass method.
*/
private static native void registerNatives();
static {
registerNatives();
}
...
可以看到首先System這個類初始化的時候會注冊native方法并執行initializeSystemClass 這個方法進行初始化,那麼我們看initializeSystemClass這個方法内容:
/**
* Initialize the system class. Called after thread initialization.
*/
private static void initializeSystemClass() {
props = new Properties();
initProperties(props); // initialized by the VM
sun.misc.VM.saveAndRemoveProperties(props);
lineSeparator = props.getProperty("line.separator");
sun.misc.Version.init();
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")));
...
這裡我删掉了部分注釋與後續代碼,友善閱讀,看最後三個方法setInt0、setOut0、setErr0。找到這三個方法:
private static native void setIn0(InputStream in);
private static native void setOut0(PrintStream out);
private static native void setErr0(PrintStream err);
發現又是native方法,這時候就不能繼續檢視native方法了,因為這種方法都是系統層面的C語言實作的,其實可以下載下傳OpenJDK看一下源碼,我這裡下載下傳了OpenJDK8源碼
下載下傳下來解壓,然後打開System.c檢視
vim OpenJDK8-master/jdk/src/share/native/java/lang/System.c
JNIEXPORT void JNICALL
Java_java_lang_System_setIn0(JNIEnv *env, jclass cla, jobject stream)
{
jfieldID fid =
(*env)->GetStaticFieldID(env,cla,"in","Ljava/io/InputStream;");
if (fid == 0)
return;
(*env)->SetStaticObjectField(env,cla,fid,stream);
}
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);
}
JNIEXPORT void JNICALL
Java_java_lang_System_setErr0(JNIEnv *env, jclass cla, jobject stream)
{
jfieldID fid =
(*env)->GetStaticFieldID(env,cla,"err","Ljava/io/PrintStream;");
if (fid == 0)
return;
(*env)->SetStaticObjectField(env,cla,fid,stream);
}
可以看到C代碼對in、out和err初始化的過程。
使用
System.in
對于System.in是很少使用的,使用最多的估計就是剛開始入門學習java的時候的經典echo程式:
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while(sc.hasNext()) {
System.out.println("echo:"+sc.nextLine());
}
}
System.out與System.err
public static void main(String[] args) {
System.out.println("out");
System.err.println("err");
}
輸出結果:
這裡out是黑色,二err為紅色。表面上看就是這點差別。
不過繼續看下面的程式:
public static void main(String[] args) {
for(int i = 0; i<100 ;i++) {
System.out.println(i);
System.err.println(i);
}
}
檢視輸出結果:
按理來說,輸出的結果都應該是成對出現,可是為何最終輸出是亂的呢?
這裡實際上就不是java層面的問題了。因為幾乎對于所有作業系統都有标準輸入、标準輸出、标準錯誤三個概念以及實作,也幾乎都是C語言實作的,C語言的标準輸入輸出是分緩沖和無緩沖的,一般地,标準輸入stdin和标準輸出stdout為行緩沖,标準錯誤輸出stderr無緩沖。
java中的标準輸出以及标準錯誤,都是直接用native方法調用系統的标準輸出以及标準錯誤,是以java中的System.out是行緩沖,System.err是無緩沖。正因為有緩沖區的存在,在同時使用System.out和System.err的時候,err會在out還未達到緩沖界限提前輸出的情況。導緻列印結果是亂的。
類似的還有異常捕獲的時候,檢視Throwable源碼
public void printStackTrace() {
printStackTrace(System.err);
}
可以看到預設的printStackTrace()方法傳入的是System.err。是以如果同時使用System.in與printStackTrace()也可能出現亂序問題,當然實際開發中隻有寫程式測試的時候才會出現這種情況,這種情況對開發也沒用影響,隻是看到現象不用奇怪即可。