實現ArrayList與LinkedList
前言
在平時的開發過程中我們會使用到許多的資料結構,其中表也許是使用最多的一種。明白Collections
容器的朋友一定都是使用過其中的List
容器。這裡我將通過建立自己的List
來說明表的原始碼實現。
這裡主要實現兩個庫類重要子集ArrayList
和LinkedList
的程式碼。
區別
ArrayList
和LinkedList
分別是表的兩種實現方式。各自有著各自的優點和缺點。我們都知道ArrayList
的底層是基於陣列的方式實現,而LinkedList
則是基於連結串列的方式。
ArrayList
ArrayList
是通過陣列來實現的一種資料結構,它由固定容量建立的一個數組,但是在需要的時候可以用雙倍容量建立一個不同的陣列。
優點:當需要對其中的元素進行索引時只需要花費常數時間。
缺點:當需要新增和刪除元素時最壞情況下的時間複雜度為O(N)。這是由於在新增或者刪除時需要移動陣列後面的所有元素。但是,如果需要新增或是刪除操作的元素位於表的高階時,則只花費O(1)的時間。
補充:當我們需要對一個數組進行擴容時,這將會是一件十分複雜的事情。
int[] oldArr = new int[10]; int[] newArr = new int[oldArr.length * 2]; //擴充套件後的陣列 for(int i=0;i<oldArr.length;i++){ newArr[i] = oldArr[i]; } oldArr = newArr;
LinkedList
LinkedList
是由一系列結點組成,這些結點在記憶體中不需要連續,每一個結點都包含一個表元素(資料),以及到該元素後面一個結點的鏈(稱為next鏈)。最後一個結點的next鏈引用為null。
但是通常我們會使用到雙鏈表。雙鏈表就是在簡單的連結串列上給每個結點加上一個到它前面一個結點的。
優點:當需要進行新增或是刪除操作時只需要花費線性時間。當我們需要新增或是刪除節點時,只需要修改該節點的前後節點鏈即可。
缺點:當需要進行索引時最壞情況需要花費O(N)的時間複雜度。因為它需要遍歷整個連結串列。
Collections介面
Collections API位於java.util
Collections
介面擴充套件了Iterable
介面,實現了Iterable
介面的類可以擁有增強的for迴圈。
public interface Collection<T> extends Iterable<T>{
int size();
boolean isEmpty();
void clear();
boolean contains(T item);
boolean add(T item);
boolean remove(T item);
Iterator<T> iterator();
}
實現Iterable
介面的集合必須提供一個iterator
方法,該方法返回一個Iterator
型別的物件。在這個物件中會記錄當前元素的位置。當我們使用迭代器遍歷集合元素時就需要使用到該方法。
public interface Iterator<E>{
boolean hasNext();
E next();
void remove();
}
在iterator
介面中有三個方法用於我們迭代元素。其中hasNext
用來告訴是否存在下一項,next
將會返回當前位置的元素,並隨後將當前位置後移,remove
可用於移除元素。
這裡需要強調的是,我們應該避免使用Collection
中的remove
方法,而儘量使用iterator
中的remove
方法。因為當我們使用Collection
中的remove
方法時需要重新去索引元素的位置,而iterator
自身儲存了當前元素的位置,因而可以減少索引時間上的開銷。
下面是迭代器的使用方法:
public static <T> void print(Collection<T> coll){
Iterator<T> iterator = coll.iterator();
while (iterator.hasNext()){
T item = iterator.next();
System.out.println(item);
}
}
ArrayList 類的實現
這裡我將編寫ArrayList
類的底層實現,為了避免與java.util
中的類庫相混,這裡我將類命名成ArrayListTest
。
在我們編寫的ArrayListTest
類中有以下幾處細節:
ArrayListTest
將以陣列為底層實現,並且將陣列的初始大小設定為10,以及該類擁有當前陣列中元素的項數size。- 在該類中我們使用
ensureCapacity
方法來對陣列進行擴容,通過獲得一個新陣列,並將老陣列拷貝到新陣列中改變陣列的容量,允許虛擬機器回收老陣列。 ArrayListTest
類提供get
、set
等實現。ArrayListTest
類提供容器基本使用方法。如size()
、isEmpty()
和clear()
方法,以及remove()
和add()
對元素進行操作。- 我們在這裡實現了
Iterator
介面的類,並重寫了其中的hasNext
、next
和remove
方法
ArrayListTest
類的完整程式碼如下:
public class ArrayListTest<T> implements Iterable<T>{
private static final int DEFAULT_CAPACITY = 10;
private int size;
private T[] items;
public ArrayListTest(){
doClear();
}
public void clear(){
doClear();
}
public int size(){
return size;
}
private void doClear(){
size = 0;
ensureCapacity(DEFAULT_CAPACITY);
}
public void ensureCapacity(int newSize){
if(newSize<size){
return;
}
T[] old = items;
items = (T[]) new Object[newSize];
for(int i=0;i<size();i++){
items[i] = old[i];
}
}
public boolean add(T x){
add(size(), x);
return true;
}
public boolean isEmpty(){
return size() == 0;
}
public T get(int index){
if(index < 0 || index >= size){
throw new IndexOutOfBoundsException();
}
return items[index];
}
public T set(int index, T x){
if(index < 0 || index >= size){
throw new IndexOutOfBoundsException();
}
T old = items[index];
items[index] = x;
return old;
}
public void add(int id, T x){
if(items.length == size){
ensureCapacity(size() * 2 +1);
}
for(int i=size;i>id;i--){
items[i] = items[i-1];
}
items[id] = x;
size++;
}
public T remove(int index){
T removeItem = items[index];
int i=index;
while (i<size()-1){
items[i] = items[i+1];
i++;
}
items[i] = null;
size--;
return removeItem;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder("ArrayListTest{items=[");
int i=0;
while (i<size()){
if(items[i] != null){
str.append(items[i]).append(",");
} else {
break;
}
i++;
}
if(isEmpty()){
return str + "]}";
}
return str.substring(0,str.length()-1) + "]}";
}
@Override
public Iterator iterator() {
return new ArrayListIterator();
}
private class ArrayListIterator implements Iterator{
private int current = 0;
@Override
public boolean hasNext() {
return current<size();
}
@Override
public T next() {
if(!hasNext()){
throw new NoSuchElementException();
}
return items[current++];
}
@Override
public void remove() {
ArrayListTest.this.remove(--current);
}
}
}
在其中33行處可以看到這樣一行語句items = (T[]) new Object[newSize]
,因為泛型陣列的建立是非法的,我們的做法是建立了一個Object
型別的陣列,然後通過泛型進行強制轉換。這將產生一個編譯器警告,但是泛型集合的實現中這是不可避免的。
在其中的add
方法中,如果新增元素達到了當前陣列的容量大小後就需要對陣列進行擴容,由於擴充容量的代價是非常昂貴的,於是在這裡我將對擴容後的大小設定為原來大小的兩倍,以避免頻繁擴容帶來的代價。
下面是對於ArrayListTest
類的測試:
public class Main {
public static void main(String[] args) {
ArrayListTest<Integer> arrayList = new ArrayListTest<Integer>();
for(int i=0;i<10;i++){
arrayList.add(i);
}
System.out.println("ArrayListTest:" + arrayList);
arrayList.remove(1);
System.out.println("remove:" + arrayList);
arrayList.set(1,100);
System.out.println("set:" + arrayList);
System.out.println("get:" + arrayList.get(1));
arrayList.clear();
System.out.println("clear:" + arrayList);
}
輸出結果如下:
ArrayListTest:ArrayListTest{items=[0,1,2,3,4,5,6,7,8,9]}
remove:ArrayListTest{items=[0,2,3,4,5,6,7,8,9]}
set:ArrayListTest{items=[0,100,3,4,5,6,7,8,9]}
get:100
clear:ArrayListTest{items=[]}
LinkedList 類的實現
LinkedList
的實現相對於ArrayList
的實現要過於複雜一點。由於它是將雙鏈表作為實現,而且還需要儲存連結串列兩端的引用。同ArrayList
一樣,為了避免與類庫中的類相混,這裡我將類命名成LinkedListTest
。
LinkedListTest
類有以下細節需要注意:
- 由於底層實現是連結串列形式,因此我們需要建立一個
Node
類作為連結串列中的每個結點。該節點包含資料以及到前一個結點的鏈(next鏈)和到後一個結點的鏈(previous鏈)。 - 在該實現中我們需要建立兩個空節點作為連結串列的兩端。
- 這裡仍然包含大多數操作方法,如
add
、set
、get
等等。 - 在這裡實現的
iterator
介面中,我們會對每次add
、remove
等操作過程進行監控,如果預期的modCount和實際的modCount不一樣,則在迭代中將丟擲異常。
LinkedListTest
類的完整程式碼如下:
public class LinkedListTest<T> implements Iterable {
private int size;
private int modCount = 0;
private Node<T> beginNode;
private Node<T> endNode;
//結點類
private class Node<T> {
public T data;
public Node<T> previous;
public Node<T> next;
public Node(T data, Node<T> p, Node<T> n) {
this.data = data;
this.previous = p;
this.next = n;
}
}
public LinkedListTest() {
doClear();
}
public void doClear() {
clear();
}
public void clear() {
beginNode = new Node<T>(null, null, null);
endNode = new Node<T>(null, beginNode, null);
beginNode.next = endNode;
size = 0;
modCount++;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size() == 0;
}
public boolean add(T x) {
add(size(), x);
return true;
}
public void add(int index, T x) {
addBefore(getNode(index, 0, size()), x);
}
public T get(int index) {
return getNode(index).data;
}
public T set(int index, T newVal) {
Node<T> node = getNode(index);
T oldData = node.data;
node.data = newVal;
return oldData;
}
public T remove(int index) {
return remove(getNode(index));
}
public T remove(Node<T> node) {
node.next = node.previous.next;
node.previous = node.next.previous;
size--;
modCount++;
return node.data;
}
//在thisNode結點前插入一個結點
private void addBefore(Node<T> thisNode, T x) {
Node<T> newNode = new Node<T>(x, thisNode.previous, thisNode);
newNode.previous.next = newNode;
thisNode.previous = newNode;
size++;
modCount++;
}
private Node<T> getNode(int index) {
return getNode(index, 0, size() - 1);
}
private Node<T> getNode(int index, int lower, int upper) {
Node<T> thisNode;
if (index < lower || index > upper) {
throw new IndexOutOfBoundsException();
}
if (index < size() / 2) {
thisNode = beginNode.next;
for (int i = 0; i < index; i++) {
thisNode = thisNode.next;
}
} else {
thisNode = endNode;
for (int i = size(); i > index; i--) {
thisNode = thisNode.previous;
}
}
return thisNode;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder("LinkedListTest{item=");
for (int i = 0; i < size; i++) {
str.append(get(i)).append(",");
}
return str.substring(0, str.length() - 1) + "}";
}
@Override
public Iterator iterator() {
return new LinkedListIterator();
}
private class LinkedListIterator implements Iterator {
private Node<T> current = beginNode.next;
private int expectedModCount = modCount;
private boolean okToRemove = false;
@Override
public boolean hasNext() {
return current != endNode;
}
@Override
public Object next() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
if (!hasNext()) {
throw new NoSuchElementException();
}
T data = current.data;
current = current.next;
okToRemove = true;
return data;
}
@Override
public void remove() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
if (!okToRemove) {
throw new IllegalStateException();
}
LinkedListTest.this.remove(current.previous);
expectedModCount++;
okToRemove = false;
}
}
}
在這個示例中,每次我們建立一個LinkedListTest
時就會首先建立兩個空節點beginNode
和endNode
,並將beginNode
的下一個結點與endNode
相連。當每次進行新增元素時,只需要修改前後兩個結點的鏈之間的關係即可,如81-87行程式碼所示。
在其中有個getNode
方法,該方法會對需要索引的元素位置進行判斷,如果該位置位於連結串列的前半部分,則從表的前端進行索引,否則從尾部進行索引。
在實現的Iterator
中,LinkedListIterator
使用到類似於ArrayListIterator
的邏輯。在該迭代器中仍保留著一個當前位置current,它表示當前連結串列中的第一個元素。注意,當current被定位到endNode時,對next的呼叫時非法的。
為了防止在迭代過程期間集合元素被修改,我們在迭代器中將對modCount進行判斷,每次進行add
、remove
等方法時都會對modCount進行加一,因此我們只需要在迭代過程中判斷modCount和expectedModCount的值是否相同,就可保證迭代過程中集合元素的完整性。
下面是對於LinkedListTest
類的簡單測試:
public class Main {
public static void main(String[] args) {
LinkedListTest<String> linkedList = new LinkedListTest<String>();
linkedList.add("111");
linkedList.add("222");
linkedList.add("333");
System.out.println(linkedList);
}
}
嘿嘿,輸出結果如下:
LinkedListTest{item=111,222,333}
總結
對於平常使用集合的過程中,我們並不會發現許多的細節,也只有當我們深入瞭解了集合的底層實現後才能更好、更正確的使用集合類。以上就是我對於表的兩種基本實現的總結。