陣列容器(ArrayList)設計與Java實現,看完這個你不懂ArrayList,你找我!!!

一無是處的研究僧發表於2022-07-06

陣列容器(ArrayList)設計與Java實現

本篇文章主要跟大家介紹我們最常使用的一種容器ArrayListVector的原理,並且自己使用Java實現自己的陣列容器MyArrayList,讓自己寫的容器能像ArrayList那樣工作。在本篇文章當中首先介紹ArrayList的一些基本功能,然後去分析我們自己的容器MyArrayList應該如何進行設計,同時分析我們自己的具體實現方法,最後進行程式碼介紹!!!

ArrayList為我們提供了哪些功能?

我們來看一個簡單的程式碼,隨機生成100個隨機數,檢視生成隨機數當中是否存在50這個數。

public class MyArrayList {

  public static void main(String[] args) {
    Random random = new Random();
    ArrayList<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
      list.add(random.nextInt(5000));
    }
    for (int i = 0; i < 100; i++) {
      if (list.get(i) == 50) {
        System.out.println("包含資料 50");
      }
    }
    list.set(5, 1000);// 設定下標為5的資料為100
    list.remove(5);// 刪除下標為5的資料
    list.remove(new Integer(888));// 刪除容器當中的第一個值為5的資料
  }
}

上述程式碼包含了ArrayList最基本的一個功能,一個是add方法,向陣列容器當中加入資料,另外一個方法是get從容器當中拿出資料,set方法改變容器裡的資料,remove方法刪除容器當中的資料。ArrayList的很多其他的方法都是圍繞這四個最基本的方法展開的,因此我們在這裡不仔細介紹其他的方法了,後面我們自己實現的時候遇到問題的時候自然會需要設計相應的方法,然後我們進行解決即可。

現在我們就需要去設計一個陣列容器實現“增刪改查”這四個基本功能。

設計原理分析

首先明白一點我們需要使用什麼工具去實現這樣一個容器。我們手裡有的工具就是Java提供給我們的最基本的功能——陣列(這個好像是廢話,我們的標題就是陣列容器?)。

當我們在Java當中使用陣列去儲存資料時,資料在Java當中的記憶體佈局大致如下圖所示。

我們在設計陣列容器這樣一個資料結構的時候主要會遇到兩個問題:

  • 我們申請陣列的長度是多少。
  • 當陣列滿了之後怎麼辦,也就是我們的擴容機制。

對於這兩個問題,首先我們陣列的初始大小可以有預設值,在我們自己實現的MyArrayList當中設定為10,我們在使用類時也可以傳遞一個引數指定初始大小。第二個問題當我們的陣列滿的時候我們需要對陣列進行擴容,在我們實現的MyArrayList當中我們採取的方式是,新陣列的長度是原陣列的兩倍(這個跟JDKArrayList方式不一樣,ArrayList擴容為原來的1.5倍)。

程式碼實現

為了讓我們的類實現的更加簡單我們在程式碼當中就不做很多非必要的邏輯判斷並且丟擲異常,我們的程式碼只要能表現出我們的思想即可。

  • 首先定義一個介面MyCollection,表示我們要實現哪些方法!
public interface MyCollection<E> {

  /**
   * 往連結串列尾部加入一個資料
   * @param o 加入到連結串列當中的資料
   * @return
   */
  boolean add(E o);

  /**
   * 表示在第 index 位置插入資料 o
   * @param index
   * @param o
   * @return
   */
  boolean add(int index, E o);

  /**
   * 從連結串列當中刪除資料 o
   * @param o
   * @return
   */
  boolean remove(E o);

  /**
   * 從連結串列當中刪除第 index 個資料
   * @param index
   * @return
   */
  boolean remove(int index);

  /**
   * 往連結串列尾部加入一個資料,功能和 add 一樣
   * @param o
   * @return
   */
  boolean append(E o);

  /**
   * 返回連結串列當中資料的個數
   * @return
   */
  int size();

  /**
   * 表示連結串列是否為空
   * @return
   */
  boolean isEmpty();

  /**
   * 表示連結串列當中是否包含資料 o
   * @param o
   * @return
   */
  boolean contain(E o);

  /**
   * 設定下標為 index 的資料為 o
   * @param index
   * @param o
   * @return
   */
  boolean set(int index, E o);
}
  • 我們的建構函式,初始化過程。
  public MyArrayList(int initialCapacity) {
    this();
    // 增長陣列的空間為 initialCapacity,即申請一個陣列
    // 且陣列的長度為 initialCapacity
    grow(initialCapacity); 
  }

  public MyArrayList() {
    this.size = 0; // 容器當中的資料個數在開始時為 0
    this.elementData = EMPTY_INSTANCE; // 將陣列設定為空陣列
  }

  • 我們需要實現的最複雜的方法就是add了,這個方法是四個方法當中最複雜的,其餘的方法都相對比較簡單。
    • 進入add方法之後,我們需要找到符合要求的最小陣列長度,這個值通常是容器當中元素的個數size + 1 ,也就是圖中的minCapacity首先先比較這個值和現在陣列的長度,如果長度不夠的話則需要進行擴容,將陣列的長度擴大到原來的兩倍。
    • 如果不需要擴容,則直接講元素放入到陣列當中即可。

  @Override
  public boolean add(E o) {
    // 這個函式的主要作用就是確保陣列的長度至少為 size + 1
    ensureCapacity(size + 1);
    // 新增加了一個資料,容器的大小需要 + 1
    elementData[++size] = o;
    return true;
  }

  /**
   * 這個函式的主要作用就是確保陣列的長度至少為 capacity
   * @param capacity
   */
  public void ensureCapacity(int capacity) {
    int candidateCapacity = findCapacity(capacity);
    if (elementData.length < candidateCapacity)
      grow(candidateCapacity);
  }

  /**
   * 這個函式的主要目的就是找到最終陣列長度需求的容量
   * @param minCapacity
   * @return
   */
  private int findCapacity(int minCapacity) {
    /**
     * 如果 if 條件為 true 即 elementData 還是初始化時設定的空陣列
     * 那麼返回預設大小和需要大小的最大值 
     * 否則直接返回 minCapacity
     */
    if (elementData == EMPTY_INSTANCE){
      return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
  }
  • 我們為什麼需要將ensureCapacity的訪問限制許可權設定為public?因為我們想讓使用者儘量去使用這個函式,因為如果我們如果寫出下面這樣的程式碼我們會一直申請記憶體空間,然後也需要將前面的陣列釋放掉,會給垃圾回收器造成更大的壓力。
    ArrayList<Integer> list = new ArrayList<>();
    for (int i = 0; i < 1000000; i++) {
      list.add(i);
    }

下面我們對ArrayList的方法進行測試:

import java.util.ArrayList;

class Person {

  String name;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  @Override
  public String toString() {
    return "Person{" +
        "name='" + name + '\'' +
        '}';
  }
}


public class ArrayListTest {

  public static void main(String[] args) {
    ArrayList<Person> o1 = new ArrayList<>();
    o1.ensureCapacity(10000000);
    long start = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
      o1.add(new Person());
    }
    long end = System.currentTimeMillis();
    System.out.println("end - start: " + (end - start));
    ArrayList<Person> o2 = new ArrayList<>();
    start = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
      o2.add(new Person());
    }
    end = System.currentTimeMillis();
    System.out.println("end - start: " + (end - start));
  }
}
// 輸出結果
end - start: 1345
end - start: 4730

從上面的測試結果我們可以看出提前使用ensureCapacity方法之後,程式執行的時間更加短。

  • 插入資料的add方法。
  @Override
  public boolean add(E o) {
    // 這個函式的主要作用就是確保陣列的長度至少為 size + 1
    ensureCapacity(size + 1);
    // 新增加了一個資料,容器的大小需要 + 1
    elementData[size] = o;
    size++;
    return true;
  }

  • add在指定下標插入資料。
    • 首先將插入下標後的資料往後移動一個位置
    • 然後在將資料放在指定下標的位置。

  /**
   * 在下標 index 位置插入資料 o
   * 首先先將 index 位置之後的資料往後移動一個位置
   * 然後將 index 賦值為 o
   * @param index
   * @param o
   * @return
   */
  @Override
  public boolean add(int index, E o) {
    // 確保容器當中的陣列長度至少為 size + 1
    ensureCapacity(size + 1);
    // 將 elementData index位置之後的資料往後移動一個位置
    // 做一個原地拷貝
    System.arraycopy(elementData, index, elementData, index + 1,
        size - index); // 移動的資料個數為 size - index
    elementData[index] = o;
    size++;
    return true;
  }

  • 刪除資料的方法remove
    • 首先先刪除指定下標的資料。
    • 然後將指定下標後的資料往前移動一個位置
    • 在實際的操作過程中我們可以不刪除,直接移動,這樣也覆蓋被插入位置的資料了。

  /**
   * 移除下標為 index 的資料
   * @param index
   * @return
   */
  @Override
  public boolean remove(int index) {
    // 需要被移動的資料個數
    int numMoved = size - index - 1;
    if (numMoved > 0)
      System.arraycopy(elementData, index+1, elementData, index,
          numMoved);
    elementData[--size] = null;

    return true;
  }
  • 移除容器當中具體的某個物件。
  /**
   * 這個方法主要是用於溢位容器當中具體的某個資料
   * 首先先通過 for 迴圈遍歷容器當中的每個資料,
   * 比較找到相同的資料對應的下標,然後通過下標移除方法
   * @param o
   * @return
   */
  @Override
  public boolean remove(E o) {
    if (o == null) {
      for (int index = 0; index < size; index++)
        if (elementData[index] == null) {
          remove(index);
          return true;
        }
    } else {
      for (int index = 0; index < size; index++)
        if (o.equals(elementData[index])) {
          remove(index);
          return true;
        }
    }
    return false;
  }

  • set方法,這個方法就很簡單了。
  @Override
  public boolean set(int index, E o) {
    elementData[index] = o;
    return true;
  }
  • 重寫toString方法。
  @Override
  public String toString() {

    if (size <= 0)
      return "[]";

    StringBuilder builder = new StringBuilder();
    builder.append("[");
    for (int index = 0; index < size; index++) {
      builder.append(elementData[index].toString() + ", ");
    }
    builder.delete(builder.length() - 2, builder.length());
    builder.append("]");
    return builder.toString();
  }

  • 測試程式碼
public static void main(String[] args) {
    MyArrayList<Integer> list = new MyArrayList<>();
    for (int i = 0; i < 15; i++) {
      list.add(-i);
    }
    System.out.println(list.contain(5));
    System.out.println(list);
    list.remove(new Integer(-6));
    System.out.println(list);
    System.out.println(list.elementData.length); // 容器會擴容兩倍,而預設容器長度為10,因此這裡是 20 
    list.add(5, 99999);
    System.out.println(list);
    System.out.println(list.contain(99999));
  }
// 程式碼輸出
false
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14]
[0, -1, -2, -3, -4, -5, -7, -8, -9, -10, -11, -12, -13, -14]
20
[0, -1, -2, -3, -4, 99999, -5, -7, -8, -9, -10, -11, -12, -13, -14]
true

完整程式碼

import java.util.ArrayList;
import java.util.Arrays;


public class MyArrayList<E> implements MyCollection<E> {

  /**
   * 容器當中儲存資料的個數
   */
  private int size;

  /**
   * 容器中陣列的預設長度
   */
  private static final int DEFAULT_CAPACITY = 10;

  /**
   * 存放具體資料的陣列,也就是我們容器當中真正儲存資料的地方
   */
  Object[] elementData;

  /**
   * 當容器當中沒有資料將 elementData 設定為這個值,這個值是所有例項一起共享的
   */
  private static final Object[] EMPTY_INSTANCE = {};


  public MyArrayList(int initialCapacity) {
    this();
    // 增長陣列的空間為 initialCapacity,即申請一個陣列
    // 且陣列的長度為 initialCapacity
    grow(initialCapacity);
  }

  public MyArrayList() {
    this.size = 0; // 容器當中的資料個數在開始時為 0
    this.elementData = EMPTY_INSTANCE; // 將陣列設定為空陣列
  }

  /**
   * 這個函式的主要作用就是確保陣列的長度至少為 capacity
   * @param capacity
   */
  public void ensureCapacity(int capacity) {
    int candidateCapacity = findCapacity(capacity);
    if (elementData.length < candidateCapacity)
      grow(candidateCapacity);
  }

  /**
   * 這個函式的主要目的就是找到最終陣列長度需求的容量
   * @param minCapacity
   * @return
   */
  private int findCapacity(int minCapacity) {
    /**
     * 如果 if 條件為 true 即 elementData 還是初始化時設定的空陣列
     * 那麼返回預設大小和需要大小的最大值
     * 否則直接返回 minCapacity
     */
    if (elementData == EMPTY_INSTANCE){
      return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
  }

  /**
   * 該函式主要保證 elementData 的長度至少為 minCapacity
   * 如果陣列的長度小於 minCapacity 則需要進行擴容,反之
   * @param minCapacity 陣列的最短長度
   */
  private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 新的陣列長度為原來陣列長度的兩倍
    int newCapacity = oldCapacity << 1;

    // 如果陣列新陣列的長度 newCapacity 小於所需要的長度 minCapacity
    // 新申請的長度應該為 minCapacity
    if (newCapacity < minCapacity) {
      newCapacity = minCapacity;
    }
    // 申請一個長度為 newCapacity 的陣列,在將原來陣列
    // elementData 的資料拷貝到新陣列當中
    elementData = Arrays.copyOf(elementData, newCapacity);
  }

  @Override
  public boolean add(E o) {
    // 這個函式的主要作用就是確保陣列的長度至少為 size + 1
    ensureCapacity(size + 1);
    // 新增加了一個資料,容器的大小需要 + 1
    elementData[size] = o;
    size++;
    return true;
  }

  /**
   * 在下標 index 位置插入資料 o
   * 首先先將 index 位置之後的資料往後移動一個位置
   * 然後將 index 賦值為 o
   * @param index
   * @param o
   * @return
   */
  @Override
  public boolean add(int index, E o) {
    // 確保容器當中的陣列長度至少為 size + 1
    ensureCapacity(size + 1);
    // 將 elementData index位置之後的資料往後移動一個位置
    // 做一個原地拷貝
    System.arraycopy(elementData, index, elementData, index + 1,
        size - index); // 移動的資料個數為 size - index
    elementData[index] = o;
    size++;
    return true;
  }

  /**
   * 這個方法主要是用於溢位容器當中具體的某個資料
   * 首先先通過 for 迴圈遍歷容器當中的每個資料,
   * 比較找到相同的資料對應的下標,然後通過下標移除方法
   * @param o
   * @return
   */
  @Override
  public boolean remove(E o) {
    if (o == null) {
      for (int index = 0; index < size; index++)
        if (elementData[index] == null) {
          remove(index);
          return true;
        }
    } else {
      for (int index = 0; index < size; index++)
        if (o.equals(elementData[index])) {
          remove(index);
          return true;
        }
    }
    return false;
  }

  /**
   * 移除下標為 index 的資料
   * @param index
   * @return
   */
  @Override
  public boolean remove(int index) {
    // 需要被移動的資料個數
    int numMoved = size - index - 1;
    if (numMoved > 0)
      System.arraycopy(elementData, index+1, elementData, index,
          numMoved);
    elementData[--size] = null;

    return true;
  }

  @Override
  public boolean append(E o) {
    return add(o);
  }

  @Override
  public int size() {
    return size;
  }

  @Override
  public boolean isEmpty() {
    return size == 0;
  }

  @Override
  public boolean contain(E o) {
    if (o == null) {
      for (int index = 0; index < size; index++)
        if (elementData[index] == null) {
          return true;
        }
    } else {
      for (int index = 0; index < size; index++)
        if (o.equals(elementData[index])) {
          return true;
        }
    }
    return false;
  }

  @Override
  public String toString() {

    if (size <= 0)
      return "[]";

    StringBuilder builder = new StringBuilder();
    builder.append("[");
    for (int index = 0; index < size; index++) {
      builder.append(elementData[index].toString() + ", ");
    }
    builder.delete(builder.length() - 2, builder.length());
    builder.append("]");
    return builder.toString();
  }
    
  @Override
  public boolean set(int index, E o) {
    elementData[index] = o;
    return true;
  }


  public static void main(String[] args) {
    MyArrayList<Integer> list = new MyArrayList<>();
    for (int i = 0; i < 15; i++) {
      list.add(-i);
    }
    System.out.println(list.contain(5));
    System.out.println(list);
    list.remove(new Integer(-6));
    System.out.println(list);
    System.out.println(list.elementData.length);
    list.add(5, 99999);
    System.out.println(list);
    System.out.println(list.contain(99999));
  }
}

本篇文章我們介紹了ArrayList的內部原理,並且我們實現了一個自己的簡單陣列容器MyArrayList,但是我們還有一些內容沒有涉及,比如cloneequals和迭代器,這些內容我們下期分析ArrayList原始碼再進行分析,我是LeHung,我們下期再見!!!

關注公眾號:一無是處的研究僧,瞭解更多計算機知識。

相關文章