如果你發現在一個接口使用有如下定義方法:
public String[] getParameters();
那麼你應該認真反思。數組不僅僅老式,而且我們有合理的理由避免暴露它們。在這篇文章中,我將試圖總結在Java API中使用數組的缺陷。首先從最出人意料的一個例子開始。
你可能認為使用數組是最快速的,因為數組是大多數collection實現的底層數據結構。使用一個純數組怎麼會比使用一個包含數組的對象性能更低?
讓我們先從這個看起來很熟悉的普遍的習慣用法開始:
public String[] getNames() {
return namesList.toArray( new String[ namesList.size() ] );
}
這個方法從一個用來在其內部保存數據的可變集合處創建了一個數據. 它通過提供一個確切大小的數組來嘗試優化數組的創建. 有趣的是,這一“優化”使得其比下面的更簡單的版本速度還要慢(請看圖表中綠色VS橘色條):
public String[] getNames() {
return namesList.toArray( new String[ 0 ] );
}
不過,如果方法返回的是一個List, 創建防御式的副本又更加的快了 (紅條):
public List<String> getNames() {
return new ArrayList( namesList );
}
不同之處在於一個ArrayList將它的數據項放在一個Object[]數組中,並且使用的是無類型的toArray方法,其比有類型的方法要快很多(藍條). 這是類型安全的,因為無類型的數組時封裝在由編譯器檢查的泛型類型ArrayList<T>中的.
這個圖標展示了一個在Java 7上n=5的參考標准. 不過,更多的數據項或者是另外一個VM情況系啊,這幅圖片並不會改變太多. CPU的開銷可能並不會太劇烈,但是會有增長. 機會有一個數組的使用者應該將其轉換到一個集合中去,以便利用它做任何事情, 然後將結果轉換回一個數組,來送進另外一個接口的方法中,諸如此類做法.
使用一個簡單的數組列表,而不是可以提高性能的一個數組,無需過多的奔波。數組列表對包裝數組增加了一個持續的32字節開銷。例如,十個對象的一個數組,需要104個字節,一個數組列表有136字節。使用集合,你甚至可以決定返回內部列表一個不可修改的版本:
public list<string>getnames()
return collections.unmodifiablelist(namelist);
}
在常數時間內完成這個操作,因此它比任何上述(黃色條)要快很多。這與備用副本不一樣。當你的內部數據庫變更時,可以改變一個不可修改的集合。如果發生這種情況,客戶可以運行ConcurrentModificationException.當迭代此項目時,它可能被認為是一個在運行時,接口提供了一個拋出Unsupportoperationexception的方法的不友好界面。不過,至少會在內部使用,這個方法對於一個備用副本來說,可能是一個高性能的解決方案-使用數組,這是不可能的
Java 是一門面向對象的語言。面向對象的核心概念就是提供一些方法來訪問和操作它們的數據,而不是直接對數據域進行操作. 這些方法創建一個接口來描述你可以在對象上面做的事情.
由於java已經對性能做了設計,原生類型和數組已經被融合進了類型系統之中. 對象可以使用數組來在內容高效地存儲數據. 然而,即使通過數組來呈現一個可變集合的元素,它們也不會提供任何方法來訪問和操作這些元素. 事實上,除了直接訪問的替換元素之外,在數組上你沒有多少其它事情可以做. 數組甚至連toString 和 equals 都沒有一個有意義的實現, 而集合卻有:
String[] array = { "foo", "bar" };
List<String> list = Arrays.asList( array );
System.out.println( list );
// -> [foo, bar]
System.out.println( array );
// -> [Ljava.lang.String;@6f548414
list.equals( Arrays.asList( "foo", "bar" ) )
// -> true
array.equals( new String[] { "foo", "bar" } )
// -> false
不同於數組,集合的 API 提供了許多有用的方法來訪問元素. 用戶可以檢查包含的元素,提取子列表或者計算交集. 集合可以向數據層添加特定的特性, 諸如線程安全,同時將實現原理保持在內部可見.
通過使用一個數據,你定義了數據被保存在內存中的哪個地方. 通過使用一個集合,你定義了用戶可以在數據上做的操作.
如果你依賴於編譯器檢查的類型安全,小心對象數組. 下面的代碼會在運行時奔潰,但是編譯器找不出問題所在:
Number[] numbers = new Integer[10];
numbers[0] = Long.valueOf( 0 ); // throws ArrayStoreException
原因是數組是“協變式”的, 比如,如果 T 是S 的一個子類型, 那麼 T[] 就會是 S[] 的一個子類型. Joshua Bloch 在其著作 Effective Java 涵蓋了所有的理論, 每一個Java開發者必讀.
歸因於這個行為,暴露數組類型的接口允許返回聲明數組類型的一個子類型, 導致了一個怪異的運行時異常.
Bloch 同時也解釋說,數組與泛型類型不兼容. 因為數組會在運行時強制要求有類型信息,而泛型則會在編譯時被檢查,泛型類型不能被放到數組中.
一般而言,數組和泛型不能很好的融合。如果你發現自己在融合它們而得到了一個編譯時錯誤或者警告,那你的第一反應應該是用list去替換數組.
- Joshua Bloch, Effective Java (第二版), 第29條
數組底層的語言構造、它們會被用在實現中,但是它們不應該想其它的類暴露. 在一個接口方法中使用數組違背了面向對象的原則,它會導致違和的API,並且它也可能給類型安全和性能造成短板.