Java 1.5新特性簡介
“JDK1.5”的一個重要主題就是通過新增一些特性來簡化開發,這些特性包括泛型,for-each 迴圈,自動裝包/拆包,列舉,可變引數, 靜態匯入 。使用這些特性有助於我們編寫更加清晰,精悍,安全的程式碼。
一. 首先簡單介紹一下各種特性及其使用
1.泛型(Generic)
C++通過模板技術可以指定集合的元素型別,而Java在1.5之前一直沒有相對應的功能。一個集合可以放任何型別的物件,相應地從集合裡面拿物件的時候我們也不得不對他們進行強制得型別轉換。猛虎引入了泛型,它允許指定集合裡元素的型別,這樣你可以得到強型別在編譯時刻進行型別檢查的好處。
“JDK1.5”的一個重要主題就是通過新增一些特性來簡化開發,這些特性包括泛型,for-each 迴圈,自動裝包/拆包,列舉,可變引數, 靜態匯入 。使用這些特性有助於我們編寫更加清晰,精悍,安全的程式碼。
一. 首先簡單介紹一下各種特性及其使用
1.泛型(Generic)
C++通過模板技術可以指定集合的元素型別,而Java在1.5之前一直沒有相對應的功能。一個集合可以放任何型別的物件,相應地從集合裡面拿物件的時候我們也不得不對他們進行強制得型別轉換。猛虎引入了泛型,它允許指定集合裡元素的型別,這樣你可以得到強型別在編譯時刻進行型別檢查的好處。
1 Collection<String> c = new ArrayList();
2 c.add(new Date());
編譯器會給出一個錯誤:
add(java.lang.String) in java.util.Collection<java
2.For-Each迴圈
For-Each迴圈得加入簡化了集合的遍歷。假設我們要遍歷一個集合對其中的元素進行一些處理。典型的程式碼為:
1 void processAll(Collection c){
2 for(Iterator i=c.iterator(); i.hasNext();){
3 MyClass myObject = (MyClass)i.next();
4 myObject.process();
5 }
6 }
使用For-Each迴圈,我們可以把程式碼改寫成:
1 void processAll(Collection<MyClass> c){
2 for (MyClass myObject :c)
3 myObject.process();
4 }
這段程式碼要比上面清晰許多,並且避免了強制型別轉換。
3.自動裝包/拆包(Autoboxing/unboxing)
自動裝包/拆包大大方便了基本型別資料和它們包裝類地使用。
自動裝包:基本型別自動轉為包裝類.(int >> Integer)
自動拆包:包裝類自動轉為基本型別.(Integer >> int)
在JDK1.5之前,我們總是對集合不能存放基本型別而耿耿於懷,現在自動轉換機制解決了我們的問題。
1 int a = 3;
2 Collection c = new ArrayList();
3c.add(a);//自動轉換成Integer.
4 Integer b = new Integer(2);
5c.add(b + 2);
這裡Integer先自動轉換為int進行加法運算,然後int再次轉換為Integer.
4.列舉(Enums)
JDK1.5加入了一個全新型別的“類”-列舉型別。為此JDK1.5引入了一個新關鍵字enmu. 我們可以這樣來定義一個列舉型別。
1 public enum Color
2 {
3 Red,
4 White,
5 Blue
6 }
然後可以這樣來使用Color myColor = Color.Red.
列舉型別還提供了兩個有用的靜態方法values()和valueOf(). 我們可以很方便地使用它們,例如
1 for (Color c : Color.values())
2 System.out.println(c);
5.可變引數(Varargs)
可變引數使程式設計師可以宣告一個接受可變數目引數的方法。注意,可變引數必須是函式宣告中的最後一個引數。假設我們要寫一個簡單的方法列印一些物件,
util.write(obj1);
util.write(obj1,obj2);
util.write(obj1,obj2,obj3);
…
在JDK1.5之前,我們可以用過載來實現,但是這樣就需要寫很多的過載函式,顯得不是很有效。如果使用可變引數的話我們只需要一個函式就行了
1 public void write(Object... objs) {
2 for (Object obj: objs)
3 System.out.println(obj);
4 }
在引入可變引數以後,Java的
反射包也更加方便使用了。對於c.getMethod("test", new
Object[0]).invoke(c.newInstance(), new
Object[0])),現在我們可以這樣寫了c.getMethod("test").invoke(c.newInstance()),這樣的程式碼比
原來清楚了很多。
6.靜態匯入(Static Imports)
要使用用靜態成員(方法和變數)我們必須給出提供這個方法的類。使用靜態匯入可以使被匯入類的所有靜態變數和靜態方法在當前類直接可見,使用這些靜態成員無需再給出他們的類名。
import static java.lang.Math.*;
…….
r = sin(PI * 2); //無需再寫r = Math.sin(Math.PI);
不過,過度使用這個特性也會一定程度上降低程式碼地可讀性。
二. 重點講一下泛型
在已釋出的Java1.4中在核心程式碼庫中增加了許多新的API(如Loging,正則表示式,NIO)等,在最新發布的JDK1.5和即將釋出的JDK1.6中也新增了許多API,其中比較有重大意義的就是Generics(範型)。
1.什麼是Generics?
Generics可以稱之為引數型別(parameterized types),由編譯器來驗證從客戶端將一種型別傳送給某一物件的機制。如Java.util.ArrayList,編譯器可以用Generics來保證型別安全。
在我們深入瞭解Generics之前,我們先來看一看當前的java集合框架(Collection)。在j2SE1.4中所有集合的Root Interface是Collection
Collections example without genericity: Example 1
1 protected void collectionsExample() {
2 ArrayList list = new ArrayList();
3 list.add(new String("test string"));
4 list.add(new Integer(9)); // purposely placed here to create a runtime ClassCastException
5 inspectCollection(list);
6 }
7
8
9 protected void inspectCollection(Collection aCollection) {
10 Iterator i = aCollection.iterator();
11 while (i.hasNext()) {
12 String element = (String) i.next();
13 }
14 }
以上的樣例程式包含的兩個方法,collectionExample方法建立了一個簡單的集合型別ArrayList,並在ArrayList中增加了 一個String和一個Integer物件.而在inspecCollection方法中,我們迭代這個ArrayList用String進行Cast。 我們看第二個方法,就出現了一個問題,Collection在內部用的是Object,而我們要取出Collection中的物件時,需要進行Cast, 那麼開發者必需用實際的型別進行Cast,像這種向下造型,編譯器無
法進行檢查,如此一來我們就要冒在程式碼在執行丟擲ClassCastException的危險。我們看inspecCollection方法,編譯時沒有問題,但在執行時就會丟擲ClassCastException異常。所以我們一定要遠離這個重大的執行時錯誤
2.使用Generics
從上一章節中的CassCastException這種異常,我們期望在程式碼編譯時就能夠捕捉到,下面我們使用範型修改上一章的樣例程式。
//Example 2
1 protected void collectionsExample() {
2 ArrayList<String> list = new ArrayList<String>();
3 list.add(new String("test string"));
4 // list.add(new Integer(9)); this no longer compiles
5 inspectCollection(list);
6 }
7
8
9 protected void inspectCollection(Collection<String> aCollection) {
10 Iterator<String> i = aCollection.iterator();
11 while(i.hasNext()) {
12 String element = i.next();
13 }
14 }
從上面第2行我們在建立ArrayList時使用了新語法,在JDK1.5中所有的Collection都加入了Generics的宣告。例:
//Example 3
1 public class ArrayList<E> extends AbstractList<E> {
2 // details omitted...
3 public void add(E element) {
4 // details omitted
5 }
6 public Iterator<E> iterator() {
7 // details omitted
8 }
9 }
這個E是一個型別變數,並沒有對它進行具體型別的定義,它只是在定義ArrayList時的型別佔位符,在Example 2中的我們在定義ArrayList的例項時用String繫結在E上,當我們用add(E element)方法向ArrayList中增加物件時,那麼就像下面的寫法一樣: public void add(String element);因為在ArrayList所有方法都會用String來替代E,無論是方法的引數還是返回值。這時我們在看Example 2中的第四行,編譯就會反映出編譯錯誤。
所以在java中增加Generics主要的目的是為了增加型別安全。
通過上面的簡單的例子我們看到使用Generics的好處有:
· 1.在型別沒有變化時,Collection是型別安全的。
· 2.內在的型別轉換優於在外部的人工造型。
· 3.使Java介面更加強壯,因為它增加了型別。
· 4.型別的匹配錯誤在編譯階段就可以捕捉到,而不是在程式碼執行時。
受約束型別變數
雖然許多Class被設計成Generics,但型別變數可以是受限的
public class C1<T extends Number> { }
public class C2<T extends Person & Comparable> { }
第一個T變數必須繼承Number,第二個T必須繼承Person和實現Comparable
3.Generics方法
像Generics類一樣,方法和建構函式也可以有型別引數。方法的引數的返回值都可以有型別引數,進行Generics。
//Example 4
1 public <T extends Comparable> T max(T t1, T t2) {
2 if (t1.compareTo(t2) > 0)
3 return t1;
4 else return t2;
5 }
這裡,max方法的引數型別為單一的T型別,而T型別繼承了Comparable,max的引數和返回值都有相同的超類。下面的Example 5顯示了max方法的幾個約束。
//Example 5
1 Integer iresult = max(new Integer(100), new Integer(200));
2 String sresult = max("AA", "BB");
3 Number nresult = max(new Integer(100), "AAA"); // does not compile
在Example 5第1行引數都為Integer,所以返回值也是Integer,注意返回值沒有進行造型。
在Example 5第2行引數都為String,所以返回值也是String,注意返回值沒有進行造型。以上都呼叫了同一個方法。
在Example 5第3行產生以下編譯錯誤:
Example.java:10: incompatible types
found : java.lang.Object&java.io.Serializable&java.lang.Comparable<?>
required: java.lang.Number
Number nresult = max(new Integer(100), "AAA");
這個錯誤發生是因為編譯器無法確定返回值型別,因為String和Integer都有相同的超類Object,注意就算我們修正了第三行,這行程式碼在執行仍然會報錯,因為比較了不同的物件。
3.通配(Wildcards)
先看以下兩行程式碼是否合法:
List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2
第一行沒問題, 關鍵在第二行程式碼, 大多數人會認為, "一個String的List自然更是一個Object的List", 因此, 第2行沒問題.
好, 接著看以下程式碼:
lo.add(new Object()); // 3
String s = ls.get(0); // 4: 試圖將一個Object賦給一個String!
可見, 通過別名lo, 我們能對ls, 一個String的列表, 進行資料操作(特別是插入一個Object), 從而導致ls不僅僅是容納了String物件! 這是Java編譯器不容許的! 編譯時, 第2行會報告一個編譯錯誤的.
通常, 若Foo是Bar的一個子型別(子類或子介面), G是某個泛型宣告, 則G<Foo>並不是G<Bar>的一個子型別.
假定要輸出一個集合中的所有元素. 以下分別是舊版本及新版本(JDK 1.5)中的寫法:
void printCollection(Collection c) {
Iterator i = c.iterator();
for( k = 0; k < c.size(); k++) {
System.out.println( i.next() );
}}
void printCollection(Collection<Object> c) {
for(Object e : c) {
System.out.println(e);
}}
問 題在於, 新版本反而不如舊版本更有用些. 因為舊版本能使用各種型別的集合作為引數, 但新版本則只能使用Collection<Object>. 而正如上節看到的, Collection<Object>並不是其它各種集合的超型別(父型別).
所有集合的超型別應該寫作: Collection<?>, 讀作: collection of unknown(未知集合), 即一個集合, 其元素型別可以與任何型別相匹配. 因此稱這種型別為"通配型別".
正確實現上述舊版本的程式碼可以這麼寫:
void printCollection(Collection<?> c) {
for(Object e : c) {
System.out.println(e);
}}
這時, 可以用任意型別的集合來呼叫此方法. 注意在方法體中, 仍然從 c 中讀入元素並賦給了Object, 這是沒有錯誤的, 因此不論型別實參是何種集合, 它的元素都是object. 然而, 如果任意給它增加一個object則是不安全的:
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 編譯時的錯誤
由 於我們不知道c的元素型別是什麼, 所以不能給它增加一個object. 方法add()接受一個型別E的引數, 而E與集合的元素型別相同. 當型別實參是?時, 它表示"未知的型別", 我們傳遞給add的引數必須是這個"未知型別"的子型別. 不幸的是, 既然型別未知, 也就無法決定其子型別, 於是什麼也不能作為其引數. 唯一的例外是null, 因為null是所有型別的一個成員.
另一方面, 如果給了一個List<?>, 我們可以呼叫get()方法並使用其返回的元素. 雖然返回的元素型別是"未知型別", 但它總歸是一個object, 因此將get()返回的元素賦給一個Object型別的變數, 或將其傳遞給一個可接受Object的引數都是安全的.Java2標準版(Java 2 Platform, Standard Edition, J2SE)1.5即將正式推出,這一次的版本更新不同於以往,它帶來了很多里程碑式的革新,SUN將其綽號取名為“虎”。這一次的變革將是Java誕生以 來從未有過的,它給我們帶來了耳目一新的感覺。下面我們就來欣賞一下其中的部分典型變化:
1.自動包裝和解包(Autoboxing and unboxing)
程式碼示例
往一個ArrayList中加入一個整數,1.5版本以前的版本寫法是:
List list = new ArrayList();
list.add( new Integer( 10 ) );
而在1.5版本中可以寫為:
list.add( 10 );
因為,在1.5版本中,對一個整數進行包裝,使之成為一個Integer物件(即包裝,boxing),然後加入到一個ArrayList中的做法被認 為是沒有必要的,反之,解包(unboxing)的做法也是沒有必要的,這樣的程式碼只是增加了程式的文字長度而已,所以1.5版本支援了自動包裝和解包操 作,對於bool/Boolean,byte/Byte,double/Double,short/Short,int/Integer,long /Long,float/Float的相應包裝/解包操作都進行了支援,從而使程式碼變得簡單。
2.更優化的迴圈語句(The inhanced for loop)
程式碼示例
一個典型的遍歷陣列的迴圈語句,1.5版本以前的寫法是:
for ( Iterator iterator = list.iterator(); iterator.hasNext(); )
{
Integer n = (Integer)iterator.next();
...
}//for
而在1.5版本中可以寫為:
for ( Integer n : list )
{
...
}//for
顯然1.5版本的寫法比以前是大大簡化了,但是在需要修改集合,比如刪除其中元素時不能採用這種寫法。之所以Java1.5版本沒有象C#那樣乾脆定義一個foreach關鍵詞,主要是因為SUN認為增加一個專門的關鍵詞成本太高了(too costly)。但1.4版本中就曾經增加了assert關鍵詞,1.5版本中也新增加了enum關鍵詞,因此這一解釋恐怕並不那麼令人信服。
3.引數可變的方法和printf
程式碼示例
當不能確定一個方法的入口引數的個數時,以往版本的Java中,通常的做法是將多個引數放在一個數組或者物件集合中作為引數來傳遞,1.5版本以前的寫法是:
int sum(Integer[] numbers)
{
int nSum = 0;
for(int i: numbers)
nSum += i;
return nSum;
}
...
//在別處呼叫該方法
sum(new Integer[] {12,13,20});
而在1.5版本中可以寫為:
int sum(Integer... numbers)
{
int nSum = 0;
for(int i: numbers)
nSum += i;
return nSum;
}
...
//在別處呼叫該方法
sum(12,13,20);
顯然,1.5版本的寫法更為簡易,也更為直觀,尤其是方法的呼叫語句,不僅簡化很多,而且更符合通常的思維方式,更易於理解。
1.5版本自身就有一個應用該特徵的典型例子,即C風格的格式化輸出方法——printf。
程式碼示例
輸出一個加法算式,1.5版本以前的寫法是:
int x = 5;
int y = 7;
int nSum = x + y;
System.out.println(x + " + " + y + " = " + nSum);
而在1.5版本中可以寫為:
System.out.printf("%d + %d = %d/n", x, y, nSum);
以上兩種寫法的輸出結構是一樣的,即“5 + 7 = 12”。
這種改變不僅僅是形式上的,printf還可以提供更為靈活、強大的輸出功能,比如限定按照兩位整數的形式輸出,可以寫為 “System.out.printf("%02d + %02d = %02d/n", x, y, nSum);”,輸出結果將是“05 + 07 = 12”。
4.列舉
程式碼示例
構建一個表示色彩的列舉,並賦值,在1.5版本中可以寫為:
public enum MyColor{ Red, Yellow, Blue }
MyColor color = MyColor.Red;
for ( MyColor mycolor : MyColor.values() )
System.out.println( mycolor );
以往的Java版本中沒有enum關鍵詞,1.5版本中終於加入了進來,這確實是一個令人高興的改進。此外,enum還提供了一個名為values() 的靜態方法,用以返回列舉的所有值的集合。所以,以上程式的輸出結果是把“Red”、“Yellow”、“Blue”分行輸出。
而enum提供的靜態方法valueOf()則將以字串的形式返回某一個具體列舉元素的值,比如“MyColor.valueOf(“Red”)”會返 回“Color.Red”。靜態方法name()則返回某一個具體列舉元素的名字,比如“MyColor.Red.name()”會返回“Red”。類似 的方法還有不少。此外,enum自身還可以有構造方法。
5.靜態引用
程式碼示例
當我們要獲取一個隨即數時,1.5版本以前的寫法是:
import java.lang.Math; //程式開頭處
...
double x = Math.random();
而在1.5版本中可以寫為:
import static java.lang.Math.random; //程式開頭處
…
double x = random();
靜態引用使我們可以象呼叫本地方法一樣呼叫一個引入的方法,當我們需要引入同一個類的多個方法時,只需寫為“import static java.lang.Math.*”即可。這樣的引用方式對於列舉也同樣有效。
6.總結
以上對J2SE1.5的部分新特徵做了一些簡單的介紹。總而言之,1.5版本的Java確實給我們帶來了很多令人激動的變革,如同以上介紹的那樣,很多功能以前的版本也能實現,但是不能實現得這樣簡單、漂亮。相信這次變革會給Java帶來更多的追隨者。