Java Tutorial 第一堂(4)套件、I/O 與例外




接著我們要來看看,第一個 HelloWorld.java 到底寫了什麼,為了方便,將程式碼在這邊再貼一次:
import java.nio.charset.Charset;
import java.io.IOException;
import java.nio.file.*;
import static java.lang.System.*;

public class HelloWorld {
    public static void main(String[] args) {
        Path source = Paths.get(args[0]);
        Charset charset = Charset.forName("UTF-8");
        try {
           for(String line : Files.readAllLines(source, charset)) {
               out.println(line);
           }
        } catch(IOException ex) {
           err.println(ex.getMessage());
        }
    }
}

這個程式碼就包括了 Java 中套件管理、輸入輸出(Input / Output)、字元編碼與例外處理等的特色,與其只給你一個簡單的 Hello! World! 程式,不如一開始就來面對它們。

套件管理

程式開發中會使用到程式庫,程式庫中會包括許多類別,為了避免彼此名稱衝突,也為了避免檔案彼此覆蓋,Java 提到了套件(Package)來加以管理,之後還會介紹套件的 package 語法與實體架構。目前你需知道的是,在程式中會使用到全名(Fully qualified name)java.nio.charset.Charsetjava.io.IOException 兩個類別,其中 java.nio.charsetjava.io 分別是 CharsetIOException 的套件名稱。不過每次都要打這麼長的名稱,撰寫上過於冗長,因而使用 import 語句事先聲明全名,之後程式中就可以使用 CharsetIOException 來代替全名。

如果有很多個類別都擁有相同的套件名稱,逐一使用 import 事先宣告全名也是很累人的事,此時可以在套件名稱之後使用 * 來表示將會用到此套件下數個類別,例如 import java.nio.file.*

在 Java 中可使用套件組織類別之外,對於一些語言中有的函式(Function),Java 可使用靜態(static)方法定義於類別之中,也就是說,類別是作為靜態方法的名稱空間(Namespace);具體實例就是範例中定義的 main 靜態方法,是以 HelloWorld 類別作為名稱空間,除了靜態方法,也可以定義靜態變數,也就是以類別作為名稱空間的變數;對於靜態方法或變數,建議以類別名稱來存取,例如 System.out

因此你也可以看出,Paths.get 表示了 get 是定義在 Paths 類別中的靜態方法,而 Charset.forName 表示了 forName 是定義在 Charset 中的靜態方法。

有時類別名稱寫出來會讓程式比較清楚,但有時對程式可讀性沒有幫助時反而顯得太過麻煩,此時可以使用像是 import static java.lang.System.* 這樣的方法,來宣告將使用到 java.lang.System 下的靜態變數或方法,之後就可以直接以變數名或方法名來進行操作。

輸入輸出

Java 的程式進入點是 public static void main(String[] args),為何這麼寫?要給簡單答案的話「這是規定」,要解釋也可以,main 是公開(public)方法,如此才能在其他套件中取用,它是靜態成員,因此 JVM 就可以在收集好命令列引數(Command line arguments)為一個字串陣列 args 後,以 HelloWorld.main(args) 來呼叫它。

在 Java 中進行輸入輸出可以基於串流(Stream)程式庫,或是基於高階的 New IO 程式庫,Files.readAllLines(source, charset) 就是高階 New IO 程式庫的功能(嚴格來說,這邊用到了 JDK7 增加的 NIO2 功能,在這之前,則是 JDK1.4 增加的 NIO 功能),可以讀取指定來源路徑的內容,依指定字元編碼轉換為字串,傳回 List 表示檔案的每一行。知道為何這邊要使用 UTF-8 嗎?在 Ubuntu 12.04 LTS 中,預設文字編碼是 UTF-8

for 迴圈語法可以與 List 結合,for(String line : Files.readAllLines(source, charset)) 的讀法是「對於 Files.readAllLines」讀取的每一行,指定給 line 變數,然後執行迴圈內容。

迴圈中的 out 實際上 java.lang.System 中定義的靜態變數 out,代表標準輸出(Standard output),通常指終端機,out.println 要求在標準輸出中列示一行並產生換行字元。類似地,errjava.lang.System 中定義的靜態變數 err,代表標準錯誤(Standard error)輸出。

例外

程式中的錯誤總是無所不在,Java 中對於錯誤的表示,不以某個常數來表示,這也是為何程式進入點宣告為 void 表示不傳回值的原因。例外(Exception)是 Java 表示錯誤的方式之一,我們可以嘗試(try)執行程式區塊,如果有錯誤發生的話,會被包裝為例外物件,如果想要針對錯誤處理,可以捕捉(catch)例外。以上例來說,我們嘗試執行:
try {
    for(String line : Files.readAllLines(source, charset)) {
        out.println(line);
    }
}

如果 Files.readAllLines 真的發生輸入輸出上的錯誤,會拋出 IOException,因為程式中指定捕捉該例外:
catch(IOException ex) {
    err.println(ex.getMessage());
}

如果真的發生錯誤了,就可取得訊息並透過標準錯誤輸出來顯示。只是,我們怎麼知道在這邊要捕捉 IOException?因為當初設計 Files.readAllLines 方法時,就好心地在方法的原始碼上聲明了,這個方法會拋出 IOException,這可以在 文件 查詢到:

java-tutorial-the-1st-class-4-1


這麼做的好處之一是,只要查詢文件,不用看原始碼,也可以知道這個方法會拋出什麼例外,實際上,如果你忘了要處理例外,像是 …
import java.nio.charset.Charset;
import java.nio.file.*;

public class HelloWorld {
    public static void main(String[] args) {
        Path source = Paths.get(args[0]);
        Charset charset = Charset.forName("UTF-8");
        for(String line : Files.readAllLines(source, charset)) {
            System.out.println(line);
        }
    }
}

忘了要處理宣告的 IOException 例外,編譯器也會以編譯失敗來提示你必須處理 IOException
/home/codedata/Hello/src/main/java/HelloWorld.java:8: error: unreported exception IOException; 
must be caught or declared to be thrown for(String line : Files.readAllLines(source, charset)) { ^ 1 error

很貼心的設計嗎?實際上,Java 是唯一採用這種作法的語言,然而這也是 Java 最常被攻訐的缺點之一,因為這必須造成語法上的繁瑣,在應用程式演化至一定規模時,也會遭遇到設計上的麻煩,有時你就是不想或無法在現階段處理例外,也得明確地採用某個處理方式,像是使用這邊的 try-catch,或是在方法聲明 throws,像是在設計 Files.readAllLines 方法時,並不會知道到時使用者會在哪個環境中呼叫,因此 Files.readAllLines 無法私自處理掉 IOException,只好在程式碼上使用 throws 宣告 IOException,將 IOException 的處理權交給呼叫 Files.readAllLines 方法的使用者。

無論如何,Java 一直堅持著這樣的例外處理風格,這就成了 Java 的特色了(Java 是門囉囉嗦嗦的語言,只是它有它囉嗦的道理),與其抗拒它,不如正確地認識如何使用它,這會是日後你繼續深入 Java 時必修的課題。

第一堂課的內容就到這邊了,我們認識了 Java 環境的安裝與準備,寫了第一個 Java 程式,瞭解了一些套件管理、輸入輸出以及例外處理等 Java 中特有的特性,休息一下!後面要來進入第二堂課,瞭解更多 Java 語言的細節。