轉>java5.0新特性

來源:互聯網
上載者:User
Java 5.0發布了,許多人都將開始使用這個JDK版本的一些新增特性。從增強for迴圈到諸如泛型(generic)之類更複雜的特性,都將很快出現在您所編寫的代碼中。我們剛剛完成了一個基於Java 5.0的大型任務,而本文就是要介紹我們使用這些新特性的體驗。本文不是一篇入門性的文章,而是對這些特性以及它們所產生的影響的深入介紹,同時還給出了一些在項目中更有效地使用這些特性的技巧。
  簡介

  在JDK 1.5的beta階段,我們為BEA的Java IDE開發了一個Java 5編譯器。因為我們實現了許多新特性,所以人們開始以新的方式利用它們;有些用法很聰明,而有些用法明顯應該被列入禁用清單。編譯器本身使用了新的語言特性,所以我們也獲得了使用這些特性維護代碼的直接體驗。本文將介紹其中的許多特性和使用它們的體驗。

  我們假定您已經熟悉了這些新特性,所以不再全面介紹每個特性,而是談論一些有趣的、但很可能不太明顯的內容和用法。這些技巧出自我們的實際體驗,並大致按照語言特性進行了分類。

  我們將從最簡單的特性開始,逐步過渡到進階特性。泛型所包含的內容特別豐富,因此佔了本文一半的篇幅。

  增強for迴圈

  為了迭代集合和數組,增強for迴圈提供了一個簡單、相容的文法。有兩點值得一提:

  Init運算式

  在迴圈中,初始設定式只計算一次。這意味著您通常可以移除一個變數聲明。在這個例子中,我們必須建立一個整型數組來儲存computeNumbers()的結果,以防止每一次迴圈都重新計算該方法。您可以看到,下面的代碼要比上面的代碼整潔一些,並且沒有泄露變數numbers:

  未增強For:
  int sum = 0;
  Integer[] numbers = computeNumbers();
  for (int i=0; i < numbers.length ; i++)
sum += numbers[i];
   增強後的For:
   int sum = 0;

  for ( int number: computeNumbers() )
sum += number;  局限性

  有時需要在迭代期間訪問迭代器或下標,看起來增強for迴圈應該允許該操作,但事實上不是這樣,請看下面的例子:
for (int i=0; i < numbers.length ; i++) {
if (i != 0) System.out.print(",");
System.out.print(numbers[i]);
}  我們希望將數組中的值列印為一個用逗號分隔的清單。我們需要知道目前是否是第一項,以便確定是否應該列印逗號。使用增強for迴圈是無法獲知這種資訊的。我們需要自己保留一個下標或一個布爾值來指示是否經過了第一項。   這是另一個例子:

for (Iterator it = n.iterator() ; it.hasNext() ; )
if (it.next() < 0)
it.remove();  在此例中,我們想從整數集合中刪除負數項。為此,需要對迭代器調用一個方法,但是當使用增強for 迴圈時,迭代器對我們來說是看不到的。因此,我們只能使用Java 5之前版本的迭代方法。   順便說一下,這裡需要注意的是,由於Iterator是泛型,所以其聲明是Iterator。許多人都忘記了這一點而使用了Iterator的原始格式。

  注釋

  注釋處理是一個很大的話題。因為本文只關注核心的語言特性,所以我們不打算涵蓋它所有的可能形式和陷阱。  我們將討論內建的注釋(SuppressWarnings,Deprecated和Override)以及一般注釋處理的局限性。
  Suppress Warnings

  該注釋關閉了類或方法層級的編譯器警告。有時候您比編譯器更清楚地知道,代碼必須使用一個被否決的方法或執行一些無法靜態確定是否型別安全的動作,而使用:

@SuppressWarnings("deprecation")
public static void selfDestruct() {
Thread.currentThread().stop();
}  這可能是內建注釋最有用的地方。遺憾的是,1.5.0_04的javac不支援它。但是1.6支援它,並且Sun正在努力將其向後移植到1.5中。
Eclipse 3.1中支援該注釋,其他IDE也可能支援它。這允許您把代碼徹底地從警告中解脫出來。如果在編譯時間出現警告,可以確定是您剛剛把它添加進來——以協助查看那些可能不安全的代碼。隨著泛型的添加,它使用起來將更趁手。

  Deprecated

  遺憾的是,Deprecated沒那麼有用。它本來旨在替換@deprecated javadoc標籤,但是由於它不包含任何欄位,所以也就沒有方法來建議deprecated類或方法的使用者應該使用什麼做為替代品。大多數用法都同時需要javadoc標籤和這個注釋。 Override
  Override表示,它所注釋的方法應該重寫超類中具有相同簽名的方法:

@Override
public int hashCode() {
...
}  看上面的例子,如果沒有在hashCode中將“C”大寫,在編譯時間不會出現錯誤,但是在運行時將無法像期望的那樣調用該方法。通過添加Override標籤,編譯器會提示它是否真正地執行了重寫。

  在超類發生改變的情況中,這也很有協助。如果向該方法中添加一個新參數,而且方法本身也被重新命名了,那麼子類將突然不能編譯,因為它不再重寫超類的任何東西。

  其它注釋

  注釋在其他情境中非常有用。當不是直接修改行為而是增強行為時,特別是在添加樣板代碼的情況下,注釋在諸如EJB和Web services這樣的架構中運行得非常好。
注釋不能用做前置處理器。Sun的設計特別預防了完全因為注釋而修改類的位元組碼。這樣可以正確地理解該語言的成果,而且IDE之類的工具也可以執行深入的程式碼分析和重構之類的功能。

  注釋不是銀彈。第一次遇到的時候,人們試圖嘗試各種技巧。請看下面這個從別人那裡獲得的建議:

public class Foo {

@Property
private int bar;

}  其思想是為私人欄位bar自動建立getter和setter方法。遺憾的是,這個想法有兩個失敗之處:1)它不能運行,2)它使代碼難以閱讀和處理。   它是無法實現的,因為前面已經提到了,Sun特別阻止了對出現注釋的類進行修改。

  即使是可能的,它也不是一個好主意,因為它使代碼可讀性差。第一次看到這段代碼的人會不知道該注釋建立了方法。此外,如果將來您需要在這些方法內部執行一些操作,注釋也是沒用的。

  總之,不要試圖用注釋去做那些常規代碼可以完成的事情。

  枚舉

  enum非常像public static final int聲明,後者作為枚舉值已經使用了很多年。對int所做的最大也是最明顯的改進是型別安全——您不能錯誤地用枚舉的一種類型代替另一種類型,這一點和int不同,所有的int對編譯器來說都是一樣的。除去極少數例外的情況,通常都應該用enum執行個體替換全部的枚舉風格的int結構。

  枚舉提供了一些附加的特性。EnumMap和EnumSet這兩個實用類是專門為枚舉最佳化的標準集合實現。如果知道集合只包含枚舉類型,那麼應該使用這些專門的集合來代替Has                                   hMap或HashSet。

  大部分情況下,可以使用enum對代碼中的所有public static final int做插入替換。它們是可比的,並且可以靜態匯入,所以對它們的引用看起來是等同的,即使是對於內部類(或內部枚舉類型)。注意,比較枚舉類型的時候,聲明它們的指令表明了它們的順序值。

  “隱藏的”靜態方法

  兩個靜態方法出現在所有枚舉型別宣告中。因為它們是枚舉子類上的靜態方法,而不是Enum本身的方法,所以它們在java.lang.Enum的javadoc中沒有出現。

  第一個是values(),返回一個枚舉類型所有可能值的數組。

  第二個是valueOf(),為提供的字串返回一個枚舉類型,該枚舉類型必須精確地匹配原始碼聲明。

  方法

  關於枚舉類型,我們最喜歡的一個方面是它可以有方法。過去您可能需要編寫一些代碼,對public static final int進行轉換,把它從資料庫類型轉換為JDBC URL。而現在則可以讓枚舉類型本身帶一個整理代碼的方法。下面就是一個例子,包括DatabaseType枚舉類型的抽象方法以及每個枚舉執行個體中提供的實現:

public enum DatabaseType {
ORACLE {
public String getJdbcUrl() {...}
},
MYSQL {
public String getJdbcUrl() {...}
};
public abstract String getJdbcUrl();
}  現在枚舉類型可以直接提供它的實用方法。例如:

DatabaseType dbType = ...;
String jdbcURL = dbType.getJdbcUrl();

  要擷取URL,必須預Crowdsourced Security Testing道該實用方法在哪裡。
  可變參數(Vararg)

  正確地使用可變參數確實可以清理一些垃圾代碼。典型的例子是一個帶有可變的String參數個數的log方法:

Log.log(String code)
Log.log(String code, String arg)
Log.log(String code, String arg1, String arg2)
Log.log(String code, String[] args)

  當討論可變參數時,比較有趣的是,如果用新的可變參數替換前四個例子,將是相容的:

Log.log(String code, String... args)

  所有的可變參數都是源相容的——那就是說,如果重新編譯log()方法的所有調用程式,可以直接替換全部的四個方法。然而,如果需要向後的二進位相容性,那麼就需要捨去前三個方法。只有最後那個帶一個字串數組參數的方法等效於可變參數版本,因此可以被可變參數版本替換。

  類型強制轉換

  如果希望調用程式瞭解應該使用哪種類型的參數,那麼應該避免用可變參數進行類型強制轉換。看下面這個例子,第一項希望是String,第二項希望是Exception:

Log.log(Object... objects) {
String message = (String)objects[0];
if (objects.length > 1) {
Exception e = (Exception)objects[1];
// Do something with the exception
}
}  方法簽名應該如下所示,相應的可變參數分別使用String和Exception聲明:

Log.log(String message, Exception e, Object... objects) {...}

  不要使用可變參數破壞類型系統。需要強型別化時才可以使用它。對於這個規則,PrintStream.printf()是一個有趣的例外:它提供類型資訊作為自己的第一個參數,以便稍後可以接受那些類型。

  協變返回

  協變返回的基本用法是用於在已知一個實現的傳回型別比API更具體的時候避免進行類型強制轉換。在下面這個例子中,有一個返回Animal對象的Zoo介面。我們的實現返回一個AnimalImpl對象,但是在JDK 1.5之前,要返回一個Animal對象就必須聲明。:

public interface Zoo {
public Animal getAnimal();
}
public class ZooImpl implements Zoo {
public Animal getAnimal(){
return new AnimalImpl();
}
}  協變返回的使用替換了三個反模式:

直接欄位訪問。為了規避API限制,一些實現把子類直接暴露為欄位: ZooImpl._animal

另一種形式是,在知道實現的實際上是特定的子類的情況下,在調用程式中執行向下轉換: ((AnimalImpl)ZooImpl.getAnimal()).implMethod();

我看到的最後一種形式是一個具體的方法,該方法用來避免由一個完全不同的簽名所引發的問題: ZooImpl._getAnimal();

  這三種模式都有它們的問題和局限性。要麼是不夠整潔,要麼就是暴露了不必要的實現細節。

  協變

  協變返回模式就比較整潔、安全並且易於維護,它也不需要類型強制轉換或特定的方法或欄位:

public AnimalImpl getAnimal(){
return new AnimalImpl();
}

  使用結果:
ZooImpl.getAnimal().implMethod();

  使用泛型

  我們將從兩個角度來瞭解泛型:使用泛型和構造泛型。我們不討論List、Set和Map的顯而易見的用法。知道泛型集合是強大的並且應該經常使用就足夠了。

  我們將討論泛型方法的使用以及編譯器推斷類型的方法。通常這些都不會出問題,但是當出問題時,錯誤資訊會非常令人費解,所以需要瞭解如何修複這些問題。

泛型方法

  除了泛型型別,Java 5還引入了泛型方法。在這個來自java.util.Collections的例子中,構造了一個單元素列表。新的List的元素類型是根據傳入方法的對象的類型來推斷的:
static List Collections.singletonList(T o)
  樣本用法:
public List getListOfOne() {
return Collections.singletonList(1);
}  在樣本用法中,我們傳入了一個int。所以方法的傳回型別就是List。編譯器把T推斷為Integer。這和泛型型別是不同的,因為您通常不需要顯式地指定型別參數。
這也顯示了自動裝箱和泛型的相互作用。型別參數必須是參考型別:這就是為什麼我們得到的是List而不是List。

  不帶參數的泛型方法

  emptyList()方法與泛型一起引入,作為java.util.Collections中EMPTY_LIST欄位的型別安全置換:
static List Collections.emptyList()
  樣本用法:
public List getNoIntegers() {
return Collections.emptyList();
}  與先前的例子不同,這個方法沒有參數,那麼編譯器如何推斷T的類型呢?基本上,它將嘗試使用一次參數。如果沒有起作用,它再次嘗試使用返回或賦實值型別。在本例中,返回的是List,所以T被推斷為Integer。

  如果在返回語句或指派陳述式之外的位置調用泛型方法會怎麼樣呢?那麼編譯器將無法執行類型推斷的第二次傳送。在下面這個例子中,emptyList()是從條件運算子內部調用的:

public List getNoIntegers() {
return x ? Collections.emptyList() : null;
}  因為編譯器看不到返回上下文,也不能推斷T,所以它放棄並採用Object。您將看到一個錯誤訊息,比如:“無法將List轉換為List。”
為了修複這個錯誤,應顯式地向方法調用傳遞型別參數。這樣,編譯器就不會試圖推斷型別參數,就可以獲得正確的結果:

return x ? Collections.emptyList() : null;  這種情況經常發生的另一個地方是在方法調用中。如果一個方法帶一個List參數,並且需要為那個參數調用這個傳遞的emptyList(),那麼也需要使用這個文法。

  集合之外

  這裡有三個泛型型別的例子,它們不是集合,而是以一種新穎的方式使用泛型。這三個例子都來自標準的Java庫:
Class
Class在類的類型上被參數化了。這就使無需類型強制轉換而構造一個newInstance成為可能。
Comparable
Comparable被實際的比較型別參數化。這就在compareTo()調用時提供了更強的類型化。例如,String實現Comparable。對除String之外的任何東西調用compareTo(),都會在編譯時間失敗。
Enum>
Enum被枚舉型別參數化。一個名為Color的枚舉類型將擴充Enum
。getDeclaringClass()方法返回枚舉類型的類對象,在這個例子中就是一個Color對象。它與getClass()不同,後者可能返回一個無名類。
  萬用字元

  泛型最複雜的部分是對萬用字元的理解。我們將討論三種類型的萬用字元以及它們的用途。

  首先讓我們瞭解一下數組是如何工作的。可以從一個Integer[]為一個Number[]賦值。如果嘗試把一個Float寫到Number[]中,那麼可以編譯,但在運行時會失敗,出現一個ArrayStoreException:
Integer[] ia = new Integer[5];
Number[] na = ia;
na[0] = 0.5; // compiles, but fails at runtime
  如果試圖把該例直接轉換成泛型,那麼會在編譯時間失敗,因為賦值是不被允許的:
List iList = new ArrayList();
List nList = iList; // not allowed
nList.add(0.5);  如果使用泛型,只要代碼在編譯時間沒有出現警告,就不會遇到運行時ClassCastException。

  上限萬用字元

  我們想要的是一個確切元素類型未知的列表,這一點與數組是不同的。

  List是一個列表,其元素類型是具體類型Number。
  List extends Number>是一個確切元素類型未知的列表。它是Number或其子類型。

  上限

  如果我們更新初始的例子,並賦值給List extends Number>,那麼現在賦值就會成功了:

List iList = new ArrayList();
List extends Number> nList = iList;
Number n = nList.get(0);
nList.add(0.5); // Not allowed  我們可以從列表中得到Number,因為無論列表的確切元素類型是什麼(Float、Integer或Number),我們都可以把它賦值給Number。

  我們仍然不能把浮點類型插入列表中。這會在編譯時間失敗,因為我們不能證明這是安全的。如果我們想要向列表中添加浮點類型,它將破壞iList的初始型別安全——它只儲存Integer。

  萬用字元給了我們比數組更多的表達能力。

  為什麼使用萬用字元

  在下面這個例子中,萬用字元用於向API的使用者隱藏類型資訊。在內部,Set被儲存為CustomerImpl。而API的使用者只知道他們正在擷取一個Set,從中可以讀取Customer。
此處萬用字元是必需的,因為無法從Set向Set賦值:
public class CustomerFactory {
private Set _customers;
public Set extends Customer> getCustomers() {
return _customers;
}
}  萬用字元和協變返回

  萬用字元的另一種常見用法是和協變返回一起使用。與賦值相同的規則可以應用到協變返回上。如果希望在重寫的方法中返回一個更具體的泛型型別,聲明的方法必須使用萬用字元:

public interface NumberGenerator {
public List extends Number> generate();
}
public class FibonacciGenerator extends NumberGenerator {
public List generate() {
...
}
}  如果要使用數組,介面可以返回Number[],而實現可以返回Integer[]。

  下限

  我們所談的主要是關於上限萬用字元的。還有一個下限萬用字元。List super Number>是一個確切“元素類型”未知的列表,但是可能是Mnumber,或者Number的超類型。所以它可能是一個List或一個List。

  下限萬用字元遠沒有上限萬用字元那樣常見,但是當需要它們的時候,它們就是必需的。

  下限與上限

List extends Number> readList = new ArrayList();
Number n = readList.get(0);

List super Number> writeList = new ArrayList();
writeList.add(new Integer(5));  第一個是可以從中讀數的列表。
  第二個是可以向其寫數的列表。

  無界萬用字元

  最後,List>列表的內容可以是任何類型,而且它與List extends Object>幾乎相同。可以隨時讀取Object,但是不能向列表中寫入內容。 公用API中的萬用字元
  總之,正如前面所說,萬用字元在向調用程式隱藏實現細節方面是非常重要的,但即使下限萬用字元看起來是提供唯讀訪問,由於remove(int position)之類的非泛型方法,它們也並非如此。如果您想要一個真正不變的集合,可以使用java.util.Collection上的方法,比如unmodifiableList()。

  編寫API的時候要記得萬用字元。通常,在傳遞泛型型別時,應該嘗試使用萬用字元。它使更多的調用程式可以訪問API。

  通過接收List extends Number>而不是List,下面的方法可以由許多不同類型的列表調用:

void removeNegatives(List extends Number> list);  建構的泛型型別

  現在我們將討論構造自己的泛型型別。我們將展示一些例子,其中通過使用泛型可以提高型別安全,我們還將討論一些實現泛型型別時的常見問題。

  集合風格(Collection-like)的函數
  第一個泛型類的例子是一個集合風格的例子。Pair有兩個型別參數,而且欄位是類型的執行個體:

public final class Pair {
public final A first;
public final B second;

public Pair(A first, B second) {
this.first = first;
this.second = second;
}
}  這使從方法返回兩個項而無需為每個兩種類型的組合編寫專用的類成為可能。另一種方法是返回Object[],而這樣是類型不安全或者不整潔的。

  在下面的用法中,我們從方法返回一個File和一個Boolean。方法的用戶端可以直接使用欄位而無需類型強制轉換:

public Pair getFileAndWriteStatus(String path){
// create file and status
return new Pair(file, status);
}

Pair result = getFileAndWriteStatus("...");
File f = result.first;
boolean writeable = result.second;  集合之外

  在下面這個例子中,泛型被用於附加的編譯時間安全性。通過把DBFactory類參數化為所建立的Peer類型,您實際上是在強制Factory子類返回一個Peer的特定子類型:

public abstract class DBFactory {
protected abstract T createEmptyPeer();
public List get(String constraint) {
List peers = new ArrayList();
// database magic
return peers;
}
}
  通過實現DBFactory,CustomerFactory必須從createEmptyPeer()返回一個Customer:
public class CustomerFactory extends DBFactory{

public Customer createEmptyPeer() {
return new Customer();
}
}  泛型方法

  不管想要對參數之間還是參數與傳回型別之間的泛型型別施加約束,都可以使用泛型方法:

  例如,如果編寫的反轉函數是在位置上反轉,那麼可能不需要泛型方法。然而,如果希望反轉返回一個新的List,那麼可能會希望新List的元素類型與傳入的List的類型相同。在這種情況下,就需要一個泛型方法:

List reverse(List list)
  具體化

  當實現一個泛型類時,您可能想要構造一個數組T[]。因為泛型是通過擦除(erasure)實現的,所以這是不允許的。

  您可以嘗試把Object[]強制轉換為T[]。但這是不安全的。

  具體化解決方案

  按照泛型教程的慣例,解決方案使用的是“類型令牌”,通過向建構函式添加一個Class參數,可以強制用戶端為類的型別參數提供正確的類對象:
public class ArrayExample {
private Class clazz;

public ArrayExample(Class clazz) {
this.clazz = clazz;
}

public T[] getArray(int size) {
return (T[])Array.newInstance(clazz, size);
}
}  為了構造ArrayExample,用戶端必須把String.class傳遞給建構函式,因為String.class的類型是Class。

  擁有類對象使構造一個具有正確元素類型的數組成為可能。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.