Java集合乾貨——ArrayList源碼分析

来源:https://www.cnblogs.com/bingyang-py/archive/2018/01/15/8286632.html
-Advertisement-
Play Games

ArrayList源碼分析 前言 在之前的文章中我們提到過ArrayList,ArrayList可以說是每一個學java的人使用最多最熟練的集合了,但是知其然不知其所以然。關於ArrayList的具體實現,一些基本的都也知道,譬如數組實現,線程不安全等等,但是更加具體的就很少去瞭解了,例如:初始化的 ...


ArrayList源碼分析

前言

在之前的文章中我們提到過ArrayList,ArrayList可以說是每一個學java的人使用最多最熟練的集合了,但是知其然不知其所以然。關於ArrayList的具體實現,一些基本的都也知道,譬如數組實現,線程不安全等等,但是更加具體的就很少去瞭解了,例如:初始化的長度,擴容等。

本篇主要通過一些對源碼的分析,講解幾個ArrayList常見的方法,以及和Vector的區別。

ArrayList

定義

1 public class ArrayList<E> extends AbstractList<E>
2         implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList實際上是一個動態數組,容量可以動態的增長,其繼承了AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些介面。

RandomAccess介面,被List實現之後,為List提供了隨機訪問功能,也就是通過下標獲取元素對象的功能。

實現了Cloneable, java.io.Serializable意味著可以被克隆和序列化。

初始化

在使用中我們經常需要new出來各種泛型的ArrayList,那麼在初始化過程是怎樣的呢?

如下一行代碼,創建一個ArrayList對象



List<Person> list = new ArrayList<>();

 

我們來看源碼,是如何初始化的,找到構造方法

//無參構造方法
public ArrayList() {
  super();
  this.elementData = EMPTY_ELEMENTDATA;
}

 

看到這些代碼的時候,我也是不解的,elementData和EMPTY_ELEMENTDATA是什麼啊?但是很明顯EMPTY_ELEMENTDATA是一個常量,追本溯源我們去看一下成員屬性。

//如果是無參構造方法創建對象的話,ArrayList的初始化長度為10,這是一個靜態常量
private static final int DEFAULT_CAPACITY = 10;
​
//在這裡可以看到我們不解的EMPTY_ELEMENTDATA實際上是一個空的對象數組
    private static final Object[] EMPTY_ELEMENTDATA = {};
​
//保存ArrayList數據的對象數組緩衝區 空的ArrayList的elementData = EMPTY_ELEMENTDATA 這就是為什麼說ArrayList底層是數組實現的了。elementData的初始容量為10,大小會根據ArrayList容量的增長而動態的增長。
    private transient Object[] elementData;
//集合的長度
    private int size;

 

執行完構造方法,如下圖

可以說ArrayList的作者真的是很貼心,連緩存都處理好了,多次new出來的新對象,都指向同一個引用。

添加方法add

add(E e)


 /**
     * Appends the specified element to the end of this list.
     */
//增加元素到集合的最後
public boolean add(E e) {
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  //因為++運算符的特點 先使用後運算  這裡實際上是
  //elementData[size] = e
  //size+1
  elementData[size++] = e;
  return true;
}

 

先不管ensureCapacityInternal的話,這個方法就是將一個元素增加到數組的size++位置上。

再說回ensureCapacityInternal,它是用來擴容的,準確說是用來進行擴容檢查的。下麵我們來看一下整個擴容的過程



//初始長度是10,size預設值0,假定添加的是第一個元素,那麼minCapacity=1 這是最小容量 如果小於這個容量就會報錯
//如果底層數組就是預設數組,那麼選一個大的值,就是10
private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //調用另一個方法ensureExplicitCapacity
        ensureExplicitCapacity(minCapacity);
    }
​
    private void ensureExplicitCapacity(int minCapacity) {
      //記錄修改的次數
        modCount++;
​
        // overflow-conscious code
      //如果傳過來的值大於初始長度 執行grow方法(參數為傳過來的值)擴容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
//真正的擴容
 private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
   //新的容量是在原有的容量基礎上+50% 右移一位就是二分之一
        int newCapacity = oldCapacity + (oldCapacity >> 1);
   //如果新容量小於最小容量,按照最小容量進行擴容
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
   //這裡是重點 調用工具類Arrays的copyOf擴容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
​
//Arrays
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
  T[] copy = ((Object)newType == (Object)Object[].class)
    ? (T[]) new Object[newLength]
    : (T[]) Array.newInstance(newType.getComponentType(), newLength);
  System.arraycopy(original, 0, copy, 0,
                   Math.min(original.length, newLength));
  return copy;
}
​


add(int index, E element)

添加到指定的位置



public void add(int index, E element) {
  //判斷索引是否越界,如果越界就會拋出下標越界異常
  rangeCheckForAdd(index);
//擴容檢查
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  //將指定下標空出 具體作法就是index及其後的所有元素後移一位
  System.arraycopy(elementData, index, elementData, index + 1,size - index);
  //將要添加元素賦值到空出來的指定下標處
  elementData[index] = element;
  //長度加1
  size++;
}
//判斷是否出現下標是否越界
private void rangeCheckForAdd(int index) {
  //如果下標超過了集合的尺寸 或者 小於0就是越界  
  if (index > size || index < 0)
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

 

remove(int index)

ArrayList支持兩種刪除元素的方式

  1. remove(int index) 按照下標刪除 常用

  2. remove(Object o) 按照元素刪除 會刪除和參數匹配的第一個元素

下麵我們看一下ArrayList的實現



 /**
 移除list中指定位置的元素
     * Removes the element at the specified position in this list.
     所有後續元素左移 下標減1
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *參數是要移除元素的下標
     * @param index the index of the element to be removed
     返回值是被移除的元素
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
public E remove(int index) {
  //下標越界檢查
  rangeCheck(index);
//修改次數統計
  modCount++;
  //獲取這個下標上的值
  E oldValue = elementData(index);
//計算出需要移動的元素個數 (因為需要將後續元素左移一位 此處計算出來的是後續元素的位數)
  int numMoved = size - index - 1;
  //如果這個值大於0 說明後續有元素需要左移  是0說明被移除的對象就是最後一位元素
  if (numMoved > 0)
    //索引index只有的所有元素左移一位  覆蓋掉index位置上的元素
    System.arraycopy(elementData, index+1, elementData, index,numMoved);
 // 將最後一個元素賦值為null  這樣就可以被gc回收了
  elementData[--size] = null; // clear to let GC do its work
//返回index位置上的元素
  return oldValue;
}
​
//移除特定元素
public boolean remove(Object o) {
  //如果元素是null 遍曆數組移除第一個null
  if (o == null) {
    for (int index = 0; index < size; index++)
      if (elementData[index] == null) {
        //遍歷找到第一個null元素的下標 調用下標移除元素的方法
        fastRemove(index);
        return true;
      }
  } else {
    //找到元素對應的下標 調用下標移除元素的方法
    for (int index = 0; index < size; index++)
      if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
      }
  }
  return false;
}
​
//按照下標移除元素
private void fastRemove(int index) {
  modCount++;
  int numMoved = size - index - 1;
  if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
  elementData[--size] = null; // clear to let GC do its work
}

 

ArrayList總結
  1. 底層數組實現,使用預設構造方法初始化出來的容量是10

  2. 擴容的長度是在原長度基礎上加二分之一

  3. 實現了RandomAccess介面,底層又是數組,get讀取元素性能很好

  4. 線程不安全,所有的方法均不是同步方法也沒有加鎖,因此多線程下慎用

  5. 順序添加很方便

  6. 刪除和插入需要複製數組 性能很差(可以使用LinkindList)

為什麼ArrayList的elementData是用transient修飾的?

transient修飾的屬性意味著不會被序列化,也就是說在序列化ArrayList的時候,不序列化elementData。

為什麼要這麼做呢?

  1. elementData不總是滿的,每次都序列化,會浪費時間和空間

  2. 重寫了writeObject 保證序列化的時候雖然不序列化全部 但是有的元素都序列化

所以說不是不序列化 而是不全部序列化。



private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
        // Write out array length
       s.writeInt(elementData.length);
    // Write out all elements in the proper order.
for (int i=0; i<size; i++)
           s.writeObject(elementData[i]);
    if (modCount != expectedModCount) {
           throw new ConcurrentModificationException();
    }
}

 

 

ArrayList和Vector的區別

標準答案
  1. ArrayList是線程不安全的,Vector是線程安全的

  2. 擴容時候ArrayList擴0.5倍,Vector擴1倍

那麼問題來了

ArrayList有沒有辦法線程安全?

Collections工具類有一個synchronizedList方法

可以把list變為線程安全的集合,但是意義不大,因為可以使用Vector

Vector為什麼是線程安全的?

老實講,拋開多線程 它倆區別沒多大,但是多線程下就不一樣了,那麼Vector是如何實現線程安全的,我們來看幾個關鍵方法

public synchronized boolean add(E e) {
  modCount++;
  ensureCapacityHelper(elementCount + 1);
  elementData[elementCount++] = e;
  return true;
}
​
public synchronized E remove(int index) {
  modCount++;
  if (index >= elementCount)
    throw new ArrayIndexOutOfBoundsException(index);
  E oldValue = elementData(index);
​
  int numMoved = elementCount - index - 1;
  if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
  elementData[--elementCount] = null; // Let gc do its work
return oldValue;
}

 

就代碼實現上來說,和ArrayList並沒有很多邏輯上的區別,但是在Vector的關鍵方法都使用了synchronized修飾。

 


我不能保證每一個地方都是對的,但是可以保證每一句話,每一行代碼都是經過推敲和斟酌的。希望每一篇文章背後都是自己追求純粹技術人生的態度。

永遠相信美好的事情即將發生。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 通過之前總結水平居中與垂直居中的基本方法,梳理垂直水平同時居中的方法就沒有那麼亂了。 text-align:center + line-height 如下圖,div2中用text-align+line-height實現單行文本水平垂直居中。 通過之前總結水平居中與垂直居中的基本方法,梳理垂直水平同時 ...
  • 昨天總結了css中水平居中的方法,今天來總結一下css中實現垂直居中的方法。 line-height line-height用於實現單行文本的垂直居中,如下圖中,我們要求單行文本垂直居中,只需要將div2設置行高line-height和height的值相同即可,也可以不用設置高度,因為單行文本的行高 ...
  • 其實呢,文件上傳的插件很多,可是現在做的東西要求儘量少用插件,所以就自己寫了一下。 之前也用node寫過對文件處理方面的東西,這次用php寫著試一下。 a.html文件 b.php文件: 在這當中也遇到了幾個問題 1.在PHP中通過print_r($_FILES)列印時,有時候formData裡面的 ...
  • 1 歷史起源 SGML——1986年國際標準化組織出版發佈了一個信息管理方面的國際標準(ISO 8879:1986信息處理)。 HTML 2.0——1995年11月作為RFC 1866發佈 XML 1.0——1998年,W3C發佈了XML1.0規範,使用它來簡化Internet的文檔信息傳輸 XHT ...
  • 前言 這篇博客主要介紹23種設計模式的適用範圍以及他們的優缺點,類圖儘量使用了實例的類圖來替代,沒有找到的類圖就用了設計模式本身的結構圖。 創建型模式 抽象工廠模式 提供一個創建產品的介面來負責創建相關或依賴的對象,而不具體明確指定具體類 優點: 抽象工廠模式將具體產品的創建延遲到具體工廠的子類中, ...
  • File類可以對操作系統中的文件進行操作: File類的靜態成員變數: File類的構造方法: File類的功能: 創建和刪除: 獲取功能: 判斷功能: 遍歷目錄獲取(list獲取): 文件過濾器: 在遍歷目錄的時候,可以根據需要,只獲取滿足條件的文件 ...
  • 普通方法,進階方法,大神方法 方法二不建議使用,因為有可能丟失精度 ...
  • Servlet上傳文件: Servlet 3.0改進了部分API,其中HttpServletRequest增加了對文件上傳的支持。 HttpServletRequest提供了兩個方法來處理文件上傳: 1.Part getPart(String name):根據名稱來獲取文件上傳域 2.Collect ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...