Java I/O系統學習四:標準IO
幾乎所有學習Java的同學寫的第一個程式都是hello world,使用的也都是System.out.println()這條語句來輸出"hello world",我也不例外,當初學的時候只是簡單拿來用,平時學習的時候需要列印日誌也會使用這條語句,並沒有去探究這條語句背後的原理,本文就來研究一下其原理。
System.out.println()提供的能力屬於標準I/O的範疇,標準I/O這個術語參考的是Unix中“程式所使用的單一資訊流”這個概念,即程式的所有輸入都可以來自標準輸入,它的所有輸出也可以傳送到標準輸出,以及所有的錯誤資訊都可以傳送到標準錯誤。標準I/O的意義在於:我們可以很容易地把程式串聯起來,一個程式的標準輸出可以成為另一程式的標準輸入。
其實對於標準I/O,直觀一點的理解可以是來自命令列的I/O,因為程式通常是在命令列下執行的,並且是在命令列環境下和使用者互動的。所以Java平臺提供了兩種方式用於和程式進行互動:通過標準流的方式和通過控制檯的方式。
標準流是很多作業系統都有的一個特性。預設情況下,標準流是從鍵盤讀取輸入並且輸出到顯示裝置(顯示器)。同時標準流也支援輸入輸出到檔案或者程式之間的I/O,但是這兩種特性是由命令列直譯器來決定的,而不是程式。
Java平臺支援三種標準流:
- 標準輸入,通過System.in獲取
- 標準輸出,通過System.out獲取
- 標準錯誤,通過System.err獲取
這些物件是預定義好的並且不需要手動開啟。標準輸出(Standard Output)和標準錯誤(Standard Error)都是用於輸出,提供錯誤輸出的好處是使用者可以將正常輸出指向一個檔案,同時還能夠讀取錯誤資訊。
下面就來詳細介紹一下這些標準流。
1. 標準輸出
1.1 類結構
說到標準輸出就不得不說常見的System.out.println()了,這是一條Java語句,沒錯,它可以將程式傳給System.out(標準輸出)的引數打印出來。我們可以將其分成三部分來看:
System
這是java.lang包中的一個final類,主要的作用如下:
- 提供瞭如標準輸入,標準輸出和錯誤輸出流等基礎設施;
- 可以訪問外部定義的屬性和環境變數;
- 提供了一種載入檔案和庫的方法;
- 提供了可以快速複製陣列的部分內容的方法;
out
這是System類的一個靜態成員欄位,型別是PrintStream。其訪問識別符號是public final,這意味著它在啟動時就會被例項化,並與主機的標準輸出控制檯進行關聯,並且該流在例項化之後會自動開啟,並準備接受資料。
println
這是PrintStream的一個方法,可以輸出內容到控制檯。
這裡直接盜用一張網上的類圖,結合起來看會更清晰其結構:
說完類結構,我們再來看看System.out的一些其他操作。
1.2 將System.out轉換成PrintWriter
System.out是一個PrintStream,而PrintStream是一個OutputStream。PrintWriter有一個可以接受OutputStream作為引數的構造器。所以,可以使用那個構造器將System.out轉換成PrintWriter:
public class ChangeSystemOut{ public static void main(String[] args){ PrintWriter out = new PrintWriter(System.out, true); out.println("Hello, world"); } }
這樣包裝了之後就可使用PrintWriter的功能了,這裡使用了有兩個引數的PrintWriter構造器,並將第二個引數設為true,以便開啟自動清空功能,不然的話可能看不到輸出。
1.3 輸出重定向
System.out中的out物件是可以手動指定的。預設會在Java執行環境啟動時進行初始化,並且可以在執行時改變其實際物件。我們可以通過setOut方法來將輸出重定向,比如下面的例子,將輸出重定向到一個檔案中:
public class ChangeOut { public static void main(String args[]) { try { System.setOut(new PrintStream(new FileOutputStream("log.txt"))); System.out.println("Now the output is redirected!"); } catch(Exception e) {} } }
在有大量輸出顯示在螢幕並且這些輸出滾動得太快以至於無法閱讀時,重定向輸出就變得極為有用。
1.4 System.out.println的效能分析
眾所周知,System.out.println的效能並不好,為什麼呢?我們可以看一下其呼叫順序:println - > print - > write()+ newLine(),這個是在Sun / Oracle JDK中的實現。其中write()和newLine()方法都包含了一個synchronized塊,同步的方式會有一點開銷,不過呢更影響效能的則是新增字元到緩衝區和列印。
有文獻表明,執行多個System.out.println並記錄時間,執行時間會按比例增加。當列印超過50個字元並列印超過50,000行時,效能下降明顯。
當然雖然System.out.println()效能不好,但是還是取決我們的使用場景,如果是寫寫demo學習則直接使用好了,因為是Java原生支援的特性,所以不需要引入任何依賴,這是其最大的好處吧。當然,在我們工作中開發商用軟體,那就最好不要用System.out.println了,這就不僅僅是因為效能問題了。
1.5 System.out.println和通用日誌元件的對比
為了方便,我們可能常常會直接使用System.out.println()輸出日誌,但是既然用System.out輸出日誌這麼方便,那又為什麼還需要那些通用日誌元件(如log4j)呢?System.out.println()又存在什麼問題?如下是一些常見的總結:
- 靈活性:像log4j這一類的通用元件提供了多種日誌級別,這樣就可以通過不同級別相應地分隔日誌資訊。例如,X訊息只能在PRODUCTION級別列印,Y訊息應列印在ERROR級別列印等,詳細的級別定義這裡就不再總結了。
- 可重構性:log4j只需一個引數更改即可關閉所有日誌記錄。
- 可維護性:想象一下,如果我們有數百個System.out.println散落在應用程式的各個角落,那將會使程式變得難以維護。
- 粒度:在應用程式中,每個類都可以有不同的記錄器並相應地進行控制。
- 實用性:在System.out中重定向訊息的選項比較少(指向檔案、指向程式),但是像log4j之類的元件,其提供了更多的重定向選擇,我們甚至可以重定向到自定義的輸出選項。
所以呢如果我們只是正在編寫一個小demo,只是為了實驗/學習目的那麼使用System.out.println是很方便的。但是當我們要開發軟體時,我們就應該使用通用的日誌元件比如log4j等。
2. 標準輸入、標準錯誤
前面我們著重學習了一下標準輸出,這裡再總結一下它的兄弟:標準輸入和標準錯誤。
標準輸入和標準輸入剛好相反,是用來從標準輸入(一般是鍵盤)裝置獲取輸入的。而標準錯誤則是通過PrintStream將錯誤資訊列印到標準錯誤輸出流中,在我們使用比如eclipse這種IDE時就可以看出它和標準輸出的區別。看一個簡單例子:
public class InOutErr { public static void main(String args[]) { try { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String filename = reader.readLine(); InputStream input = new FileInputStream(filename); System.out.println("File opened..."); } catch (IOException e){ System.err.println("Where is that file?"); } } }
啟動程式之後會阻塞,等待輸入檔名稱,隨意輸入,如果找不到對應檔案,就會輸出錯誤日誌,可以看一下結果,err的列印是紅色的。
同樣,標準輸入和標準錯誤也可以進行重定向,可以通過System提供的一些靜態方法完成重定向:
- setIn(InputStream)
- setOut(PrintStream)
- setErr(PrintStream)
這裡是一個簡單例子演示這些方法的使用:
public class Redirecting { public static void main(String[] args) throws IOException{ PrintStream console = System.out; BufferedInputStream in = new BufferedInputStream(new FileInputStream("pom.xml")); PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("test.out"))); System.setIn(in); System.setOut(out); System.setErr(out); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String s; while((s = br.readLine()) != null){ System.out.println(s); } out.close(); // Remember this! System.setOut(console); } }
這個例子將標準輸入重定向到檔案上,並將標準輸出和標準錯誤重定向到另一個檔案上。注意,它在程式開頭處儲存了對最初的System.out物件的引用,並且在結尾處將系統輸出恢復到了該物件上。
I/O重定向操縱的是位元組流,而不是字元流,所以這裡使用的是InputStream和OutputStream,而不是Reader和Writer。
3. 標準輸入和輸出的區別
標準輸入和輸出除了一個是輸入,一個是輸出,還有使用上的一些區別。
由於某些歷史原因,標準流屬於位元組流,System.out和System.err其實是PrintStream型別。雖然是位元組流,但是PrintStream利用一個內部字元流物件來字元流的許多特性。
相對標準輸出而言System.in就只是一個單純的位元組流了,沒有包含內部的字元流物件。如果要像字元流一樣使用標準輸入則需要通過InputStreamReader將其包裝一下了:
InputStreamReader cin = new InputStreamReader(System.in);
標準輸出和標準輸入在這一點上的區別也可從兩者的使用上看出來,以我們最常用的在控制檯列印一條語句和從控制檯接收鍵盤輸入為例:
// 列印日誌 System.out.println(""); // 接收標準輸入 Scanner scan = new Scanner(System.in);
前者直接使用的是字元流的特性,後者則通過了一個Scanner進行包裝。
4. 總結
- Java平臺提供了三種標準流,分別是System.in(標準輸入)、System.out(標準輸出)、System.err(標準錯誤),我們常用的System.out.println()就是屬於標準輸出。
- System是java.lang包中的一個final類,out則是System類的一個靜態成員,其型別為PrintStream,println()則是PrintStream的一個方法,可以輸出內容到控制檯。
- 標準流屬於位元組流,System.out和System.err其實是PrintStream型別,但是具有許多字元流的特性,而System.in就只是一個單純的位元組流。
- 標準流都可以進行重定向。
- System.out.println()效能並不好,但是平時學習使用是不影響的。
- 在專案中儘量不要使用System.out.println()輸出日誌,而應該使用更通用的日誌元件來完成日誌列印的任務。
&n