1. 程式人生 > >將Java中的陣列進行二次封裝成屬於我們自己的陣列

將Java中的陣列進行二次封裝成屬於我們自己的陣列

我們來簡略回顧一下Java陣列的基礎概念:

  • 陣列最大的優點是可以快速查詢,因為陣列直接通過索引查詢很快:array[2]。其資料結構是簡單的線性序列,這使得元素訪問非常快速,並且按照索引遍歷陣列方便
  • 陣列最好應用於“索引有語意”的情況
  • 但並非所有有語意的索引都適用於陣列,例如索引是×××號這種長度的數字,就無法作為索引使用
  • 而陣列也同樣可以處理“索引沒有語意”的情況
  • 陣列的缺點:
    • 根據內容查詢元素速度慢
    • 陣列的大小一經確定不能改變
    • 陣列只能儲存一種型別的資料
    • 插入、指定刪除元素效率低
    • 未封裝任何方法,所有操作都需要使用者自己定義

“索引沒有語意”的情況會遇到的問題:

  • 索引沒有語意,如何表示沒有元素?
  • 如何新增元素?如何刪除元素?如何修改元素?

所以我們要將Java中的陣列進行二次封裝成屬於我們自己的陣列容器,以此來解決這些問題。我們將其封裝在一個類中,該類命名為Array,通過提高各種增刪改查的方法來運算元組。我們首先來編寫這個Array類的基本框架:

/**
 * @program: Data-Structure
 * @description: 將Java中的靜態陣列進行二次封裝成動態陣列
 * @author: 01
 * @create: 2018-11-02 22:17
 **/
public class Array {
    /**
     * 實際存放資料的陣列
     */
    private int[] data;

    /**
     * 表示陣列中元素的個數
     */
    private int size;

    /**
     * 傳入陣列的容量capacity構造Array
     * 以便使用者自定義陣列的容量
     *
     * @param capacity 容量
     */
    public Array(int capacity) {
        data = new int[capacity];
        size = 0;
    }

    public Array() {
        // 預設陣列容量為10
        this(10);
    }

    /**
     * 獲取陣列中元素的個數
     *
     * @return 陣列中元素的個數
     */
    public int getSize() {
        return size;
    }

    /**
     * 獲取陣列的容量
     *
     * @return 陣列的容量
     */
    public int getCapacity() {
        return data.length;
    }

    /**
     * 返回陣列是否為空
     *
     * @return 為空返回true,否則返回false
     */
    public boolean isEmpty() {
        return size == 0;
    }
}

向陣列中新增元素

在上一小節中,我們完成了Array基本框架的編寫,這一小節我們來實現向陣列中新增元素。最簡單方式就是向陣列的末尾新增元素,因為size始終會指向最後一個元素+1的位置,即陣列的末尾第一個沒有元素的位置。所以當新增元素的時候,我們將元素放置在size的位置即可,然後我們需要維護size,讓其+1,這樣size又繼續指向陣列的末尾,以此類推。

具體程式碼如下:

/**
 * 向最後一個元素+1的位置新增一個新的元素
 *
 * @param e 新的元素
 */
public void addLast(int e) {
    // 若陣列已滿則丟擲異常,這裡暫時先不做動態擴容
    if (size == data.length) {
        throw new IllegalArgumentException("AddLast failed. Array is full.");
    }
    data[size] = e;
    size++;
}

但在一些特殊的需求下,可能需要向指定的位置新增元素,例如陣列中的元素是有序的,我希望把新元素插入到合適的位置,以保證陣列內元素的有序性,如下圖:
將Java中的陣列進行二次封裝成屬於我們自己的陣列

在這種情況下,實現思路就是將索引為1位置的元素及其後面的元素都往後挪一下。首先我們從100開始往後挪一,挪到索引為4的位置,99挪到索引為3的位置,88挪到索引為2的位置,此時索引1就空出來了,於是把77放入到索引為1的位置,最後還需要將size+1,讓其指向陣列末尾即可。

具體實現程式碼如下:

/**
 * 在第index的位置下插入一個新的元素e
 *
 * @param index 索引
 * @param e     新的元素
 */
public void add(int index, int e) {
    // 若陣列已滿則丟擲異常,這裡暫時先不做動態擴容
    if (size == data.length) {
        throw new IllegalArgumentException("Add failed. Array is full.");
    }

    // 若索引為負數或索引大於size,則丟擲異常
    // 因為index大於size會造成陣列內的元素不具有連續性,而index小於0則是無效的索引
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
    }

    // 從最後一個元素開始遍歷陣列中的元素,直到抵達index指向的索引位置
    for (int i = size - 1; i >= index; i--) {
        // 每個元素向後挪一位
        data[i + 1] = data[i];
    }

    // 也可以直接使用陣列拷貝的函式來實現這個邏輯,這裡為了表明這個插入的邏輯,所以沒有使用陣列拷貝
    // System.arraycopy(data, index, data, index + 1, size - index);

    // 在index的位置插入新的元素
    data[index] = e;
    // 最後size需+1
    size++;
}

實現了插入的方法後,我們就可以複用該方法,將之前的addLast簡化,如下:

/**
 * 向最後一個元素+1的位置新增一個新的元素
 *
 * @param e 新的元素
 */
public void addLast(int e) {
    add(size, e);
}

同樣的可以複用該方法,實現往陣列的最前面新增一個新的元素,如下:

/**
 * 往所有元素前新增一個新的元素
 *
 * @param e 新的元素
 */
public void addFirst(int e) {
    add(0, e);
}

陣列中查詢元素和修改元素

有時候我們需要知道陣列中有哪些元素以及陣列當前的size和容量是多少,這時候我們就可以實現toString方法,將這些資料作為字串打印出來,這也屬於是查詢的一種了。實現程式碼如下:

@Override
public String toString() {
    if (isEmpty()) {
        return "[]";
    }

    StringBuilder sb = new StringBuilder();
    sb.append(String.format("Array: size = %d, capacity = %d\n", size, data.length));
    sb.append("[");
    for (int i = 0; i < size; i++) {
        sb.append(data[i]);
        if (i != size - 1) {
            sb.append(", ");
        }
    }
    return sb.append("]").toString();
}

除此之外,我們很多時候需要查詢特定的元素以及修改特定的元素,這兩個邏輯實現起來也很簡單。使用者在修改、查詢特定元素時,都需將索引傳遞進來,所以在此之前我們來封裝一個私有的方法,用於檢查索引是否合法,這樣其他方法就能複用這個邏輯,無需重複編寫檢查index的邏輯了。程式碼如下:

/**
 * 檢查索引是否合法
 *
 * @param index index
 */
private void checkIndex(int index) {
    // 若索引為負數或索引大於等於size,則丟擲異常
    // 因為index大於等於size時使用者會訪問到無效的資料,而index小於0則是無效的索引
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Require index >= 0 and index <= size.");
    }
}

然後實現查詢特定的元素以及修改特定的元素的方法:

/**
 * 獲取index索引位置的元素
 *
 * @param index index
 * @return index索引位置的元素
 */
public int get(int index) {
    checkIndex(index);
    return data[index];
}

/**
 * 修改index索引位置的元素為e
 *
 * @param index index
 * @param e     e
 */
public void set(int index, int e) {
    checkIndex(index);
    data[index] = e;
}

包含,搜尋和刪除

當我們的數組裡有一定數量的元素時,往往會遇到一個需求,就是查詢這些元素中,是否包含某個特定的元素。還有一個常見的需求就是查詢特定元素所在的索引位置,即搜尋該元素並返回該元素所在的索引,若該元素不存在則返回一個特定的值,一般是-1,因為-1通常代表無效索引。

這兩個邏輯實現起來也很簡單,具體程式碼如下:

/**
 * 查詢陣列中是否有元素e
 *
 * @param e e
 * @return 有返回true,沒有返回false
 */
public boolean contains(int e) {
    int index = indexOf(e);

    return index != -1;
}

/**
 * 查詢陣列中元素e所在的索引,如果不存在元素e,則返回-1
 *
 * @param e e
 * @return 元素e所在的索引,或-1
 */
public int indexOf(int e) {
    for (int i = 0; i < size; i++) {
        if (data[i] == e) {
            return i;
        }
    }
    return -1;
}

接下來我們看看如何刪除指定索引位置的元素,例如我要刪除索引為1的元素,如下圖:
將Java中的陣列進行二次封裝成屬於我們自己的陣列

其實這個刪除與我們之前實現的插入邏輯差不多,反過來就可以了。如圖中,我們需要刪除的索引為1的元素,只需要把索引為2的元素往左移一格,覆蓋索引為1的元素,然後索引為3的元素再往左移一格,覆蓋索引為2的元素,接著索引為4的元素再往左移一格,覆蓋索引為3的元素,以此類推...直到所有的元素都往左移一格後,將size-1即可。最後原本索引為1的元素已經被覆蓋掉了,陣列中不再有77這個元素,也就實現了刪除的邏輯,如下圖:
將Java中的陣列進行二次封裝成屬於我們自己的陣列

有的小夥伴可能會有疑問,最後陣列中多了一個元素,會有影響嗎?其實並不會有影響,因為size所指向的索引位置及其後面的索引位置,對於使用者來說都是不可見的。而且陣列在初始化的時候也是會有一個預設值的,例如這裡是int型別的資料預設值就為0,由於使用者只能訪問到他新增進陣列的元素,並且我們在上一小節中也編寫了一個檢查索引的方法,能夠保證使用者的索引是合法的,所以使用者並不知道這裡多了一個元素,也就不會有任何影響。當然你也可以在size-1後將這個多出來的元素給置空。最後還需要提一下的是,基本資料型別的陣列可以不用管也無所謂,但如果是引用型別的陣列的話,最好是將這個多出來的元素覆蓋為null,這樣該資料就能夠快速的被垃圾回收掉,能夠稍微優化一些效能。

具體的實現程式碼如下:

/**
 * 從陣列中刪除index位置的元素,並返回刪除的元素
 *
 * @param index index
 * @return 被刪除的元素
 */
public int remove(int index) {
    checkIndex(index);

    int ret = data[index];
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }

    // 同樣的,這裡也可以直接使用陣列拷貝的函式來實現這個邏輯
    // System.arraycopy(data, index + 1, data, index, size - index + 1);

    size--;
    data[size] = 0;

    return ret;
}

然後基於這個刪除方法,我們還可以擴充套件一些便捷的方法提供給使用者使用,如下:

/**
 * 從陣列中刪除第一個元素,並返回刪除的元素
 *
 * @return 被刪除的元素
 */
public int removeFirst() {
    return remove(0);
}

/**
 * 從陣列中刪除最後一個元素,並返回刪除的元素
 *
 * @return 被刪除的元素
 */
public int removeLast() {
    return remove(size - 1);
}

/**
 * 從陣列中刪除元素e
 *
 * @param e 需要刪除的元素
 * @return 刪除成功返回true,否則返回false
 */
public boolean removeElement(int e) {
    int index = indexOf(e);
    if (index != -1) {
        remove(index);
        return true;
    }
    return false;
}

/**
 * 刪除所有的元素e
 *
 * @param e 需要刪除的元素
 * @return 刪除成功返回true,否則返回false
 */
public boolean removeAllElement(E e) {
    for (int i = 0; i < size; ) {
        if (data[i] == e) {
            remove(i);
        } else {
            i++;
        }
    }

    return indexOf(e) == -1;
}

使用泛型

到目前為止,我們的Array類只能夠儲存int型別的資料,但是其作為儲存資料的容器,不應該只能儲存一種型別的資料,而是能夠儲存任意型別的資料。為了讓我們的Array類能夠儲存任意型別的資料,就需要使用到Java中的泛型。但是需要知道Java中的泛型是不能夠接收基本資料型別的,只能夠接收引用型別。不過好在Java中的基本資料型別都有各自的包裝類,所謂包裝類就是把基本型別封裝成一個類,這樣泛型就能夠接收了。

這裡不對泛型進行過多的介紹,如果對泛型不太清楚的話,可以查閱相關資料。使用泛型改造後的Array類程式碼如下:

/**
 * @program: Data-Structure
 * @description: 將Java中的靜態陣列進行二次封裝成動態陣列
 * @author: 01
 * @create: 2018-11-02 22:17
 **/
public class Array<E> {
    /**
     * 實際存放資料的陣列
     */
    private E[] data;

    /**
     * 陣列中元素的個數
     */
    private int size;

    /**
     * 傳入陣列的容量capacity構造Array
     * 以便使用者自定義陣列的容量
     *
     * @param capacity 容量
     */
    public Array(int capacity) {
        // java語法不支援直接new泛型或泛型陣列,所以我們需要先new一個Object進行強轉
        data = (E[]) new Object[capacity];
        size = 0;
    }

    public Array() {
        // 預設陣列容量為10
        this(10);
    }

    /**
     * 獲取陣列中元素的個數
     *
     * @return 陣列中元素的個數
     */
    public int getSize() {
        return size;
    }

    /**
     * 獲取陣列的容量
     *
     * @return 陣列的容量
     */
    public int getCapacity() {
        return data.length;
    }

    /**
     * 返回陣列是否為空
     *
     * @return 為空返回true,否則返回false
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 向最後一個元素+1的位置新增一個新的元素
     *
     * @param e 新的元素
     */
    public void addLast(E e) {
        add(size, e);
    }

    /**
     * 往所有元素前新增一個新的元素
     *
     * @param e 新的元素
     */
    public void addFirst(E e) {
        add(0, e);
    }

    /**
     * 在第index的位置下插入一個新的元素e
     *
     * @param index 索引
     * @param e     新的元素
     */
    public void add(int index, E e) {
        // 若陣列已滿則丟擲異常
        if (size == data.length) {
            throw new IllegalArgumentException("Add failed. Array is full.");
        }

        // 若索引為負數或索引大於size,則丟擲異常
        // 因為index大於size會造成陣列內的元素不具有連續性,而index小於0則是無效的索引
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
        }

        // 從最後一個元素開始遍歷陣列中的元素,直到抵達index指向的索引位置
        for (int i = size - 1; i >= index; i--) {
            // 每個元素向後挪一位
            data[i + 1] = data[i];
        }

        // 也可以直接使用陣列拷貝的函式來實現這個邏輯,這裡為了表明這個插入的邏輯,所以沒有使用陣列拷貝
        // System.arraycopy(data, index, data, index + 1, size - index);

        data[index] = e;
        size++;
    }

    /**
     * 獲取index索引位置的元素
     *
     * @param index index
     * @return index索引位置的元素
     */
    public E get(int index) {
        checkIndex(index);
        return data[index];
    }

    /**
     * 修改index索引位置的元素為e
     *
     * @param index index
     * @param e     e
     */
    public void set(int index, E e) {
        checkIndex(index);
        data[index] = e;
    }

    /**
     * 查詢陣列中是否有元素e
     *
     * @param e e
     * @return 有返回true,沒有返回false
     */
    public boolean contains(E e) {
        int index = indexOf(e);

        return index != -1;
    }

    /**
     * 查詢陣列中元素e所在的索引,如果不存在元素e,則返回-1
     *
     * @param e e
     * @return 元素e所在的索引,或-1
     */
    public int indexOf(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 從陣列中刪除index位置的元素,並返回刪除的元素
     *
     * @param index index
     * @return 被刪除的元素
     */
    public E remove(int index) {
        checkIndex(index);

        E ret = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = data[i];
        }

        // 同樣的,這裡也可以直接使用陣列拷貝的函式來實現這個邏輯
        // System.arraycopy(data, index + 1, data, index, size - index + 1);

        size--;
        data[size] = null;

        return ret;
    }

    /**
     * 從陣列中刪除第一個元素,並返回刪除的元素
     *
     * @return 被刪除的元素
     */
    public E removeFirst() {
        return remove(0);
    }

    /**
     * 從陣列中刪除最後一個元素,並返回刪除的元素
     *
     * @return 被刪除的元素
     */
    public E removeLast() {
        return remove(size - 1);
    }

    /**
     * 從陣列中刪除元素e
     *
     * @param e 需要刪除的元素
     * @return 刪除成功返回true,否則返回false
     */
    public boolean removeElement(E e) {
        int index = indexOf(e);
        if (index != -1) {
            remove(index);
            return true;
        }
        return false;
    }

    /**
     * 刪除所有的元素e
     *
     * @param e 需要刪除的元素
     * @return 刪除成功返回true,否則返回false
     */
    public boolean removeAllElement(E e) {
        for (int i = 0; i < size; ) {
            if (data[i] == e) {
                remove(i);
            } else {
                i++;
            }
        }

        return indexOf(e) == -1;
    }

    @Override
    public String toString() {
        if (isEmpty()) {
            return "[]";
        }

        StringBuilder sb = new StringBuilder();
        sb.append(String.format("Array: size = %d, capacity = %d\n", size, data.length));
        sb.append("[");
        for (int i = 0; i < size; i++) {
            sb.append(data[i]);
            if (i != size - 1) {
                sb.append(", ");
            }
        }
        return sb.append("]").toString();
    }

    /**
     * 檢查索引是否合法
     *
     * @param index index
     */
    private void checkIndex(int index) {
        // 若索引為負數或索引大於等於size,則丟擲異常
        // 因為index大於等於size時使用者會訪問到無效的資料,而index小於0則是無效的索引
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Require index >= 0 and index <= size.");
        }
    }
}

動態陣列

通過引入泛型現在我們的Array已經能夠儲存任意型別的資料了,並且在以上小節中也已經實現了基本的增刪查改方法。現在就剩最後一個問題,Array內部的陣列還是一個靜態陣列,而靜態陣列的容量是有限的,並且在初始化的時候就已經定下了大小無法改變。在實際開發中,我們通常無法確定陣列的大小,我們希望當陣列容量滿了之後可以自動進行擴容,而不是丟擲陣列越界異常,所以我們要實現動態陣列。

其實實現動態擴容的思路也很簡單,當新增元素時發現數組容量滿了之後,就建立一個容量更大的陣列,例如建立一個比原來陣列大兩倍的一個新陣列(ArrayList中為1.5倍),然後把舊陣列的元素通通拷貝到新陣列中,接著把data的引用指向這個新的陣列即可,此時舊陣列就會失去引用,從而被垃圾回收掉,這樣一來就完成了擴容。剩下的邏輯還是和之前一樣把新的元素照常新增到data中就行了。

具體的重置陣列容量的方法程式碼如下:

/**
 * 重置陣列容量
 *
 * @param newCapacity 新的容量
 */
private void resize(int newCapacity) {
    E[] newData = (E[]) new Object[newCapacity];
    for (int i = 0; i < size; i++) {
        newData[i] = data[i];
    }

    // 可以直接使用陣列拷貝的方式
    // System.arraycopy(data, 0, newData, 0, size);

    data = newData;
}

然後修改新增元素的方法如下:

/**
 * 在第index的位置下插入一個新的元素e
 *
 * @param index 索引
 * @param e     新的元素
 */
public void add(int index, E e) {
    // 若索引為負數或索引大於size,則丟擲異常
    // 因為index大於size會造成陣列內的元素不具有連續性,而index小於0則是無效的索引
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
    }

    // 若陣列已滿則進行擴容
    if (size == data.length) {
        resize(data.length * 2);
    }

    // 從最後一個元素開始遍歷陣列中的元素,直到抵達index指向的索引位置
    for (int i = size - 1; i >= index; i--) {
        // 每個元素向後挪一位
        data[i + 1] = data[i];
    }

    // 也可以直接使用陣列拷貝的函式來實現這個邏輯,這裡為了表明這個插入的邏輯,所以沒有使用陣列拷貝
    // System.arraycopy(data, index, data, index + 1, size - index);

    data[index] = e;
    size++;
}

在刪除元素的時候,我們也希望陣列中的元素少於陣列容量的二分之一時就縮減容量,同樣的只需要呼叫之前寫好的resize方法即可。修改remove方法的程式碼如下:

/**
 * 從陣列中刪除index位置的元素,並返回刪除的元素
 *
 * @param index index
 * @return 被刪除的元素
 */
public E remove(int index) {
    checkIndex(index);

    E ret = data[index];
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }

    // 同樣的,這裡也可以直接使用陣列拷貝的函式來實現這個邏輯
    // System.arraycopy(data, index + 1, data, index, size - index + 1);

    size--;
    data[size] = null;

    if (size == data.length / 2) {
        resize(data.length / 2);
    }
    return ret;
}

專案原始碼的GitHub地址如下:

https://gitee.com/Zero-One/data_structure_learning