避免在Java介面中使用陣列的3個理由

2014-08-07    分類:JAVA開發、程式設計開發、首頁精華4人評論發表於2014-08-07

如果你發現在一個介面使用有如下定義方法:

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>中的.

toArray 3 Good Reasons to Avoid Arrays in Java Interfaces

這個圖示展示了一個在Java 7上n=5的參考標準. 不過,更多的資料項或者是另外一個VM情況系啊,這幅圖片並不會改變太多. CPU的開銷可能並不會太劇烈,但是會有增長. 機會有一個陣列的使用者應該將其轉換到一個集合中去,以便利用它做任何事情, 然後將結果轉換回一個陣列,來送進另外一個介面的方法中,諸如此類做法.

是用一個簡單的ArrayList,而不是一個陣列來提升效能,無需再動太多的手腳. ArrayList 為封裝的陣列增加了32位元組的恆定開銷. 例如,一個有十個物件的陣列需要104位元組,一個ArrayList 136位元組.

使用 集合,你甚至可能決定返回內部列表的一個不可修改的版本:

public List<String> getNames() {
return Collections.unmodifiableList( namesList );
}

此操作會在固定的市價執行,因此他比任何上述其它的方法都要快很多(黃條). 其同一個防禦式的拷貝不同。一個不可修改的集合將會在你的內部資料變化時跟著變化。如果變化發生了,客戶端會在迭代資料項時執行到一個ConcurrentModificationException中. 可以認為它是一個糟糕的設計,介面提供了一個在執行時丟擲一個UnsupportedOperationException. 不過,至少對於內部的使用,這個方法對於一個防禦式的拷貝而言,會是一個高效能的選擇 – 一些不可能使用陣列實現的東西.

陣列定義一個結構,而不是一個介面

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,並且它也可能給型別安全和效能造成短板.

來自:oschina

相關文章