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<integer> 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
這兩個實用類是專門為枚舉優化的標準集合實現。如果知道集合只包含枚舉類型,那么應該使用這些專門的集合來代替
HashMap
或
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
,必須預先知道該實用方法在哪里。
可變參數
(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 <T> List<T> Collections.singletonList(T o)
示例用法:
public List<Integer> getListOfOne() {
??? return Collections.singletonList(1);
}
示例用法
:
在示例用法中,我們傳入了一個
int
。所以方法的返回類型就是
List
。編譯器把
T
推斷為
Integer
。這和泛型類型是不同的,因為您通常不需要顯式地指定類型參數。
這也顯示了自動裝箱和泛型的相互作用。類型參數必須是引用類型
:
這就是為什么我們得到的是
List
而不是
List
。
不帶參數的泛型方法
emptyList()
方法與泛型一起引入,作為
java.util.Collections
中
EMPTY_LIST
字段的類型安全置換
:
static <T> List<T> Collections.emptyList()
示例用法:
public List<Integer> getNoIntegers() {
??? return Collections.emptyList();
}
與先前的例子不同,這個方法沒有參數,那么編譯器如何推斷
T
的類型呢
?
基本上,它將嘗試使用一次參數。如果沒有起作用,它再次嘗試使用返回或賦值類型。在本例中,返回的是
List
,所以
T
被推斷為
Integer
。
如果在返回語句或賦值語句之外的位置調用泛型方法會怎么樣呢
?
那么編譯器將無法執行類型推斷的第二次傳送。在下面這個例子中,
emptyList()
是從條件運算符內部調用的
:
public List<Integer> getNoIntegers() {
??? return x ? Collections.emptyList() : null;
}
因為編譯器看不到返回上下文,也不能推斷
T
,所以它放棄并采用
Object
。您將看到一個錯誤消息,比如
:“
無法將
List<Object>
轉換為
List<Integer>
。
”
為了修復這個錯誤,應顯式地向方法調用傳遞類型參數。這樣,編譯器就不會試圖推斷類型參數,就可以獲得正確的結果:
return x ? Collections.<Integer>emptyList() : null;
這種情況經常發生的另一個地方是在方法調用中。如果一個方法帶一個
List<String>
參數,并且需要為那個參數調用這個傳遞的
emptyList()
,那么也需要使用這個語法。
集合之外
這里有三個泛型類型的例子,它們不是集合,而是以一種新穎的方式使用泛型。這三個例子都來自標準的
Java
庫:
??Class<T>
Class
在類的類型上被參數化了。這就使無需類型強制轉換而構造一個
newInstance
成為可能。
??Comparable<T>
Comparable
被實際的比較類型參數化。這就在
compareTo()
調用時提供了更強的類型化。例如,
String
實現
Comparable<String>
。對除
String
之外的任何東西調用
compareTo()
,都會在編譯時失敗。
??Enum<E extends Enum<E>>
Enum
被枚舉類型參數化。一個名為
Color
的枚舉類型將擴展
Enum<Color>
。
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<Integer> iList = new ArrayList<Integer>();
List<Number> nList = iList; // not allowed
nList.add(0.5);
如果使用泛型,只要代碼在編譯時沒有出現警告,就不會遇到運行時
ClassCastException
。
上限通配符
我們想要的是一個確切元素類型未知的列表,這一點與數組是不同的。
List<Number>
是一個列表,其元素類型是具體類型
Number
。
List<? extends Number>
是一個確切元素類型未知的列表。它是
Number
或其子類型。
上限
如果我們更新初始的例子,并賦值給
List<? extends Number>
,那么現在賦值就會成功了:
List<Integer> iList = new ArrayList<Integer>();
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<CustomerImpl>
向
Set<Customer>
賦值:
public class CustomerFactory {
??? private Set<CustomerImpl> _customers;
??? public Set<? extends Customer> getCustomers() {
??????? return _customers;
??? }
}
通配符和協變返回
通配符的另一種常見用法是和協變返回一起使用。與賦值相同的規則可以應用到協變返回上。如果希望在重寫的方法中返回一個更具體的泛型類型,聲明的方法必須使用通配符:
public interface NumberGenerator {
??? public List<? extends Number> generate();
}
public class FibonacciGenerator extends NumberGenerator {
??? public List<Integer> generate() {
??????? ...
??? }
}
如果要使用數組,接口可以返回
Number[]
,而實現可以返回
Integer[]
。
下限
我們所談的主要是關于上限通配符的。還有一個下限通配符。
List<? super Number>
是一個確切
“
元素類型
”
未知的列表,但是可能是
Mnumber
,或者
Number
的超類型。所以它可能是一個
List<Number>
或一個
List<Object>
。
下限通配符遠沒有上限通配符那樣常見,但是當需要它們的時候,它們就是必需的。
下限與上限
List<? extends Number> readList = new ArrayList<Integer>();
Number n = readList.get(0);
List<? super Number> writeList = new ArrayList<Object>();
writeList.add(new Integer(5));
第一個是可以從中讀數的列表。
第二個是可以向其寫數的列表。
無界通配符
最后,
List<?>
列表的內容可以是任何類型,而且它與
List<? extends Object>
幾乎相同。可以隨時讀取
Object
,但是不能向列表中寫入內容。
公共
API
中的通配符
總之,正如前面所說,通配符在向調用程序隱藏實現細節方面是非常重要的,但即使下限通配符看起來是提供只讀訪問,由于
remove(int position)
之類的非泛型方法,它們也并非如此。如果您想要一個真正不變的集合,可以使用
java.util.Collection
上的方法,比如
unmodifiableList()
。
編寫
API
的時候要記得通配符。通常,在傳遞泛型類型時,應該嘗試使用通配符。它使更多的調用程序可以訪問
API
。
通過接收
List<? extends Number>
而不是
List<Number>
,下面的方法可以由許多不同類型的列表調用:
void removeNegatives(List<? extends Number> list);
構造泛型類型
現在我們將討論構造自己的泛型類型。我們將展示一些例子,其中通過使用泛型可以提高類型安全性,我們還將討論一些實現泛型類型時的常見問題。
集合風格
(Collection-like)
的函數
第一個泛型類的例子是一個集合風格的例子。
Pair
有兩個類型參數,而且字段是類型的實例:
public final class Pair<A,B> {
??? 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<File,Boolean> getFileAndWriteStatus(String path){
??? // create file and status
??? return new Pair<File,Boolean>(file, status);
}
Pair<File,Boolean> result = getFileAndWriteStatus("...");
File f = result.first;
boolean writeable = result.second;
集合之外
在下面這個例子中,泛型被用于附加的編譯時安全性。通過把
DBFactory
類參數化為所創建的
Peer
類型,您實際上是在強制
Factory
子類返回一個
Peer
的特定子類型:
public abstract class DBFactory<T extends DBPeer> {
??? protected abstract T createEmptyPeer();
??? public List<T> get(String constraint) {
??????? List<T> peers = new ArrayList<T>();
??????? // database magic
??????? return peers;
??? }
}
通過實現
DBFactory<Customer>
,
CustomerFactory
必須從
createEmptyPeer()
返回一個
Customer
:
public class CustomerFactory extends DBFactory<Customer>{
??? public Customer createEmptyPeer() {
??????? return new Customer();
??? }
}
泛型方法
不管想要對參數之間還是參數與返回類型之間的泛型類型施加約束,都可以使用泛型方法:
例如,如果編寫的反轉函數是在位置上反轉,那么可能不需要泛型方法。然而,如果希望反轉返回一個新的
List
,那么可能會希望新
List
的元素類型與傳入的
List
的類型相同。在這種情況下,就需要一個泛型方法:
<T> List<T> reverse(List<T> list)
具體化
當實現一個泛型類時,您可能想要構造一個數組
T[]
。因為泛型是通過擦除
(erasure)
實現的,所以這是不允許的。
您可以嘗試把
Object[]
強制轉換為
T[]
。但這是不安全的。
具體化解決方案
按照泛型教程的慣例,解決方案使用的是
“
類型令牌
”
,通過向構造函數添加一個
Class<T>
參數,可以強制客戶端為類的類型參數提供正確的類對象:
public class ArrayExample<T> {
??? private Class<T> clazz;
??? public ArrayExample(Class<T> clazz) {
??????? this.clazz = clazz;
??? }
??? public T[] getArray(int size) {
??????? return (T[])Array.newInstance(clazz, size);
??? }
}
為了構造
ArrayExample<String>
,客戶端必須把
String.class
傳遞給構造函數,因為
String.class
的類型是
Class<String>
。
擁有類對象使構造一個具有正確元素類型的數組成為可能。
結束語
總而言之,新的語言特性有助于從根本上改變
Java
。通過了解在什么場景下使用以及如何使用這些新特性,您將會編寫出更好的代碼。
凡是有該標志的文章,都是該blog博主Caoer(草兒)原創,凡是索引、收藏
、轉載請注明來處和原文作者。非常感謝。