Java SE 在 1.4 時加入了 NIO (New I/O) 的新API,事隔多年後,在 Java SE 7 裡加入了第二代 NIO - NIO2 (JSR-203)。
第二代的 NIO2 新增了三個主要的功能:
1. File System API 的增強
2. Asynchronous IO
3. 其它
[b]**********[/b]
File System API 從 Java 1.0 開始,十幾年來一點都沒改進,而且功能少的可以!這次的 NIO2 總算增強、補齊了這塊拼圖。主要的改進有:
[b]* 不同平台的完整支援[/b]
早期的 File System API 不論是對 Windows 或是 Linux 平台的檔案系統都不友善,你只能用它來做基本的檔案存取,沒有辦法做進階地處理,像是檔案的權限等…。而新的 File System API 則是依各個平台的不同,而有不同的實作,能完整地支援各個平台其檔案系統的特色。而且程式開發者可以透過標準且統一的介面,來操作檔案系統,不需要擔心底層到底是什麼平台。
[b]* 完整的檔案操作[/b]
舊的 File System API 只能簡單地刪除檔案,想要拷備或是搬移檔案,都得自己處理。而新的 File System API 提供了完整的 拷備(copy)、搬移(move)和刪除(delete)的方法,讓你能輕易地操作這個檔案。
[b]* 檔案捷徑(symbolic link)的支援[/b]
檔案捷徑不是一個完整的檔案,而是某一個實體檔案的替身,因為它不是一個完整的檔案,所以舊的 File System API 並沒有辦法正確的處理它,現在透過新的 File System API 就沒這個困擾了。
[b]* 完整檔案屬性的存取[/b]
一個檔案有許多不同的屬性(attribute),例如是誰建立了這個檔案?哪些人有這個檔案的存取權限等…而不同的平台上會有一些該平台特別的屬性,像是隱藏檔案的屬性,在Windows 和 Linux 平台上就不一樣。新的 File System API 提供了完整存取不同平台檔案屬性的方法。
[b]* 其它 [/b]
其它新的 File System API 功能有,像是能走訪整個目錄(包含目錄下的檔案及其子目錄)的 File tree walk API;還有檔案系統的監看API (WatchService),你可以用它來監看檔案系統裡有哪些檔案被新增、修改或刪除了。還有像是檔案過濾器(Glob),你可以在讀取檔案列表時,設定你要的過濾條件,只列出你想要的檔案,例如所有的 .java 檔案或是 .jpg 案。新的 File System API 真的是大大補完了原本所欠缺的功能,應該能滿足你絕大部份的檔案操作需求。
[b]**********[/b]
在 Java SE 7之前,所有的 I/O 操作都是同步的(synchronous),所謂的同步操作是指檔案存取時,程式必需等在那裡,一直到檔案資料都讀完/存完後,才會執行下一行程式碼。而 asynchronous 是非同步的意思,跟同步最大的差別在於,例如我要把一張很大的圖片存起來,我只要執行了存檔的程式碼後,程式就可以去執行別的程式碼,當存好後,Java 會自動通知你存好了,然後你再回來處理下一個動作。非同步 I/O 對於大量的檔案處理是巨大的檔案處理時,非常地有效率,程式不會因為長時間的檔案存取,而整個程式像當機一樣卡在那裡。
[b]**********[/b]
其它新的 NIO2 的功能還有支援 GB 級的存取緩衝區(buffer)、網路廣播(Multicasting)等。
NIO2 的東西實在太多了,在這裡只能給大家一個簡單的介紹,有興趣的讀者可以進一步查閱 NIO2 的相關文件,筆者也會慢慢地把相關的範例程式給補上,敬請期待~
JDBC 是 Java Database Connectivity 的縮寫,它是讓你的 Java 程式跟資料庫溝通的一組 API,透過這個統一的 API 介面,你可以連接各式不同的資料庫系統,對資料庫裡的資料做新增、刪除、修改、查詢等動作。
Java SE 7 裡支援 JDBC 4.1 的規格,跟JDBC 4.0 版比較起來,有兩個主要的改進。第一個就是前面也介紹過的 try-with-resource 語法的支援,第二就是新的 RowSetFactory 這個新的 API。
前面在介紹(第21到24天) try-with-resource 語法時,我們只介紹了 java.io 的部份(還有自創的類別),而 JDBC 4.1 裡的 [b]java.sql.Connection[\b]、[b]java.sql.ResultSet[\b]、[b]java.sql.Statement[\b] 這三個介面(interface) 也都繼承了 [b]java.lang.AutoCloseable[/b] 介面,所以當你用到實作這三個介面的 SQL 物件時,也能將宣告這些物件的陳述式寫在 try-with-resource 的陳述式裡,這樣就不怕忘記關閉資料庫資源了。
範例:
[code]
package idv.jacky.ironman4.day29;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class Day29Example1 {
public static void sampleQueryProcessing(Connection sampleCon) throws SQLException {
String sampleQuery = "select * from STUDENT";
try (Statement sampleStmt = sampleCon.createStatement()) {
ResultSet rs = sampleStmt.executeQuery(sampleQuery);
while (rs.next()) {
String studentName = rs.getString("NAME");
String studentAge = rs.getString("AGE");
System.out.printf(" NAME: %S, AGE: %s%n", studentName, studentAge);
}
}
}
}
[/code]
RowSetFactory 是一組新的 API,早期沒有 RowSetFactory 時,我們必需把實作 RowSet 的類刟名稱寫死在程式碼裡(因為各家的資料庫都會提供自己的類別)。現在則可以透過 RowSetFactory 這個 API 來統一產生 RowSet 物件,而不需要去管到底是什麼類別實作的。這樣一來,你的程式就更有彈性,只需要更改系統的參數,例如在執行 Java 程式前,用命令列參數來設定:
[b]java -Djavax.sql.rowset.RowSetFactory=com.sun.rowset.RowSetFactoryImpl[/b]
如此你就可以隨時抽換使用不同的實作類別,相當的方便。而當你不指定時,Java 會預設使用 com.sun.rowset.RowSetFactoryImpl。
範例:
[code]
package idv.jacky.ironman4.day29;
import java.sql.SQLException;
import javax.sql.rowset.JdbcRowSet;
import javax.sql.rowset.RowSetFactory;
import javax.sql.rowset.RowSetProvider;
public class Day29Example2 {
public void sampleMethod(String sampleUserName, String samplePassword) throws SQLException {
RowSetFactory sampleRowSetFactory = null;
JdbcRowSet sampleRowSet = null;
try {
sampleRowSetFactory = RowSetProvider.newFactory();
sampleRowSet = sampleRowSetFactory.createJdbcRowSet();
sampleRowSet.setUrl("jdbc:sampleDriver:sampleAttribute");
sampleRowSet.setUsername(sampleUserName);
sampleRowSet.setPassword(samplePassword);
sampleRowSet.setCommand("select * from STUDENT");
sampleRowSet.execute();
} catch(Exception e) {
e.printStackTrace();
}
}
}
[/code]
07 十一月, 2011 00:25
Project Coin 裡的最後一個新功能叫 簡化變動參數方法的呼叫 (Simplified varargs method invocation),從名稱上看不出個所以然,它又跟昨天提到的 Heap Pollution 有什麼關係呢?
變動參數也是在 Java 1.5 中新增的功能,而它特殊的 [b]…[/b] 語法,實際上就是轉換成陣列使用,再配合泛型的不特定型別的宣告,在某些情況下,也是會造成 Heap Pollution。請看下面這個例子:
[code]
package idv.jacky.ironman4.day26;
public class Day26Example1 {
public static void main(String[] args) {
System.out.printf("Sum = %d%n", Util2.sum(args));
}
}
[/code]
[code]
package idv.jacky.ironman4.day26;
public class Util2 {
public static <T> int sum(T...numbers) {
int sum = 0;
for(T i : numbers)
sum += ((Number)i).intValue();
return sum;
}
}
[/code]
我們把 sum 方法的參數,改成使用 varargs 的語法,我們大譫地假設呼叫 sum 方法的人,所傳進來的物件,都是 Number 類別的物件,或其子類別物件,例如 Integer, Double等…然後呼叫 Number 的 intValue 方法,再把它們通通加起來。
問題就出在我們的「大膽假設」上,[b]T[/b] 指的是不定型別,也就是說我們不把 sum 方法的參數型別給寫死,完全在執行時期時,才去動態的傳入。問題就在於如果傳入的不是 Number 或其子類別物件的話,那就會遇到 Heap Pollution 的問題。
上面的範例,在編譯時加上 [b]XIint:unchecked[/b] 的參數的話,就會看到 Java 所丟出來的警告訊息:
[b]
/Library/Java/JavaVirtualMachines/1.7.0.jdk/Contents/Home/bin/javac -Xlint:unchecked Util2.java
Util2.java:6: warning: [unchecked] Possible heap pollution from parameterized vararg type T
public static <T> int sum(T...numbers) {
^ where T is a type-variable:
T extends Object declared in method <T>sum(T...)
1 warning
[b]
這次 Java 就很明確的告訴你,Util2.sum 方法可能會有 Heap Pollution 的問題!
如果我們真的非常肯定,Util2.sum 方法只有我們自己會用,且一定會傳入正確型別的變數進去,例如:
[code]
package idv.jacky.ironman4.day26;
import java.util.ArrayList;import java.util.List;
public class Day26Example2 { public static void main(String[] args) { List<Double> numbers = new ArrayList<>(); for(String str : args) numbers.add(Double.parseDouble(str)); System.out.printf("Sum = %d%n", Util2.sum(numbers)); }}
[/code]
我們不想在編譯時看到剛剛的警告訊息的話,在 Java SE 7 之前,我們可以用一個 Annotation - [b]@SuppressWarings("unchecked")[/b] 來壓制這個警告。在 Java SE 7 裡則多了一個新的 Annotation 特別設計給 varrags 用的,就是 [b]@SafeVarargs[/b]。效果跟 [b]@SuppressWarings("unchecked")[/b] 一樣,我們在 sum 方法宣告的的上一行加上 [b]@SafeVarargs[/b],
[code]
@SafeVarargs
//@SuppressWarnings("unchecked")
public static <T> int sum(T...numbers) {
[/code]
這樣編譯器就會認為你已經知道 Heap Pollution 的風險,而略過對這個方法提出警告。
不過,這只是把警告給壓制下來,實際上 Heap Pollution 的問題並沒有消失,所以在呼叫這類的方法時,還是得特別地注意。
Java 從1.5 版加入泛型的功能後,一直有個潛在的使用問題,那就是 [b]Heap Pollution[/b]。
我們先來看看下面這個範例:
[code]
package idv.jacky.ironman4.day25;
import java.util.Arrays;import java.util.List;
public class Day25Example1 {
public static void main(String[] args) { List numbers = Arrays.asList(args); System.out.printf("Sum = %d%n", Util.sum(numbers)); }}
[/code]
[code]
package idv.jacky.ironman4.day25;
import java.util.List;
public class Util {
public static int sum(List<Integer> ls) {
int sum = 0;
for(Integer i : ls)
sum += i;
return sum;
}
}
[/code]
你應該一眼就能看出來上面這個範例程式哪兒出了問題,問題就在於 Util.sum 方法只能接受 List<Integer> 的參數,但我們卻在第10行把一個 List 物件傳進去,而 List 物件則是透過 Arrays.asList 的方法,把 main 方法 的參數 args 字串陣列,轉成 List 物件。
程式在編譯時,Java 編譯器會丟出一個警告訊息:
[b]
javac Day25Example1.java
Note: Day25Example1.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
[/b]
意思是說程式裡有個未受檢查或不安全的程式碼,建議你在編譯時加上 [b]-Xlint:unchecked[/b] 來看詳細的資料。
[b]
javac -Xlint:unchecked Day25Example1.java
Day25Example1.java:10: warning: [unchecked] unchecked conversion
found : java.util.List
required: java.util.List<java.lang.Integer>
System.out.printf("Sum = %d%n", sum(numbers));
^
1 warning
[/b]
Java 編譯器有檢查到,sum 方法的參數型別應該是 List<Integer>,但你給的是 List。
程式依然可以編譯成功,但執行時就會有例外出現:

所以 [b]Heap Pollution[/b] 指的就是將一個未定參數型別的 Collection 物件,指定給一個有指定參數型別的 Collection 物件後,所隱藏的潛在問題。
既然知道這樣會有潛在的危險,因為我們不能確定所有呼叫 sum 方法的人,傳進來的都是 List<Integer> 物件,那為什麼不能在編譯時就給個錯誤,不讓程式編譯成功呢?主要的原因是,Java 無法知道誰會呼叫 sum 方法。我們的範例很簡單,你一下就看出來哪邊有問題,但今天如果 Util 類別是要給別人使用的,你並沒有辦法知道別人會不會正確的使用它。而 Java 程式裡變數的型別檢查,都是在編譯時其做的,在執行時期,是完全沒有型別資料的。一直要到發生例外時,我們才知道問題出在哪。
所以當你在編譯 Java 程式碼時,如果要你使用 [b]-Xlint:unchecked[/b] 來察看詳細的警告資料時,請花點時間看一下,修正可能會發生錯的程式碼,這樣能確保你的程式能更安全、穩固地執行。
https://sites.google.com/a/prever.co.kr/josh/java/whatisheappollution
05 十一月, 2011 19:52
除了後開先關的規則之外,在使用 try-with-resource 語法時,還有一點要注意的,就是 [b]例外的壓制(Exception Suppressed)[/b]。
我們已經知道 close 方法有可能會丟出例外,那如果在 try 區塊裡也不小心丟出例外時,Java 會怎麼處理這兩個例外呢?我們先宣設計另一個可關閉的類別:
[code]
package idv.jacky.ironman4.day24;
public class MyResource3 implements AutoCloseable{
@Override
public void close() throws Exception {
System.out.println("Close resource 3.");
throw new Exception("MyResource3 Close Exception");
}
}
[/code]
在 MyResource3.close 方法裡,我們它丟出一個 Exception 例外。然後我們這樣來測試一下:
[code]
package idv.jacky.ironman4.day24;
public class Day24Example1 {
public static void main(String[] args) {
try {
foo();
} catch (Exception e) {
System.out.println(e);
}
}
public static void foo() throws Exception {
try (MyResource3 r3 = new MyResource3()) {
System.out.println("Do something...");
throw new Exception("Something error!");
}
}
}
[/code]
我們有個 foo 的方法,方法裡會產生 MyResource3 的物件,然後在 foo 方法的 try 區塊裡也丟出一個例外。而呼叫 foo 的 main 方法裡,用個try-catch 區塊來補捉 foo 方法所丟出來的例外。最後你猜猜在程式的第9行會印出什麼?

很意外嗎? 在 close 方法裡的 "MyResource3 Close Exception" 跑哪去?因為 catch 陳述式裡一次只能補捉一個例外,try 區塊中已經丟出一個例外了,Java 會優先補捉它。一般來說 try 區塊的程式碼是主要的,所以有例外發生時,通常會需要特地去處理;而 close 所丟出來的例外是次要的,你可以依情況進一步處理。
close 方法丟出的例外並沒有不見,只是被壓制(Suppressed)了,Java SE 7 的 Throwable 介面裡(所有的 Exception 都實作這個介面)有個 [b]getSuppressed[/b] 的方法,它就是用來取出所有被壓制的例外!我們來改寫一下剛剛的例子:
[code]
package idv.jacky.ironman4.day24;
public class Day24Example2 {
public static void main(String[] args) {
try {
foo();
} catch (Exception e) {
System.out.println(e);
System.out.println("Suppressed Exceptions: ");
Throwable[] th = e.getSuppressed();
for(Throwable t : th)
System.out.println(t);
}
}
public static void foo() throws Exception {
try (MyResource3 r3 = new MyResource3()) {
System.out.println("Do something...");
throw new Exception("Something error!");
}
}
}
[/code]
我們在 main 方法裡補捉到例外後(第21行丟出來的),呼叫 getSuppressed 方法來取得被壓制的例外,也就是 MyResource3 裡第8行所丟出來的。getSuppressed 會回傳一個 Throwable 物件陣列,因為 try-with-resource 陳述式裡可以有多個可關閉物件的宣告。然後我們再用個 for 迴圈把這裡被壓制的例外也印出來(本範例中只有一個),程式執行結果如下:

現在你知道 MyResource3.close 方法丟出的例外跑哪去了吧!
04 十一月, 2011 23:33
在 Java SE 7 裡,還有另一個繼承 java.lang.AutoCloseable 的介面,所有 java.io package 裡的資料串流類別其實是實作這個介面,反而不是直接實作 java.lang.AutoCloseable 介面。這個介面就是 java.io.Closeable!
就兩個介面的宣告上來說,幾乎沒什麼差別,唯一的差別就是 java.io.Closeable 的 close 方法宣告會丟出 IOException 例外,而 java.lang.AutoCloseable 的 close 方法宣告會丟出 Exception 例外。當然每個資料串流類別實作 close 方法的細節不同,介面的好處就是,不管底層類別如何實作,介面的方法呼叫還是一樣。
我們再來設計一個實作 java.io.Closeable 介面的類別:
[code]
package idv.jacky.ironman4.day23;
import java.io.Closeable;import java.io.IOException;
public class MyResource2 implements Closeable{
public void close() throws IOException { System.out.println("Close resource 2."); }
}
[/code]
MyResource2 類別跟昨天的 MyResource1 類別差不多,只差實作的介面不同。
有時因為程式的需要,我們可以在 try-with-resource 陳述式裡,宣告多個可關閉的物件嗎?答案當然是可以的,你就把 try-with-resource 的小括號當成一般的程式碼區塊,把要宣告的可關閉物件寫在裡面,每個物件的宣告用分號(;)隔開,例如:
[code]
package idv.jacky.ironman4.day23;
import idv.jacky.ironman4.day22.MyResource1;
public class Day23Example {
public static void main (String[] args) throws Exception {
try (MyResource1 r1 = new MyResource1();
MyResource2 r2 = new MyResource2()) {
System.out.println("Do something...");
}
}
}
[/code]
現在要考考大家,這個範例程式會印出三個字串,請問順序會如何?毫無疑問的,"Do something…" 一定會是第一個,那接下來呢? MyResource1 和 MyResource2 哪個物件會先被關閉呢?規則是,後產生的物件會先被關閉,所以執行的結果會是:

為什麼會後宣告的先關閉呢?因為有可能你會在後宣告的物件裡,使用前面宣告的物件,例如:
[code]
package idv.jacky.ironman4.day23;
import java.io.BufferedReader;
import java.io.FileReader;
public class Day23Example2 {
public static void main(String[] args) throws Exception {
try (FileReader f = new FileReader("c:\\temp.txt");
BufferedReader b = new BufferedReader(f);) {
String line = b.readLine();
}
}
}
[/code]
如果我們先關閉了第一個物件,有可能會讓第二個物件無法順利操作!所以在使用 tr-with-resource 陳述式時,請記得 [b]後開先關/b] 的原則!
03 十一月, 2011 21:21
那 Java 是如何知道/判斷,在 try-with-resource 陳述式裡的物件是可以關閉(close)的呢?
在 Java SE 7 裡有一個新的介面(Interface)叫 java.lang.AutoCloseable,裡面只定義了一個 close 的方法,所以實作這個介面的類別,其所生成的物件,都可以放在 try-with-resource 陳述式裡執行。所以我們來自己寫一個實作 java.lang.AutoCloseable 的類別:
[code]
package idv.jacky.ironman4.day22;
public class MyResource1 implements AutoCloseable{
@Override
public void close() throws Exception {
System.out.println("Close resource 1.");
}
}
[/code]
我們把實作 java.lang.AutoCloseable 的類別命名為 MyResource1,然後實作 close 這個方法,裡面只叫 Java 印出 "Close resource 1" 這個字串。接著我們來看看怎麼使用它:
[code]
package idv.jacky.ironman4.day22;
public class Day22Example1 {
public static void main (String[] args) throws Exception {
try (MyResource1 r1 = new MyResource1()) {
System.out.println("Do something...");
}
}
}
[/code]
就像昨天的範例一樣,我們在 try-with-resource 陳述式裡宣告了一個 MyResource1 的物件 r1,然後再 try 區塊裡印出 "Do something…" 的字串。然後下面就是程式執行的結果:

Java 會先執行第7行,印出 "Do something…"後,離開 try 區塊時會去關閉實作 java.lang.AutoCloseable 的 MyResource1 物件,而在 MyResource1.close 的方法裡我們請 Java 印出 "Close resource 1"。因為我們沒有 catch 區塊來補捉 close 方法可能丟出來的例外,所以我們要在 main 方法的宣告上多宣告會丟出 Exception 例外!
這樣是不是有點初步的概念了呢!
02 十一月, 2011 23:53
昨天的範例程式裡,我們得多做一些事情才能正確地關閉資料串流,像是在 try-catch 區塊外先宣告物件變數,然後在 final 區塊裡得先檢查物件變數是不是 null 等。Java SE 7 裡提供了一個簡便的新陳述式,來簡化這些事。
新的陳述式就叫做 try-with-resource 。簡單的說,就是把宣告資料串流物件這樣的程式碼,直接在在 try 的陳述式裡。我們直接看範例比較快:
[code]
[/code]
你可以看到程式碼第10行,原本 try 陳述式就只有一個 try 加上左大括號,現在變得跟方法一樣,多個小括號,然後把需要關閉的資料串流物件變數宣裡裡面,然後在 try 區塊裡,我們一樣能自由地使用 br 物件,也不用多個 final 區塊來關閉它,當程式離開 try-catch 區塊時,會自動把宣告在 try-with-resource 陳述式裡的資料串流物件給關閉!
Java 只是幫你呼叫 close 方法,雖然你沒看到也沒寫,但 close 方法會丟出的 IOException 一樣得處理。不過我們已經有 catch 了,close 和 readLine 方法都是在 catch 之前呼叫的,所以會一起被 catch 下來。如果你省略了 catch, 那就得在方法宣告上多宣告會丟出 IOException 喔!
[code]
[/code]
還記得第15天的 範例 嗎?那個範例其實不夠完整!
在那個範例程式裡,我們開啟了在 C 磁碟根目錄下的 temp.txt 檔案(第12行),然後程式讀取一行(第13行)就結束了。我們少做了一件事,那就是把檔案給關閉!雖然程式很短,沒有關閉似乎也不影響什麼,但當你的程式愈寫愈大時,可能會存取某些案上千上萬次,若是忽略了這個小小的動作,輕則造成程式當機,重則保貴的資料毀損,那可就得不嘗失了~
所以好習慣的養成,就是靠著每個小細節,那該怎麼正確地關閉程式中開敵的檔案呢?請看下面這個例子:
[code]
[/code]
上面的例子,我們做得完整一點,程式同樣開啟 C 磁碟根目錄下的 temp.txt 檔案,接著透過一個 while 迴圈,將案的內容一行讀取出來後印出在螢幕上。因為產生 FileReader 物件和 BufferedReader.readLine 方法可能會丟出 IOException,所以我們必需要 catch 這個例外。而關閉檔案的程式碼我們就寫在 final 這個區塊裡,之所以要寫在 final 區塊裡主要的原因是,除了程式突然中斷執行外,在 main 方法結束前,一定會執行 final 區塊裡的程式碼!這樣我們就不怕不會沒有執行到關閉檔案的程式碼。
我們將 BufferedReader 物件變數宣告 try-catch 區塊之前,原因是如果宣告在 try 區塊內的話,在離開 try 區塊之後,這個變數就失效了,那麼 final 區塊就沒辦法使用 br 變數了!在 final 區塊裡,我們得先確認一下 br 變數是不是 null,因為我們在第10行宣告時,並沒有對它做初始化的動作,程式有可能在第12行產生 BufferedReader 變數時出錯,程式會直接跳到第17行執行後,接著執行 final 區塊。如果我們沒有判斷而直接執行第20行程式碼的話,程式將會丟出 NullPointerException。
程式第20行呼叫的 close 方法,就是關閉 BufferedReader 物件資料流的方法,它也會順便幫你把 FileReader 物件資料流給關閉。所有在 java.io 這個 package 下的 Stream 和 Reader 還有 Writer 類別等,都有提供 close 這個方法,讓你來關閉相關的資料流。
這樣就好了嗎?close 方法也會有可能丟出 IOException 例外,所以怎麼辦呢?一種是再 final 區塊裡,再用一個 try-catch 來包住 close 程式碼;或是我們想交給呼叫這個方法的人來處理,那就在方法的宣告上,多宣告這個方法會丟出 IOException 例外(第9行)。這樣整個範例就完整了!
31 十月, 2011 21:58
Java SE 7 又提供了這個偷懶的方法,那使用上有什麼例外或限制嗎?
在 Java 裡,所有的數字類別(Integer, Long, Float, Double 等),都是繼承 Number 類別,還記得前兩天的 Constants 類別嗎?我們想說讓 Constant 類別更有彈性一點,所以寫了下面這樣的程式碼:
[code]
package idv.jacky.ironman4;
import idv.jacky.ironman4.day17.Constant;
public class Day19Example {
public static void main(String[] args) {
Constant<Double> pi = new Constant<>(3.14);
Constant<Number> c = new Constant<>(3.14);
}
}
[/code]
既然 Double 是一種 Number,那程式碼第10行應該沒問題吧?不過,在編譯時,我們遇到這樣的錯誤訊息:
[b]
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Type mismatch: cannot convert from Constant<Double> to Constant<Number>
at idv.jacky.ironman4.Day19Example.main(Day19Example.java:10)
[/b]
怎麼會!根據 Java 多型的特性,Double 應該可以轉型成 Number 物件啊!例如:
[code]
Number n = new Double(3.14);
[/code]
很不幸地,泛型就是要你指定確定的單一型別,所以像 Constant<Double> 這種複合型別是沒辦法自動轉型的,因此我們還是得乖乖地指定確定的型別才行。
30 十月, 2011 15:21
那泛型在 Java SE 7裡有什麼樣新的功能呢?
泛型很好用,Java 也強迫你在使用 Collection 類別時,一定要用泛型來指定型別,不然編譯時會出現警告訊息。但泛型用習慣了,每次都要輸入長長的型別宣告,例如
[code]
Map<String, List<Integer>> numbers = new HashMap<String, List<Integer>>();
[/code]
我在寫程式時,還真的常常用到這樣落落長的泛型宣告,Java難到不能聰明一點,看到前面已經宣告過了,後面就直接省略不就好了。是的!Java SE 7裡就提供了這樣的省略功能,剛剛的那行程式碼就可以簡略成:
[code]
Map<String, List<Integer>> numbers = new HashMap<>();
[/code]
是的,後面的實體類別就直接用一個空的角刮號就搞定了。Java 編譯器會聰明地幫你把完整的程式碼補上。
昨天的程式碼我們也可以這樣改造一下:
[code]
package idv.jacky.ironman4;
import idv.jacky.ironman4.day16.Apple;
import java.util.ArrayList;
import java.util.List;
public class Day18Example {
public static void main(String[] args) {
List<Apple> fruits = new ArrayList<>();
fruits.add(new Apple());
fruits.add(new Apple());
makeJuice(fruits);
}
public static void makeJuice(List<Apple> fruits) {
for(Apple a : fruits) {
a.makeJuice();
}
}
}
[/code]
真的是省了一些事...
上一篇文章,我們復習了泛型最基本的應用 - Collection 類別的型別指定。除此之外,泛型還能用在其它的地方。
例如我們宣告了一個常數類別 Constant:
[code]
package idv.jacky.ironman4.day17;
public class Constant<T> {
private T t;
public Constant(T t) {
this.t = t;
}
}
[/code]
你可以看到熟悉地角刮號,但這次是出現在類別宣告的類別名稱後方。我們不指定特定的型別給它,所以用個 T 代替。然後我們有個 private 的變數,是個 T 型別的 t,在 Constant 建構子裡,傳入了一個 t ,指定給 Constant 類別自己的 t。
有聽沒有懂?我們來看看使用的例子:
[code]
package idv.jacky.ironman4.day17;
public class Day17Example {
public static void main(String[] args) {
Constant<Integer> life = new Constant<>(42);
Constant<Double> pi = new Constant<>(3.14);
}
}
[/code]
程式的第6行,我們宣告了一個 Constant 物件,名叫 life,然後指定它為 Integer 型別,並設定它的值為 42。
程式的第7行,我們宣告了另一個 Constant物件,名叫 pi,然後指定它為 Double 型別,並設定它的值為 3.14。
這個例子雖然沒有什麼實質上的應用,但它告訴我們可以透過泛型,來讓一個類別能處理不同的型別物件。
這是泛型的第二種基本應用 - 擴展。讓類別能更有彈性地容納不同的型別。
在講下一個新的功能之前,我們先來復習一下什麼是泛型(Generic)。
在 Java SE 1.5 版之前,還沒有納入泛型的語法,沒有泛型有什麼壞處呢?我們拿最常用的 Collection 類別來看看:
[code]
package idv.jacky.ironman4.day16;
import java.util.ArrayList;
import java.util.List;
public class Day16Example1 {
public static void main(String[] args) {
List fruits = new ArrayList();
fruits.add(new Apple());
fruits.add(new Banana());
fruits.add(new Apple());
makeJuice(fruits);
}
public static void makeJuice(List fruits) {
for(Object obj : fruits) {
Apple a = (Apple)obj;
a.makeJuice();
}
}
}
[/code]
上面的程式碼裡,我們宣告了一個 fruits 的 List 物件(用ArrayList 為實體),然後依序放入了 Apple,Banana 和 Apple 這三個物件。Apple 和 Banana 的程式碼如下:
[code]
package idv.jacky.ironman4.day16;
public class Apple {
public void makeJuice() {
}
}
[/code]
[code]
package idv.jacky.ironman4.day16;
public class Banana {
}
[/code]
Apple 跟 Banana 兩個類別最大的不同在於,Apple 類別裡有個 makeJuice 的方法,而 Banana 類別裡沒有(應該沒有人打香蕉汁來喝吧??)。回到最上面的程式碼,有個叫 makeJuice 的方法,所傳入的參數是個 List 物件;在方法裡我們會把 List 物件裡的物件一個個拿出來,轉成 Apple 物件後呼叫物件自己的 makeJuice 方法。
整個程式可以正常的編譯,但在執行時就出了問題了,錯誤訊息是
Exception in thread "main" java.lang.ClassCastException: idv.jacky.ironman4.day16.Banana cannot be cast to idv.jacky.ironman4.day16.Apple
at idv.jacky.ironman4.day16.Day16Example1.makeJuice(Day16Example1.java:19)
at idv.jacky.ironman4.day16.Day16Example1.main(Day16Example1.java:14)
是的,當程式試著將一個 Banana 物件 強迫轉型成 Apple 物件時,JVM 就會丟出 ClassCaseExcetpion!因為香蕉不可能變成蘋果!
Java 是一種 強型別程式語言(Stong Typing Programming Language),簡單的說就是,Java 程式碼在編譯時期,就會檢查各個宣告的變數,是否已指定了正確的型態。不過像這個例子,Java 編譯器確束手無策,我們明明知道,程式第19行有潛在的危險,但確還是讓程式碼能成功編譯。一直到執行時期,才丟出例外,但為時已晚。
基於這類的原因,Java SE 1.5 版終於在大家的期盼下加入了泛型這個功能!於是我們改造了一下這個範例
[code]
package idv.jacky.ironman4.day16;
import java.util.ArrayList;
import java.util.List;
public class Day16Example2 {
public static void main(String[] args) {
List<Apple> fruits = new ArrayList<Apple>();
fruits.add(new Apple());
fruits.add(new Banana());
fruits.add(new Apple());
makeJuice(fruits);
}
public static void makeJuice(List<Apple> fruits) {
for(Apple a : fruits) {
a.makeJuice();
}
}
}
[/code]
泛型的使用方式很簡單,就是在 Collection 類別後加上角刮號(大於、小於),然後把型別指定進去。以程式第9行來說,就是宣告一個只能放 Apple 物件的 List 物件。既然宣告只能放 Apple 物件,那原本的第11行在這裡就會出錯,程式會沒辦法成功地編譯,編譯時會告訴你錯誤和原因:
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The method add(Apple) in the type List<Apple> is not applicable for the arguments (Banana)
at idv.jacky.ironman4.day16.Day16Example2.main(Day16Example2.java:11)
這樣一來,我們在寫程式的過程式,就會修正這個錯誤,而剛剛執行時期的錯誤也就不會發生,讓程式能更穩定的執行!
這是泛型的第一種基本應用 - 限制,限制你不能將不同的型別,指定給某種類別,例如 Collection。
27 十月, 2011 20:42
我在有時在撰寫 Java 程式時,會設計一些 API (Application Programming Interface) 讓別人來呼叫使用,你的 API 會產生一些例外需要呼叫的人來處理時,你就會在方法的宣告上使用 throws 這個關鍵字,意思就是丟出例外。
我們直接來看下面的例子:
[code]
package idv.jacky.ironman4;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Day15Example {
public void foo() throws IOException, FileNotFoundException {
try {
BufferedReader br = new BufferedReader(new FileReader("c:\\temp.txt"));
String line = br.readLine();
} catch (Exception e) {
throw e;
}
}
}
[/code]
上面的程式碼裡,在第12行我們建立了一個 FileReader 的物件,要程式去開啟 c:\\temp.txt 這個檔案。在產生 FileReader 物件時,若無法找到指定的案,會丟出 FileNotFoundException。接著我們將產生好的 FileReader 物件傳入 BufferedReader 建構子,來產生 BufferedReader 的物件。第13行,我們使用 BufferedReader 物件所提供的 readLine 方法,來讀取檔案中的第一行資料。這個方法如果讀取失敗的話,會丟出 IOException。
因為第12、13行都有可能會丟出例外,所以我們在第14行用 catch 來處理它,因為懶得寫那麼多個 catch,所以就直接 catch Exception。可是我們希望呼叫這個方法的人,能進一步地處理這兩個例外,所以我們在 catch 區塊裡什麼也沒做,就直接又把例外丟出來。
如果我們要把例外丟出你的方法外的話,需要在方法的宣告上,明確的指定你這個方法會丟出什麼例外。所以我們就在第10行的方法宣告上宣告說,我會丟出 IOException 和 FileNotFoundException。
不幸地,這個例子在 Java SE 7 版之前是沒有辦法編譯的!因為我們在第14行已經透過多型轉型的方式,將 IOException 和 FileNotFoundException 轉成 Exception 型別了, 最後在第15行丟出來的是 Exception 型別,而不是 IOException 和 FileNotFoundException 型別。
所以在 Java SE 7 版之前,我們只有兩種方式來解決這個問題:
1. 在方法的宣告上,直接宣告丟出 Exception
2. 多重 catch 分別來處理
Java SE 7知道我們想要偷懶一點,所以剛剛這個例子直接拿去給 Java SE 7 編譯是會沒有錯誤地!也就是說 Java SE 7 的編譯器會很聰明地去檢查程式碼是不是只會丟出 IOException 和 FileNotFoundException 這兩種例外,所以你就可以偷懶囉~~
推文( 0 )

