【干貨】用大白話聊聊JavaSE — ArrayList 深入剖析和Java基礎知識詳解(二)


在上一節中,我們簡單闡述了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);

Paste_Image.png

可見,的確是成功添加進去了。

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行。

Paste_Image.png

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(",");
}

再運行一次,

Paste_Image.png

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

Paste_Image.png

數組中元素個數是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,然后數組就變成了這樣

Paste_Image.png

然后讓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的元素。那么接下來,我是不是要把這兩個元素都往左邊移動一個單位呀:

Paste_Image.png

那么, 從哪里開始拷貝?

是不是從下標為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);
	}
}

Paste_Image.png

如果我要把所有的元素都刪掉咋辦?

那就連續刪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);
	}
}

結果:

Paste_Image.png

OK了。

未完待續。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM