靈魂拷問:你真的理解System.out.println()執行原理嗎?
原創/朱季謙
靈魂拷問,這位獨秀同學,你會這道題嗎?
請說說,「System.out.println()」原理……
這應該是剛開始學習Java時用到最多一段程式碼,迄今為止,與它算是老朋友了。既然是老朋友,就應該多去深入了解下其「內心」深處的「真正想法」。
在深入了解之前,先給自己提幾個問題:
System是什麼?out是什麼?println又是什麼?三個程式碼組成為何能實現列印資訊的功能?
接下來,我們就帶著問題,去熟悉我們這位相處已久的老夥計。
先從System開始一步一步探究。
在百度百科上,有對System做了這樣的說明:System類代表系統,其中系統級的很多屬性和控制方法都放置在該類的內部。
簡而意之,該類與系統有關,可獲取系統內部的眾多屬性以及方法,其部分源碼如下:
1 public final class System { 2 private static native void registerNatives(); 3 static { 4 registerNatives(); 5 } 6 private System() { 7 } 8 public final static InputStream in = null; 9 public final static PrintStream out = null; 10 public final static PrintStream err = null; 11 private static volatile SecurityManager security = null; 12 public static void setIn(InputStream in) { 13 checkIO(); 14 setIn0(in); 15 } 16 public static void setOut(PrintStream out) { 17 checkIO(); 18 setOut0(out); 19 } 20 ...... 21 }
打開源碼,發現這是一個final定義的類,其次,該類的構造器是以private許可權進行定義的。根據這兩情況可以說明,該類即不能被繼承也無法實例化成對象,同時需注意一點,就是這個類里定義的很多變數和方法都是static來定義的,即這些類成員都是屬於類而非對象。
因此,若需調用類中的這些帶static定義的屬性或者方法,無需創建對象就能直接通過「類名.成員名」來調用。
在System源碼中,需要留意的是in,out,or三者,它們分別代表標準輸入流,標準輸出流,標準錯誤輸出流。
到這一步,便可以逐漸看到System.out.println中的影子,沒錯,這行程式碼里的System.out,即為引用System類里靜態成員out,它是PrintStream類型的引用變數,稱為”位元組輸出流”。作為static定義的out引用變數,它在類載入時就被初始化了,初始化後,會創建PrintStream對象對out賦值,之後便能調用PrintStream類中定義的方法。
具體怎麼創建PrintStream並賦值給靜態成員out,我放在本文後面講解。
接著,進入到PrintStream類當中——
1 public class PrintStream extends FilterOutputStream 2 implements Appendable, Closeable 3 { 4 ...... 5 public void println() { 6 newLine(); 7 } 8 9 public void println(boolean x) { 10 synchronized (this) { 11 print(x); 12 newLine(); 13 } 14 } 15 16 public void println(char x) { 17 synchronized (this) { 18 print(x); 19 newLine(); 20 } 21 } 22 23 public void println(int x) { 24 synchronized (this) { 25 print(x); 26 newLine(); 27 } 28 } 29 30 public void println(long x) { 31 synchronized (this) { 32 print(x); 33 newLine(); 34 } 35 } 36 37 public void println(float x) { 38 synchronized (this) { 39 print(x); 40 newLine(); 41 } 42 } 43 44 public void println(double x) { 45 synchronized (this) { 46 print(x); 47 newLine(); 48 } 49 } 50 51 public void println(char x[]) { 52 synchronized (this) { 53 print(x); 54 newLine(); 55 } 56 } 57 58 public void println(String x) { 59 synchronized (this) { 60 print(x); 61 newLine(); 62 } 63 } 64 65 ...... 66 }
發現這PrintStream裡邊存在諸多以println名字命名的重載方法。
這個,就是我們本文中最後需要回答的問題,即println是什麼?
它其實是PrintStream列印輸出流類里的方法。
每個有傳參的println方法里,其最後調用的方法都是print()與newLine()。
值得注意一點,這些帶有傳參的println方法當中,裡面都是通過同步synchronized來修飾,這說明System.out.println其實是執行緒安全的。同時還有一點需注意,在多執行緒情況下,當大量方法執行同一個println列印時,其synchronized同步性能效率都可能出現嚴重性能問題。因此,在實際生產上,普遍是用log.info()類似方式來列印日誌而不會用到System.out.println。
在以上程式碼里,其中 newLine()是代表列印換行的意思。
眾所周知,以System.out.println()來列印資訊時,每條列印資訊都會換行的,之所以會出現換行,其原理就是println()內部通過newLine()方法實現的。
若換成System.out.print()來列印,則不會出現換行情況。
為什麼print()不會出現換行呢?
分析一下print()里程式碼便可得知,是因為其方法里並沒有調用newLine()方法來實現換行的——
1 public void print(boolean b) { 2 write(b ? "true" : "false"); 3 } 4 5 public void print(char c) { 6 write(String.valueOf(c)); 7 } 8 9 public void print(int i) { 10 write(String.valueOf(i)); 11 } 12 13 public void print(long l) { 14 write(String.valueOf(l)); 15 } 16 17 public void print(float f) { 18 write(String.valueOf(f)); 19 } 20 21 public void print(double d) { 22 write(String.valueOf(d)); 23 } 24 25 public void print(char s[]) { 26 write(s); 27 } 28 29 30 public void print(String s) { 31 if (s == null) { 32 s = "null"; 33 } 34 write(s); 35 }
這些重載方法裡面都調用相同的write()方法,值得注意的是,在調用write()時,部分方法的實現是都把參數轉換成了String字元串類型,之後進入到write()方法詳情里——
1 private void write(String s) { 2 try { 3 synchronized (this) { 4 ensureOpen(); 5 textOut.write(s); 6 textOut.flushBuffer(); 7 charOut.flushBuffer(); 8 if (autoFlush && (s.indexOf('\n') >= 0)) 9 out.flush(); 10 } 11 } 12 catch (InterruptedIOException x) { 13 Thread.currentThread().interrupt(); 14 } 15 catch (IOException x) { 16 trouble = true; 17 } 18 }
其中,ensureOpen()的方法是判斷out流是否已經開啟,其詳細方法如下:
1 private void ensureOpen() throws IOException { 2 if (out == null) 3 throw new IOException("Stream closed"); 4 }
由方法可得知,在進行寫入列印資訊時,需判斷PrintStream流是否已經開啟,若沒有開啟,則無法將列印資訊寫入電腦,故而拋出說明流是關閉狀態的異常提示:「Stream closed」
若流是開啟的,即可執行 textOut.write(s);
根據個人理解,這裡的textOut是BufferedWriter引用變數,即為常說的IO流里寫入流,最終會將資訊寫入到控制台上,即我們平常說的控制台列印。可以理解成,控制台就是一個文件,但是能被我們實時看到裡面是什麼的文件,這樣當每次寫入東西時,就會實時呈現在文件里,也就是能被我們看到的控制台列印資訊。
那麼,問題來了,哪行程式碼是表示寫入到控制台文件的呢?System、out、println又是如何組成到一起來起作用的?
讓我們回到System類最開始的地方——
1 public final class System { 2 3 /* register the natives via the static initializer. 4 * 5 * VM will invoke the initializeSystemClass method to complete 6 * the initialization for this class separated from clinit. 7 * Note that to use properties set by the VM, see the constraints 8 * described in the initializeSystemClass method. 9 */ 10 private static native void registerNatives(); 11 static { 12 registerNatives(); 13 } 14 15 }
以上的靜態程式碼會在類的初始化階段被初始化,其會調用一個native方法registerNatives()。根據該方法的英文注釋「VM will invoke the initializeSystemClass method to complete」,可知,VM將調用initializeSystemClass方法來完成該類初始化。
我們找到該initializeSystemClass方法,下面只列出本文需要用到的核心程式碼,稍微做了一下注釋:
1 private static void initializeSystemClass() { 2 //被vm執行系統屬性初始化 3 props = new Properties(); 4 initProperties(props); 5 sun.misc.VM.saveAndRemoveProperties(props); 6 7 //從系統屬性中獲取系統相關的換行符,賦值給變數lineSeparator 8 lineSeparator = props.getProperty("line.separator"); 9 sun.misc.Version.init(); 10 //分別創建in、out、err的實例對象,並通過set()方法初始化 11 FileInputStream fdIn = new FileInputStream(FileDescriptor.in); 12 FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out); 13 FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err); 14 setIn0(new BufferedInputStream(fdIn)); 15 setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding"))); 16 setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding"))); 17 18 ...... 19 }
主要關注這兩行程式碼:
1 FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out); 2 setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
一.這裡逐行進行分析,首先FileDescriptor是一個「文件描述符」,可以通俗地把它當成一個文件,它有以下三個屬性:
-
in:標準輸入(鍵盤)的描述符
-
out:標準輸出(螢幕)的描述符
-
err:標準錯誤輸出(螢幕)的描述符
FileDescriptor.out代表為「標準輸出(螢幕)」,可以通俗地理解成標準輸出到控制台的文件,即表示控制台。
new FileOutputStream(FileDescriptor.out)該行程式碼即說明通過文件輸出流將資訊輸出到螢幕即控制台上。
若還是不理解,可舉一個比較常見的例子——
1 public static void main(String[] args) throws IOException { 2 FileOutputStream out=new FileOutputStream("C:\\file.txt"); 3 out.write(66); 4 }
這是比較簡單的通過FileOutputStream輸出流寫入文件的寫法,這裡的路徑「C:\file.txt」就與FileDescriptor.out做法類似,都是描述一個可寫入數據的文件,只不過FileDescriptor.out比較特殊,它描述的是螢幕,即常說的控制台。
二.接下來是newPrintStream(fdOut, props.getProperty(“sun.stdout.encoding”))——
1 private static PrintStream newPrintStream(FileOutputStream fos, String enc) { 2 if (enc != null) { 3 try { 4 return new PrintStream(new BufferedOutputStream(fos, 128), true, enc); 5 } catch (UnsupportedEncodingException uee) {} 6 } 7 return new PrintStream(new BufferedOutputStream(fos, 128), true); 8 }
該方法是為輸出流創建一個BufferedOutputStream緩衝輸出流,起到流緩衝的作用,最後通過new PrintStream()創建一個列印輸出流。
通過該流的列印介面,如print(), println(),可實現列印輸出的作用。
三.最後就是執行 setOut0(newPrintStream(fdOut, props.getProperty(“sun.stdout.encoding”)));
可知,該方法是一個native方法,感興趣的童鞋可繼續深入研究,這裡大概就是將生成的PrintStream對象賦值給System里的靜態對象引用變數:out。
到這裡,就回到了我們最開始的地方:System.out.println,沒錯,這裡面的out,就是通過setOut0來進行PrintStream對象賦值的,我們既然能拿到了PrintStream的對象引用out,自然就可以訪問PrintStream類里的任何public方法里,包括println(),包括print(),等等。
可提取以上初始化out的源碼重做一個手動列印的測試,如:
執行,發現可以控制台上列印出”測試列印”四字。
最後,總結一下,System.out.println的原理是在類載入System時,會初始化System的initializeSystemClass()方法,該方法中將創建一個列印輸出流PrintStream對象,隨後通過setOut0(PrintStream out)方法,會將初始化創建的PrintStream 對象賦值給System靜態引用變數out。out被賦值對象地址後,就可以調用PrintStream中的各種public修飾的方法里,其中就包括println()、print()這類列印資訊的方法,通過out.println(「xxxx」)即可將「xxxx」列印到控制台上,也就是等價於System.out.println(“xxxx”)。
1 System.out.println("列印數據"); 2 等價於---> 3 PrintStream out=System.out; 4 out.println("列印數據");
以上,就是System.out.println的執行原理。
若有不足,還請指出改正。