在上一節中,我們簡單闡述了Java的一些基礎知識,比如多態,接口的實現等。
然后,演示了ArrayList的幾個基本方法。
ArrayList是一個集合框架,它的底層其實就是一個數組,這一點,官方文檔已經說得很清楚了。
作為一個容器,ArrayList有添加元素,刪除元素,以及獲取元素的方法。
本節我們先不看ArrayLis底層的源碼,而是按照平常的思路來模擬一下ArrayList的具體實現。看看如果我們自己來寫的話,會怎么實現ArrayList的功能?
1. 新建一個MyList類
好的,我們來模擬一下ArrayList類,怎么模擬呢,是不是這樣就行了?
import java.util.ArrayList;
public class MyList extends ArrayList{
}
寫完了。
額,開個玩笑,別打我。。。
好的,讓我們開始吧。
這個MyList類,主要用來模擬一下ArrayList的基本方法,我們新建一個MyList 類
package jianshu;
public class MyList {
}
現在的MyList是不是啥也沒有啊,就好像一個新生的嬰兒一樣,純潔得像一張白紙。
我們現在需要給MyList添加一個新的身份。
添加身份,不就是實現接口或者繼承某個類么?
原先的ArrayList因為繼承了List接口,所以必須實現List接口所有的抽象方法,我們為了簡單起見,就不去實現List接口了。
我們來定義一個簡單的List接口,名字就叫做 SimpleList 吧。
里面定義幾個常用的抽象方法。
package jianshu;
/**
* 簡單的List接口
* @author 剽悍一小兔
*
*/
public interface SimpleList {
/**
* 添加元素
* @param obj
* @return boolean
*/
boolean add(Object obj);
/**
* 根據元素下標刪除元素
* @param index
*/
void remove(int index);
/**
* 根據元素下標獲取對應的元素
* @return Object
*/
Object get(int index);
/**
* 將當前的SimpleList轉換成Object數組
* @return Object[]
*/
Object[] toArray();
/**
* 獲取當前列表中元素的個數
* @return int
*/
int size();
}
現在,讓MyList實現這個接口。這步操作,就相當於給MyList添加一個新的身份。
因為MyList可以變身成為SimpleList,那么就必須擁有SimpleList的所有能力。
所以,我們是不是必須要實現SimpleList中所有的抽象方法呢?
package jianshu;
public class MyList implements SimpleList{
public boolean add(Object obj) {
return false;
}
public void remove(int index) {
}
public Object get(int index) {
return null;
}
public Object[] toArray() {
return null;
}
public int size() {
return 0;
}
}
接着,定義一個測試類,專門用來測試MyList
package jianshu;
public class TestMyList {
public static void main(String[] args) {
}
}
2. 構造函數設計
2.1 容器選型
我們完全按照ArrayList的規范來,打開api,發現其實ArrayList不止一個構造方法。
ArrayList有三個構造方法,分別為
ArrayList() --- 空構造方法。
ArrayList(Collection<? extends E> c) --- 傳入參數為一個Collection對象。
ArrayList(int initialCapacity) --- 傳入參數為一個int類型的數字,initialCapacity表示容量,在ArrayList被new出來的時候就規定一下初始容量是多少。
我們知道,Java在定義數組的時候,必須有一個長度。
比如:
Object[] objs = new Object[3];
這樣我就定義了一個長度為3的數組。
這個是顯示定義的。
當然還可以這樣:
Object[] objs = new Object[]{1,2,3};
雖然沒有明確指出數組的長度是多少,但是我們都知道它的長度就是3,這屬於隱式定義。
我們的MyList本身沒有存儲數據的能力,為了讓它具備這方面的能力,是不是要給他定義一個屬性啊。
一個Java類,無非就是屬性和方法,大部分情況下,方法無非就是用來給屬性賦值的。
屬性是干嘛用的,不就是用來存儲數據的嗎?
你說對不對呢?
Java有八種基本數據類型:
分別為整型 int,短整型 short,長整型 long,字節型 byte,布爾型 boolean,字符型 char,單精度浮點數 float,雙精度浮點數 double。
就比如說int,Java編譯器規定int占四個字節,也就是4個byte,一個字節占8位,一個位就是一個bit。
bit是計算機中最小的單位,它只有0和1兩種狀態。
我們常說一個文件有多少兆,這個兆就是MB,1MB有1024KB,1KB有1024個字節。
當你定義了一個int類型的變量,在運行的時候就會在Java虛擬機中申請一個4個字節的空間。
1MB有1024KB,1KB有1024個字節。所以說1MB可以存放 (1024 * 1024 / 4) = 262144 個int變量。
Java虛擬機的默認內存是64MB,所以最多應該能存放16777216個int類型的變量。
當你定義一個int類型的變量,那么運行的時候,虛擬機的剩余內存就會被減掉4個字節。
所以,屬性是干嘛用的,我們在寫Java類的時候,為什么要定義屬性。
我覺得沒有別的含義了,定義屬性就是為了存儲數據的嘛。
我們寫一個
private int a;
Java虛擬機(JVM)跑起來,一旦我們new了這個對象。
這個a變量就會被放到JVM的內存中,然后JVM就會專門開辟一個空間,來裝載這個數據。
然后,我們才可以在計算機中操作這些個數據。
你總不可能說,我有一個數字100,就要計算機對這個數字進行加減乘除的運算吧。
計算機怎么知道這個事情呢?
你是不是必須要告訴計算機有一個數字100,它才會知道?
為了裝載這些數據,所以才有了八種基本數據類型,每一個數據類型就好比一個籃子,有的籃子大一點,比如long類型,可以放好長好長的數字。有的籃子小一點,比如byte類型,只能放一點點大的數字。
言歸正傳,我們的 MyList 也需要像ArrayList那樣,可以add,也可以get。
那么,我們是不是必須要有一個屬性,用來儲存這些數據呢?
很顯然,Java給我們提供的8中基本數據類型都無法滿足這個需求。
接下來,我們想到,是不是可以定義一個數組,作為我們的容器呢?
數組,嚴格來說也是一個類,直接繼承自Object。我們不是可以通過new來生成一個數組對象嗎?而且數組擁有一個length屬性,這些證據都足以說明數組也是一個類。
為了驗證這一點,我們來做個實驗。
Object arr = new Object[12];
這樣寫,是不會報錯的,說明數組也擁有一個 Object 身份。這又是多態,我們懷疑數組是否繼承自Object,用多態的寫法去驗證一下就好了。
基本上確定下來了,我們就采用 Object 數組作為存儲數據的容器吧。
private Object[] elementData;
2.2 數組容量初始化
容器選型完畢后,開始着手設計構造函數。因為我們的底層采用數組來作為存儲數據的媒介,而數組這個東西,我們知道是要有一個初始容量的。
那么,我們是不是可以用構造函數的方式來給數組進行初始化呢?
public MyList(int initialCapacity){
this.elementData = new Object[initialCapacity];
}
這樣就行了。
為了看效果,我們需要有一個方法來獲取數組中的值,所以現在來改寫一個toString方法。
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < elementData.length; i++) {
sb.append(elementData[i].toString()).append(",");
}
//去掉最后一個逗號
String str = sb.toString().substring(0, sb.toString().length() - 1);
str += "]";
return str;
}
3. add方法實現
我們通過add方法來給數組添加數據
我們給數組添加元素的方式是這樣的:
Object[] arr = new Object[12];
arr[0] = "Hello";
arr[1] = "World";
System.out.println(arr[0]);
System.out.println(arr[1]);
可見,添加元素的時候,我們必須要知道它的下標。
為了方便,我們增加一個私有屬性size,來存儲當前數組中 實際存在 的元素個數。
private int size;
size默認是0
當我們調用add方法的時候,只需要動態地給 size + 1 就行了。
add方法初步實現:
/**
* 添加新的元素
*/
public boolean add(Object obj) {
elementData[size ++] = obj;
return true;
}
return true 代表添加成功。
測試:
SimpleList list = new MyList(3);
list.add("Hello");
list.add("World");
list.add("Java");
System.out.println(list);
可見,的確是成功添加進去了。
MyList的容量為3,我就添加了3個元素。如果我添加兩個呢?
SimpleList list = new MyList(3);
list.add("Hello");
list.add("World");
System.out.println(list);
一運行,報錯了:
Exception in thread "main" java.lang.NullPointerException
at jianshu.MyList.toString(MyList.java:52)
at java.lang.String.valueOf(String.java:2854)
at java.io.PrintStream.println(PrintStream.java:821)
at jianshu.TestMyList.main(TestMyList.java:12)
從錯誤信息中可以看出,我們的toString方法報錯了。
錯誤位置是第52行。
for (int i = 0; i < elementData.length; i++) {
sb.append(elementData[i].toString()).append(",");
}
這個for循環出了問題,原因很簡單,因為數組的長度有3個,而我們只添加了兩個元素。
也就是說,只有elementData[0] , 和elementData[1]有數據,
而elementData[2] = null
null.toString() 當然就報錯了。
來改一下咯,最先想到的應該是修改for循環中的elementData.length,顯然我們不應該用elementData的長度來作為總個數,要知道,elementData.length是數組的長度,而非數組中實際元素的個數。
所以,我們是不是應該要采用size屬性呢?
for (int i = 0; i < size; i++) {
sb.append(elementData[i].toString()).append(",");
}
再運行一次,
OK了。
4. remove方法實現
remove方法傳入一個int類型的數字,這個就是需要刪除的元素下標。
我們根據這個下標找到這個元素。當然,還得確保你傳入的參數不能小於0,也不能超過數組中實際元素的個數。
remove方法初稿:
/**
* 根據下標刪除元素
*/
public void remove(int index) {
if(index < 0 || index >= size){
throw new IndexOutOfBoundsException("您輸入的下標為:"+index+",而數組中最大的下標為:"+(size - 1));
}
elementData[index] = null;
}
測試一下,如果我們刪除下標為3的元素,看看會怎樣?
public class TestMyList {
public static void main(String[] args) {
SimpleList list = new MyList(3);
list.add("Hello");
list.add("World");
list.add("Java");
list.remove(3);
System.out.println(list);
}
}
報錯了。
Exception in thread "main" java.lang.IndexOutOfBoundsException: 您輸入的下標為:3,而數組中最大的下標為:2
at jianshu.MyList.remove(MyList.java:35)
at jianshu.TestMyList.main(TestMyList.java:13)
這個異常信息是我們自己定義的。
提示信息已經很清楚了,他說您輸入的下標為:3,而數組中最大的下標為:2
。
數組中元素個數是3,下標最大為2。
那我們傳一個 0 吧。
list.remove(0);
又報錯了:
Exception in thread "main" java.lang.NullPointerException
at jianshu.MyList.toString(MyList.java:58)
at java.lang.String.valueOf(String.java:2854)
at java.io.PrintStream.println(PrintStream.java:821)
at jianshu.TestMyList.main(TestMyList.java:15)
又是空指針,第58行。
它和上面add方法測試的時候報一樣的錯誤,錯誤代碼也一樣,也是那個for循環報錯。
for (int i = 0; i < size; i++) {
sb.append(elementData[i].toString()).append(",");
}
因為我們用remove方法刪除元素的時候,直接把那個元素賦一個null來達到刪除的目的,顯然這是不合理的。
那我們換一種思路,比如刪除Hello,能不能讓后面的元素全部往前移動一個位置呢?
比如我刪除Hello,然后數組就變成了這樣
然后讓size減一,表示數組中實際存在的元素個數 - 1。
因為我們的toString方法循環數組的時候,是根據size來的,所以哪怕最后一個位置是null,也不會被遍歷到。
好了,現在問題就演變為,我如何才能把要刪除的那個元素后面的所有元素,都左移一個單位呢?
方案已經確定了,剩下的就是如何實現的問題。
要是有一個數組拷貝的方法就好了。比如數組有三個元素,下標分別為0,1,2。
我們刪除下標為0的元素,只要把下標為1,2的元素拷貝到下標為0,1的地方,就可以了。
還真有這個方法,就是
System.arraycopy(src, srcPos, dest, destPos, length);
參數的含義(看了api,我反復琢磨以后,感覺這樣翻譯比較好)
src : 需要拷貝的數組。
srcPos : 從哪里開始 拷貝?
dest : 目標數組
destPos : 從哪里開始 粘貼?
length : 拷貝的元素個數
改進后的remove方法:
/**
* 根據下標刪除元素
*/
public void remove(int index) {
if(index < 0 || index >= size){
throw new IndexOutOfBoundsException("您輸入的下標為:"+index+",而數組中最大的下標為:"+(size - 1));
}
System.arraycopy(elementData, index + 1, elementData, index, elementData.length - index - 1 );
elementData[elementData.length - 1] = null;
size = size - 1;
}
稍微解釋一下這句話吧:
System.arraycopy(elementData, index + 1, elementData, index, elementData.length - index - 1 );
比如我要刪除下標為0的元素,那么需要拷貝的數組和目標數組都是elementData吧,這個沒問題。
index 等於 0 ,表示我要刪除下標為0的元素。那么接下來,我是不是要把這兩個元素都往左邊移動一個單位呀:
那么, 從哪里開始拷貝?
是不是從下標為1的地方開始拷貝?
也就是從World開始拷貝吧。(所以第二個參數是 index + 1)
粘貼到哪?
是不是從Hello的地方開始粘貼,也就是第0個元素。(所以第四個參數是 index)
拷貝多少個元素呢?
我們需要把Hello后面的兩個元素都拷貝一下,所以是2個元素。
可是我們不能直接寫一個2吧,remove方法可不知道你本來有多少個元素,所以這個地方需要動態計算一下。
即通過數組的最大下標減去需要刪除的元素下標。
其實最后一個參數應該寫成
(elementData.length - 1 ) - index
這樣可能比較好理解。
移動位置后,最后一個元素肯定還是之前的元素,所以我們需要把它賦空。然后size - 1,這樣toString的時候沒問題了。不然又會報空指針。
測試:
public class TestMyList {
public static void main(String[] args) {
SimpleList list = new MyList(3);
list.add("Hello");
list.add("World");
list.add("Java");
list.remove(0);
System.out.println(list);
}
}
如果我要把所有的元素都刪掉咋辦?
那就連續刪3次咯。
在這里我們需要把toString方法改一下:
如果 size 為 0,就直接返回一個 []
if(size == 0){
return "[]";
}
測試
public class TestMyList {
public static void main(String[] args) {
SimpleList list = new MyList(3);
list.add("Hello");
list.add("World");
list.add("Java");
list.remove(0);
list.remove(0);
list.remove(0);
System.out.println(list);
}
}
結果:
OK了。
未完待續。