前不久幫同事一塊兒 review
一個 job
執行緩慢的問題時發現很多朋友在擼碼實現功能時仍是有須要細節不夠注意,因而便有了這篇文章。java
List<String> temp = new ArrayList() ;
//獲取一批數據
List<String> all = getData();
for(String str : all) {
temp.add(str);
}
複製代碼
首先你們看看這段代碼有什麼問題嘛?git
其實在大部分狀況下這都是沒啥問題,無非就是循環的往 ArrayList
中寫入數據而已。github
但在特殊狀況下,好比這裏的 getData()
返回數據很是巨大時後續 temp.add(str)
就會有問題了。api
好比咱們在 review
代碼時發現這裏返回的數據有時會高達 2000W,這時 ArrayList
寫入的問題就凸顯出來了。數組
你們都知道 ArrayList 是由數組實現,而數據的長度有限;須要在合適的時機對數組擴容。函數
這裏以插入到尾部爲例 add(E e)。性能
ArrayList<String> temp = new ArrayList<>(2) ;
temp.add("1");
temp.add("2");
temp.add("3");
複製代碼
當咱們初始化一個長度爲 2 的 ArrayList
,並往裏邊寫入三條數據時 ArrayList
就得擴容了,也就是將以前的數據複製一份到新的數組長度爲 3 的數組中。測試
之因此是 3 ,是由於新的長度=原有長度 * 1.5ui
經過源碼咱們能夠得知 ArrayList
的默認長度爲 10.spa
但其實並非在初始化的時候就建立了 DEFAULT_CAPACITY = 10
的數組。
而是在往裏邊 add
第一個數據的時候會擴容到 10.
既然知道了默認的長度爲 10 ,那說明後續一旦寫入到第九個元素的時候就會擴容爲 10*1.5 =15
。 這一步爲數組複製,也就是要從新開闢一塊新的內存空間存放這 15 個數組。
一旦咱們頻繁且數量巨大的進行寫入時就會致使許多的數組複製,這個效率是極低的。
但若是咱們提早預知了可能會寫入多少條數據時就能夠提早避免這個問題。
好比咱們往裏邊寫入 1000W 條數據,在初始化的時候就給定數組長度與用默認 10 的長度之間性能是差距巨大的。
我用 JMH 基準測試驗證以下:
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class CollectionsTest {
private static final int TEN_MILLION = 10000000;
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void arrayList() {
List<String> array = new ArrayList<>();
for (int i = 0; i < TEN_MILLION; i++) {
array.add("123");
}
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void arrayListSize() {
List<String> array = new ArrayList<>(TEN_MILLION);
for (int i = 0; i < TEN_MILLION; i++) {
array.add("123");
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(CollectionsTest.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
複製代碼
根據結果能夠看出預設長度的效率會比用默認的效率高上不少(這裏的 Score
指執行完函數所消耗的時間)。
因此這裏強烈建議你們:在有大量數據寫入 ArrayList
時,必定要初始化指定長度。
再一個是必定要慎用 add(int index, E element)
向指定位置寫入數據。
經過源碼咱們能夠看出,每一次寫入都會將 index 後的數據日後移動一遍,其實本質也是要複製數組;
但區別於往常規的往數組尾部寫入數據,它每次都會進行數組複製,效率極低。
提到 ArrayList
就不得不聊下 LinkedList
這個孿生兄弟;雖然說都是 List
的容器,但本質實現卻徹底不一樣。
LinkedList
是由鏈表組成,每一個節點又有頭尾兩個節點分別引用了先後兩個節點;所以它也是一個雙向鏈表。
因此理論上來講它的寫入很是高效,將不會有 ArrayList 中效率極低的數組複製,每次只須要移動指針便可。
這裏偷懶就不畫圖了,你們自行腦補下。
坊間一直流傳:
LinkedList 的寫入效率高於 ArrayList,因此在寫大於讀的時候很是適用於 LinkedList 。
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void linkedList() {
List<String> array = new LinkedList<>();
for (int i = 0; i < TEN_MILLION; i++) {
array.add("123");
}
}
複製代碼
這裏測試看下結論是否符合;一樣的也是對 LinkedList
寫入 1000W
次數據,經過結果來看初始化數組長度的 ArrayList
效率明顯是要高於 LinkedList
。
但這裏的前提是要提早預設 ArrayList
的數組長度,避免數組擴容,這樣 ArrayList
的寫入效率是很是高的,而 LinkedList
的雖然不須要複製內存,但卻須要建立對象,變換指針等操做。
而查詢就不用多說了,ArrayList
能夠支持下標隨機訪問,效率很是高。
LinkedList
因爲底層不是數組,不支持經過下標訪問,而是須要根據查詢 index 所在的位置來判斷是從頭仍是從尾進行遍歷。
但不論是哪一種都得須要移動指針來一個個遍歷,特別是 index
靠近中間位置時將會很是慢。
高性能應用都是從小細節一點點堆砌起來的,就如這裏提到的 ArrayList
的坑同樣,平常使用沒啥大問題,一旦數據量起來全部的小問題都會成爲大問題。
因此再總結下:
add(index,e)
api,會致使複製數組,下降效率。Map
容器 HashMap
也是推薦要初始化長度從而避免擴容。本文全部測試代碼:
你的點贊與分享是對我最大的支持