將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++; }
但在一些特殊的需求下,可能需要向指定的位置新增元素,例如陣列中的元素是有序的,我希望把新元素插入到合適的位置,以保證陣列內元素的有序性,如下圖:
在這種情況下,實現思路就是將索引為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的元素,如下圖:
其實這個刪除與我們之前實現的插入邏輯差不多,反過來就可以了。如圖中,我們需要刪除的索引為1的元素,只需要把索引為2的元素往左移一格,覆蓋索引為1的元素,然後索引為3的元素再往左移一格,覆蓋索引為2的元素,接著索引為4的元素再往左移一格,覆蓋索引為3的元素,以此類推...直到所有的元素都往左移一格後,將size-1即可。最後原本索引為1的元素已經被覆蓋掉了,陣列中不再有77這個元素,也就實現了刪除的邏輯,如下圖:
有的小夥伴可能會有疑問,最後陣列中多了一個元素,會有影響嗎?其實並不會有影響,因為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地址如下: