1. 程式人生 > >java集合介面 -- List

java集合介面 -- List

參考連結:Java基礎——集合原始碼解析 List List 介面

加入部分自己的見解,並做了若干調整

UML圖:

 

List 是一個介面,定義了一組元素是有序的、可重複的集合。

List 繼承自 Collection,較之 Collection,List 還添加了以下操作方法

  • 位置相關:List 的元素是有序的,因此有get(index)、set(index,object)、add(index,object)、remove(index) 方法。
  • 搜尋:indexOf(),lastIndexOf();
  • 迭代:使用 Iterator 的功能板迭代器
  • 範圍性操作:使用 subList 方法對 list 進行任意範圍操作。

List的抽象實現類 AbstractList

AbstractList 繼承自 AbstractCollection 類,實現了 List 介面。整個類的設計類似於AbstractCollection,實現了大多數方法,抽象了對於需要根據資料操作的方法。

List 的實現類

ArrayList

ArrayList 是我們最常用的一個類,它具有如下特點:

  • 容量不固定,可以動態擴容
  • 有序(基於陣列的實現,當然有序~~)
  • 元素可以為 null
  • 效率高
    • 查詢操作的時間複雜度是 O(1)
    • 增刪操作的時間複雜度是 O(n)
    • 其他操作基本也都是 O(n)
  • 佔用空間少,相比 LinkedList,不用佔用額外空間維護表結構

從成員變數,我們可以得知

  • Object[] elementData:資料結構---陣列
  • 兩個預設空陣列,僅在構造方法中使用,不關心   注:jdk1.7中 ,沒有找到DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • DEFAULT_CAPACITY: 陣列初始容量為10
  • size:當前元素個數
  • MAX_ARRAY_SIZE:陣列最大容量   指定為 Integer.MAX_VALUE - 8,但是實際最大容量可能大於這個值,具體可以參考hugeCapacity()方法

現在我們知道了 ArrayList 其實就是基於陣列的實現。因此,增刪改查操作就變得很容易理解了。

  • get(index)直接獲取陣列的底 index 個元素
  • set(index,object)直接修改陣列的第 index 個元素的引用
  • add(index,object)新增一個元素到index,這裡會牽涉到陣列的擴容,擴容我們後面再單獨看

這裡的操作很簡單,比如說含有8個元素的陣列array,要在第五個位置插入一個元素x,則將第[5,8)角標的元素分別往後移動一位變成[6,9),此時角標為5的位置空出來,使 array[5] = x 即可。

移動時,使用了System.arraycopy(elementData, index, elementData, index + 1, size - index);

  • remove(object)刪除一個元素,刪除的過程同新增元素。

現在我們來看看擴容機制,假設我們現在有一個集合 list,裡面正好含有10個元素,此時,我們呼叫 add(object)方法新增一個元素,看看是怎樣執行擴容操作的。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

//將原文中的程式碼換為1.7中的程式碼,更容易理解

public boolean add(E e) {

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

 

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

 

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

 

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

從上面我們可以看到,修改某個角標的值或者查詢某個角標的值時,我們可以直接呼叫陣列的操作,效率很高。但是新增和刪除則是要操作整個陣列的移動,效率稍低。

這裡我們也可以看到,其實 ArrayList 就是一個針對陣列操作的封裝類。

LinkedList

剛剛我們看了 ArrayList,ArrayList 的增刪操作效率相對較低。因此,Java 給我們設計了另外一種增刪效率比較高的集合 LinkedList。

LinkedList 繼承自 AbstractSequentialList。

AbstractSequentialList 又繼承自AbstractList,並且基於 iterator 實現了預設增刪改查操作。

再回過頭來看 LinkedList,LinkedList 還實現了Deque(雙向佇列)介面,雙向佇列後面我們會單獨去學習,這裡不再做過多的贅述。

再來看看成員變數~~

  • size 連結串列元素個數
  • first 第一個元素
  • last 最後一個元素

getFirst(),getLast為LinkedList自有的public方法,為null時會丟擲 java.util.NoSuchElementException

* get(index)方法很有意思,先要判斷index是否為合法範圍(大於零且小於size),然後判斷index是否大於size的一半,如果小於則從first找起,否則,從list找起。

特點:和 ArrayList 的優缺點互補。

連結串列的實現,連結串列的實現很簡單,就是最基本的連結串列資料結構。理解連結串列資料結構可以跳過這裡了。

我舉個例子吧,現在要讓 a、b、c、d 四個同學按順序投籃。有兩種方法,第一種是 abcd 排成一個隊伍,依次去投籃;但是體育課上讓所有的同學等著投籃很浪費時間,因此有了第二種辦法:依次告訴 a‘你投了藍之後叫 b 來投籃’,告訴 b‘你投了藍之後叫 c 來投籃’以此類推。
這樣,就形成了一個簡單的單向列表,abcd 按照順序依次去投籃。此時,x 同學由於身體不舒服需要提前投籃回教室休息,則老師只需要告訴 a 同學投完籃之後不用叫 b 同學了,改叫 x 同學,x 同學投完籃之後叫 b 同學即可。
不多說了,新手聽不懂,老手用不上。不懂連結串列的同學好好去學學資料結構吧。

後面的增刪改查操作就只是基礎的遍歷連結串列操作,就不去一一去讀原始碼了,連結串列操作記不太清楚的同學可以去看一下 LinkedList 的原始碼。

Vector

在學習了 ArrayList 之後,Vector 這個類我想用”執行緒安全的 ArrayList“可以一句話概括了。

/*

Vector是Java早期提供的執行緒安全的動態陣列,如果不需要執行緒安全,則不建議選擇,畢竟通不是有額外開銷的。

與ArrayList的另一個區別是,擴容時,Vector是增長一倍,而ArrayList是50%

最好可以對比 java.util.concurrent.CopyOnWriteArrayList。

*/

Vector 和 ArrayList 一樣都繼承自 AbstractList,為什麼說”Vector 是執行緒安全的 ArrayList“,本來還準備列個表讓大家對比一下成員變數以及主要操作方法的實現。but,除了 Vector 的方法上多了個 synchronized 外,程式碼都是一樣的,比較個毛。因此,如果需要考慮執行緒安全,直接使用 Vector 即可,但是因為所有方法都加了synchronized ,效率相對會比較低,如果沒有執行緒安全的需求,那就使用 ArrayList 唄。

最後,還是說一下Vector 和 ArrayList的區別吧,反正也沒什麼卵用,大家看看就好

  • Vector 出生早,JDK1.0的時候出生的,ArrayList 是1.2才出生
  • Vector 是執行緒安全的,ArrayList 不是。(這是最大的特點了)
  • Vector 預設擴容2倍,ArrayList 是1.5倍。這個~~有意義嗎?
  • Vector 多了一種迭代器 Enumeration。好吧,反正我沒用過。

Enumeration

為了找到 Enumeration 這種迭代器有什麼特點,我去翻了一下 Vector 的程式碼,找到了一個這樣的方法和這樣的介面,你們感受一下。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

public Enumeration<E> elements() {

    return new Enumeration() {

        int count = 0;

        public boolean hasMoreElements() {

            return this.count < Vector.this.elementCount;

        }

        public E nextElement() {

            Vector var1 = Vector.this;

            synchronized(Vector.this) {//區別在這裡

                if(this.count < Vector.this.elementCount) {

                    return Vector.this.elementData(this.count++);

                }

            }

            throw new NoSuchElementException("Vector Enumeration");

        }

    };

}

 

public interface Enumeration<E> {

    boolean hasMoreElements();

 

    E nextElement();

}

mmp,這個 Enumeration 和Iterator 介面除了名字不一樣,還有什麼區別?然後仔細看了一遍,在elements()方法裡面的匿名內部了裡面找到了nextElement()方法裡面有個同步程式碼塊。好吧, Enumeration 大概是執行緒安全的Iterator?

Stack

Stack 繼承自Vector,也是一個執行緒安全的集合。

Stack 也是基於陣列實現的。

Stack 實現的是棧結構集合

什麼是棧結構?

資料結構中,棧也是一種線性的資料結構,遵守 LIFO(後進先出)的操作順序,這裡用一張很汙的圖,保證你們看了之後能熟記棧結構特徵。

小時候肯定都玩過羽毛球吧,羽毛球不經打,要經常換球,於是我買了一盒羽毛球,如下圖,就是一個羽毛球盒子,最先放進去的羽毛球(棧底的),要最後才能取出來。

Stack 的 程式碼實現

類結構圖如下,程式碼量也不多,一共才30幾行

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

public class Stack<E> extends Vector<E> {

    private static final long serialVersionUID = 1224463164541339165L;

 

    public Stack() {

    }

 

    //入棧,新增一個元素到陣列的最後一個

    public E push(E var1) {

        this.addElement(var1);

        return var1;

    }

    //出棧,刪除陣列最後一個元素並返回

    public synchronized E pop() {

        int var2 = this.size();

        Object var1 = this.peek();

        this.removeElementAt(var2 - 1);

        return var1;

    }

    //獲取最後一個元素,不刪除

    public synchronized E peek() {

        int var1 = this.size();

        if(var1 == 0) {

            throw new EmptyStackException();

        else {

            return this.elementAt(var1 - 1);

        }

    }

 

    public boolean empty() {

        return this.size() == 0;

    }

    獲取棧中的 位置。

    public synchronized int search(Object var1) {

        int var2 = this.lastIndexOf(var1);

        return var2 >= 0?this.size() - var2:-1;

    }

}

整個類的實現非常簡單,就是繼承 Vector,然後添加了 peek、pop、push、search 等方法,然後然新增刪除都在最末尾的元素做操作即可。

思考:怎樣用連結串列的結構快速實現 LinkedListStack?

/*

LinkedListStack(){

    super();

}

pop() {

return removeLast();

}

push(E){

return addLast(E);
}

peek(){

return getLast();

}

*/

關於執行緒安全

ArrayList和LinkedList都不是執行緒安全的(有些文章中提到,linkedList中使用了synchronized來保證執行緒安全,但是在jdk1.7的程式碼中並沒有。楊曉峰在極客時間的專欄中《第8講對比Vector、ArrayList、LinkedList有何區別?》中,也提及“它也不是執行緒安全的”)

jdk的原始碼註釋中有涉及:

<strong>Note that this implementation is not synchronized.</strong>
 If multiple threads access a linked list concurrently, and at least
 one of the threads modifies the list structurally, it <i>must</i> be
 synchronized externally.  (A structural modification is any operation
 that adds or deletes one or more elements; merely setting the value of
 an element is not a structural modification.)  This is typically
 accomplished by synchronizing on some object that naturally
 encapsulates the list.

Vector以及它的子類Stack是執行緒安全的。

如果需要執行緒安全的使用List

1.使用Collections工具類中提供的Synchronized方法:

  List  list = Collections.synchronizedList(List<T> list);

可以考慮使用java.util.CopyOnWriteArrayList (不太熟悉,後面需要進一步瞭解。)