靈魂拷問:你真的理解System.out.println()打印原理嗎?

原創/朱季謙安全

 靈魂拷問,這位獨秀同窗,你會這道題嗎?多線程

 請說說,「System.out.println()」原理......性能


這應該是剛開始學習Java時用到最多一段代碼,迄今爲止,與它算是老朋友了。既然是老朋友,就應該多去深刻了解下其「心裏」深處的「真正想法」。
學習

在深刻了解以前,先給本身提幾個問題:測試

System是什麼?out是什麼?println又是什麼?三個代碼組成爲什麼能實現打印信息的功能?this

接下來,咱們就帶着問題,去熟悉咱們這位相處已久的老夥計。spa

 

先從System開始一步一步探究。線程

在百度百科上,有對System作了這樣的說明:System類表明系統,其中系統級的不少屬性和控制方法都放置在該類的內部。3d

簡而意之,該類與系統有關,可獲取系統內部的衆多屬性以及方法,其部分源碼以下:日誌

 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三者,它們分別表明標準輸入流,標準輸出流,標準錯誤輸出流。

image

到這一步,即可以逐漸看到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是一個「文件描述符」,能夠通俗地把它當成一個文件,它有如下三個屬性:

  1. in:標準輸入(鍵盤)的描述符

  2. out:標準輸出(屏幕)的描述符

  3. 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")));

1 private static native void setOut0(PrintStream out);

可知,該方法是一個native方法,感興趣的童鞋可繼續深刻研究,這裏大概就是將生成的PrintStream對象賦值給System裏的靜態對象引用變量:out。

1 public final static PrintStream out = null;

到這裏,就回到了咱們最開始的地方:System.out.println,沒錯,這裏面的out,就是經過setOut0來進行PrintStream對象賦值的,咱們既然能拿到了PrintStream的對象引用out,天然就能夠訪問PrintStream類裏的任何public方法裏,包括println(),包括print(),等等。

可提取以上初始化out的源碼重作一個手動打印的測試,如:

image

執行,發現能夠控制檯上打印出"測試打印"四字。

最後,總結一下,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的執行原理。

 

 

 

如有不足,還請指出改正。

相關文章
相關標籤/搜索