Java List詳解

2022-01-29 Java專欄

點擊上方「Java專欄」,選擇「置頂或者星標」

第一時間閱讀精彩文章!

2、☞ 《Java面試手冊》.PDF    點擊查看

為了清晰List的結構,我在這手工畫了一張圖,用於回顧下

1.List接口

首先看下List的官方定義

這段描述解決了許多公司經常問的兩個問題List有什麼特點和Set有什麼區別。上面清楚的說明了List是一個有序的集合,和set不同的是,List允許存儲項的值為空,也允許存儲相等值的存儲項,還舉了e1.equal(e2)的例子。

List是繼承於Collection接口,除了Collection通用的方法以外,擴展了部分只屬於List的方法。

從上圖可以發現,List比Collection主要多了幾個add(…)方法和remove(…)方法的重載,還有幾個index(…), get(…)方法。而AbstractList也只是實現了List接口部分的方法,和AbstractCollection是一個思路,這裡就不具體介紹了,有興趣的同學可以自行研究下。

2.ArraryList

ArrayList是一個數組實現的列表,由於數據是存入數組中的,所以它的特點也和數組一樣,查詢很快,但是中間部分的插入和刪除很慢。我們來看幾段關鍵的代碼。

首先是ArrayList的類關係和成員變量

//ArrayList繼承了Serializable並且申明了serialVersionUID,表示ArrayList是一個可序列化的對象,可以用Bundle傳遞

public class ArrayList<E> extends AbstractList<E>

implements List<E>, RandomAccess, Cloneable, java.io.Serializable

{

private static final long serialVersionUID = 8683452581122892189L;

/**

* Default initial capacity.

*/

private static final int DEFAULT_CAPACITY = 10;

/**

* Shared empty array instance used for empty instances.

*/

private static final Object[] EMPTY_ELEMENTDATA = {};

/**

* The array buffer into which the elements of the ArrayList are stored.

* The capacity of the ArrayList is the length of this array buffer. Any

* empty ArrayList with elementData == EMPTY_ELEMENTDATA will be expanded to

* DEFAULT_CAPACITY when the first element is added.

*/

//從這裡可以看到,ArrayList的底層是由數組實現的,並且默認數組的默認大小是10

private transient Object[] elementData;

/**

* The size of the ArrayList (the number of elements it contains).

*

* @serial

*/

private int size;

然後是構造函數

//ArrayList有2個構造函數,一個是默認無參的,一個是傳入數組大小的

//在JavaEffect書中明確提到,如果預先能知道或者估計所需數據項個數的,需要傳入initialCapacity

//因為如果使用無參的構造函數,會首先把EMPTY_ELEMENTDATA賦值給elementData

//然後根據插入個數於當前數組size比較,不停調用Arrays.copyOf()方法,擴展數組大小

//造成性能浪費

/**

* Constructs an empty list with the specified initial capacity.

*

* @param initialCapacity the initial capacity of the list

* @throws IllegalArgumentException if the specified initial capacity

* is negative

*/

public ArrayList(int initialCapacity) {

super();

if (initialCapacity < 0)

throw new IllegalArgumentException("Illegal Capacity: "+

initialCapacity);

this.elementData = new Object[initialCapacity];

}

/**

* Constructs an empty list with an initial capacity of ten.

*/

public ArrayList() {

super();

this.elementData = EMPTY_ELEMENTDATA;

}

然後是add()操作

//首先看到,不過是指定index執行add操作,還是在尾部執行add操作,都會先確認當前的數組空間是否夠插入數據

//並且從

//int oldCapacity = elementData.length;

//int newCapacity = oldCapacity + (oldCapacity >> 1);

//if (newCapacity - minCapacity < 0)

// newCapacity = minCapacity;

//看出,ArrayList默認每次都是自增50%的大小再和minCapacity比較,如果還是不夠,就把當的

//size擴充至minCapacity

//然後,如果是隊尾插入,也簡單,就把數組向後移動一位,然後賦值

//如果是在中間插入,需要用到System.arraycopy,把index開始所有數據向後移動一位

//再進行插入

/**

* Appends the specified element to the end of this list.

*

* @param e element to be appended to this list

* @return <tt>true</tt> (as specified by {@link Collection#add})

*/

public boolean add(E e) {

ensureCapacityInternal(size + 1); // Increments modCount!!

elementData[size++] = e;

return true;

}

/**

* Inserts the specified element at the specified position in this

* list. Shifts the element currently at that position (if any) and

* any subsequent elements to the right (adds one to their indices).

*

* @param index index at which the specified element is to be inserted

* @param element element to be inserted

* @throws IndexOutOfBoundsException {@inheritDoc}

*/

public void add(int index, E element) {

rangeCheckForAdd(index);

ensureCapacityInternal(size + 1); // Increments modCount!!

System.arraycopy(elementData, index, elementData, index + 1,

size - index);

elementData[index] = element;

size++;

}

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);

}

然後是remove操作

//個人感覺整個remove操作的代碼寫了很冗餘,不像甲骨文這些大神的風格

//首先來看remove(int index)

//先進行邊界確認,傳入的index是否超過了當前數組的大小,如果是拋出異常

//如果在數組範圍內,就把index之後的數據整體向前移動一位,最後一位值清空

//如果是remove(Object o),傳入的是一個對象,就會進行一次indexOf的操作,去當前數組中尋找

//判斷是否存在,這裡的代碼就十分冗餘了,就是把indexOf的代碼拷貝了一次,完全可以調用indexOf方法

//根據返回值是否為-1來判斷該值是否存在,如果存在就調用fastRemove方法

//fastRemove(int index)和remove(int index)方法除了邊界檢查一模一樣

//完全可以在remove調用完rangeCheck(index)後調用fastRemove就可以了

//這裡不是很明白設計者的意圖

/**

* Removes the element at the specified position in this list.

* Shifts any subsequent elements to the left (subtracts one from their

* indices).

*

* @param index the index of the element to be removed

* @return the element that was removed from the list

* @throws IndexOutOfBoundsException {@inheritDoc}

*/

public E remove(int index) {

rangeCheck(index);

modCount++;

E oldValue = elementData(index);

int numMoved = size - index - 1;

if (numMoved > 0)

System.arraycopy(elementData, index+1, elementData, index,

numMoved);

elementData[--size] = null; // clear to let GC do its work

return oldValue;

}

/**

* Removes the first occurrence of the specified element from this list,

* if it is present. If the list does not contain the element, it is

* unchanged. More formally, removes the element with the lowest index

* <tt>i</tt> such that

* <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>

* (if such an element exists). Returns <tt>true</tt> if this list

* contained the specified element (or equivalently, if this list

* changed as a result of the call).

*

* @param o element to be removed from this list, if present

* @return <tt>true</tt> if this list contained the specified element

*/

public boolean remove(Object o) {

if (o == null) {

for (int index = 0; index < size; index++)

if (elementData[index] == null) {

fastRemove(index);

return true;

}

} else {

for (int index = 0; index < size; index++)

if (o.equals(elementData[index])) {

fastRemove(index);

return true;

}

}

return false;

}

/*

* Private remove method that skips bounds checking and does not

* return the value removed.

*/

private void fastRemove(int index) {

modCount++;

int numMoved = size - index - 1;

if (numMoved > 0)

System.arraycopy(elementData, index+1, elementData, index,

numMoved);

elementData[--size] = null; // clear to let GC do its work

}

indexof

//這裡就和上面remove尋找是一模一樣的,就不進行探討了

public int indexOf(Object o) {

if (o == null) {

for (int i = 0; i < size; i++)

if (elementData[i]==null)

return i;

} else {

for (int i = 0; i < size; i++)

if (o.equals(elementData[i]))

return i;

}

return -1;

}

3.Vector

Vector就是ArrayList的線程安全版,它的方法前都加了synchronized鎖,其他實現邏輯都相同。如果對線程安全要求不高的話,可以選擇ArrayList,畢竟synchronized也很耗性能

4.LinkedList

故名思意就是鍊表,和我們大學在數據結構裡學的鍊表是一會事,LinkedList還是一個雙向鍊表。

LinkedList繼承於AbstractSequentialList,和ArrayList一個套路。內部維護了3個成員變量,一個是當前鍊表的頭節點,一個是尾部節點,還有是鍊表長度。然後我們在來看下Node這個數據結構。

和C語言實現方式差不多,由於是雙向鍊表,所以記錄了next和prev,只不過把C語言裡的指針換成了對象。

然後我們簡單的在來看下鍊表額度查詢,插入和刪除操作

首先是add(E e)操作

//學過數據結構的同學看這部分代碼特別輕鬆

//首先來看下void linkLast(E e),尾部插入

//就是把newNode的前面節點執行現在的尾部節點,newNode的後面節點執行null,因為是在尾部嘛

//然後把現在的尾部節點的後面節點指向newNode,因為現在的尾部節點不是最後一個了

//然後再來看下中間插入

//也是一個套路。假設現在在3號位插入一個newNode

//就是通過現在的3號Node的prev找到2號節點,然後修改2號節點的next,指向nowNode

//然後nowNode的prev指向2號節點,next指向3號節點

//最後3號節點的prev變成了nowNode,next不變

//這樣就完成了一次中間插入

/**

* Inserts the specified element at the specified position in this list.

* Shifts the element currently at that position (if any) and any

* subsequent elements to the right (adds one to their indices).

*

* @param index index at which the specified element is to be inserted

* @param element element to be inserted

* @throws IndexOutOfBoundsException {@inheritDoc}

*/

public void add(int index, E element) {

checkPositionIndex(index);

if (index == size)

linkLast(element);

else

linkBefore(element, node(index));

}

/**

* Appends the specified element to the end of this list.

*

* <p>This method is equivalent to {@link #addLast}.

*

* @param e element to be appended to this list

* @return {@code true} (as specified by {@link Collection#add})

*/

public boolean add(E e) {

linkLast(e);

return true;

}

/**

* Links e as last element.

*/

void linkLast(E e) {

final Node<E> l = last;

final Node<E> newNode = new Node<>(l, e, null);

last = newNode;

if (l == null)

first = newNode;

else

l.next = newNode;

size++;

modCount++;

}

/**

* Inserts element e before non-null Node succ.

*/

void linkBefore(E e, Node<E> succ) {

// assert succ != null;

final Node<E> pred = succ.prev;

final Node<E> newNode = new Node<>(pred, e, succ);

succ.prev = newNode;

if (pred == null)

first = newNode;

else

pred.next = newNode;

size++;

modCount++;

}

然後是void linkLast(E e)操作

//indexOf操作非常簡單,就是從頭開始遍歷整個鍊表,如果沒有就反-1,有就返回當前下標

/**

* Returns the index of the first occurrence of the specified element

* in this list, or -1 if this list does not contain the element.

* More formally, returns the lowest index {@code i} such that

* <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,

* or -1 if there is no such index.

*

* @param o element to search for

* @return the index of the first occurrence of the specified element in

* this list, or -1 if this list does not contain the element

*/

public int indexOf(Object o) {

int index = 0;

if (o == null) {

for (Node<E> x = first; x != null; x = x.next) {

if (x.item == null)

return index;

index++;

}

} else {

for (Node<E> x = first; x != null; x = x.next) {

if (o.equals(x.item))

return index;

index++;

}

}

return -1;

}

雖然indexOf非常簡單,但是我在這還是寫了個例子,幫助大家理解下

List list = new ArrayList();

list.add("zero");

list.add(null);

list.add("two");

list.add(null);

list.add("three");

Log.i("test", "index : " + list.indexOf(null));

在不看答案的情況下 大家能準確的說出答案嗎?

Answer:I/test: index : 1

從這個例子可以看出三點List的特徵1.是按順序查找2.允許存儲項為空3.允許多個存儲項的值相等

最後看下remove操作

//如果直接調無參的remove(),就會默認刪除頭節點

//刪除頭節點非常簡單,就是把頭節點的值清空,next清空

//然後把nextNode只為頭節點,然後清空next的prev

//最後size減1

//如果是刪除中間節點,調用remove(int index)

//首先判斷Index對應的節點是否為頭節點,即index是否為0

//如果不是中間節點,就是x的prev指向x的next

public E remove() {

return removeFirst();

}

public E remove(int index) {

checkElementIndex(index);

return unlink(node(index));

}

public E removeFirst() {

final Node<E> f = first;

if (f == null)

throw new NoSuchElementException();

return unlinkFirst(f);

}

private E unlinkLast(Node<E> l) {

// assert l == last && l != null;

final E element = l.item;

final Node<E> prev = l.prev;

l.item = null;

l.prev = null; // help GC

last = prev;

if (prev == null)

first = null;

else

prev.next = null;

size--;

modCount++;

return element;

}

/**

* Unlinks non-null node x.

*/

E unlink(Node<E> x) {

// assert x != null;

final E element = x.item;

final Node<E> next = x.next;

final Node<E> prev = x.prev;

if (prev == null) {

first = next;

} else {

prev.next = next;

x.prev = null;

}

if (next == null) {

last = prev;

} else {

next.prev = prev;

x.next = null;

}

x.item = null;

size--;

modCount++;

return element;

}

/**

* Unlinks non-null first node f.

*/

private E unlinkFirst(Node<E> f) {

// assert f == first && f != null;

final E element = f.item;

final Node<E> next = f.next;

f.item = null;

f.next = null; // help GC

first = next;

if (next == null)

last = null;

else

next.prev = null;

size--;

modCount++;

return element;

}

總結

通過上面對ArrayList和LinkedList的分析,可以理解List的3個特性1.是按順序查找2.允許存儲項為空3.允許多個存儲項的值相等可以知其然知其所以然

然後對比LinkedList和ArrayList的實現方式不同,可以在不同的場景下使用不同的ListArrayList是由數組實現的,方便查找,返回數組下標對應的值即可,適用於多查找的場景LinkedList由鍊表實現,插入和刪除方便,適用於多次數據替換的場景

以上,便是今天的分享,希望大家喜歡,覺得內容不錯的,歡迎點擊「在看」支持,謝謝各位

喜歡文章,點個在看 

相關焦點

  • Java 數組轉 List 的三種方式及對比
    strArray);  //對轉換後的list插入一條數據  list.add("1");  System.out.println(list); }Exception in thread "main" java.lang.UnsupportedOperationException
  • Java | 第一個 SpringBoot 工程詳解
    廢話少說,今天接著前文給你們帶來了第一個 SpringBoot 工程的詳解。第一個 SpringBoot 工程前文已經說過了 SpringBoot 工程的創建,這裡不再贅述,還不會的朋友,請看下面這篇文章。
  • Java中List排序的3種方法!
    來源 | Java中文社群(ID:javacn666);        // 列印 list 集合        list.forEach(p -> {            System.out.println(p);        });    }}//  以下 set/get/toString 使用的是 lombok 的註解@Getter@Setter
  • Java開發者易犯錯誤Top10
    本文總結了Java開發者經常會犯的前十種錯誤列表。在一個循環中從一個列表裡刪除一個元素考慮下面刪除元素的代碼在迭代中的結果:[java]ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d")); for (int i = 0; i < list.size
  • Java 8—Java 10 特性詳解
    Java 8 特性詳解知識體系函數編程在Java世界裡面,面向對象還是主流思想,對於習慣了面向對象編程的開發者來說,抽象的概念並不陌生。面向對象編程是對數據進行抽象,而函數式編程是對行為進行抽象。現實世界中,數據和行為並存,程序也是如此,因此這兩種編程方式我們都得學。
  • java集合【6】——— Iterable接口
    內部定義的方法 java集合最源頭的接口,實現這個接口的作用主要是集合對象可以通過迭代器去遍歷每一個元素。 = new ArrayList();        list.add("Jam");        list.add("Jane");        list.add("Sam");        Iterator<String> iterator = list.iterator();        Iterator var2 = list.iterator
  • List 去除重複數據的五種方式,舒服~
    import java.util.ArrayList;import java.util.Arrays;import java.util.LinkedHashSet;public class ArrayListExample{    public static void main(String[] args)
  • 每日一課 | Java 中如何將 ArrayList 與 HashSet 互相轉換?
    Set set = new HashSet(list);List list = new ArrayList(set);1.列出示例import java.util.ArrayList;import java.util.HashSet;import java.util.List
  • 最通俗易懂的 Java 10 新特性講解 | 原力計劃
    查看自己的 Java 10 版本。主要增加了下面幾個擴展方法。java.time.temporal.WeekFields::ofjava.util.Calendar::{getFirstDayOfWeek,getMinimalDaysInWeek}java.util.Currency::getInstancejava.util.Locale::getDisplayNamejava.util.spi.LocaleNameProvider
  • Java中的Set、List、Map的用法與區別
    最基本的兩種檢索集合中的所有對象的方法:1:for循環和get()方法: for(int i=0; i<list.size();i++){ System.out.println(list.get(i)); } 2:
  • java集合詳解合集
    所以的集合類都位於java.util包下,後來為了處理多線程環境下的並發安全問題,java5還在java.util.concurrent包下提供了一些多線程支持的集合類。在學習Java中的集合類的API、編程原理的時候,我們一定要明白,"集合"是一個很古老的數學概念,它遠遠早於Java的出現。
  • 這些 Java 8 官方挖的坑,你踩過幾個?
    Caused by: java.lang.IllegalArgumentException: Illegal base64 character 3f    at java.util.Base64$Decoder.decode0(Base64.java:714)    at java.util.Base64$Decoder.decode
  • 耿老師教你學Java:求最大(最小)連接數
    (Number n) {        String strTwo = ""+n.number;        String strOne =""+number;        int one =Integer.parseInt(strOne.concat(strTwo));        int two =Integer.parseInt(strTwo.concat(strOne));import ja
  • Java 中的異常和處理詳解
    : / by zero     at com.example.AllDemo.devide( AllDemo.java:30 )     at com.example.AllDemo.CMDCalculate( AllDemo.java:22 )     at com.example.AllDemo.main( AllDemo.java:12 )
  • Java集合深入理解:List(藍瘦,香菇,這些我怎麼才知道!)
    The user can access elements by their integer index (position in the list), and search for elements in the list.可以看到,List 接口的實現類在實現插入元素時,都會根據索引進行排列。比如 ArrayList,本質是一個數組:
  • 還不會Java SPI機制?學!
    ├─LoggerService.java                ├─Main.java                └─MyServicesLoader.java新建 Logger 接口,這個就是 SPI , 服務提供者接口,後面的服務提供者就要針對這個接口進行實現
  • Java Set集合的詳解
    如果對兩個引用調用hashCode方法,會得到相同的結果,如果對象所屬的類沒有覆蓋Object的hashCode方法的話,hashCode會返回每個對象特有的序號(java是依據對象的內存地址計算出的此序號),所以兩個不同的對象的hashCode值是不可能相等的。
  • Java8 快速實現List轉map 、分組、過濾等操作
    collect(Collectors.minBy(Comparator.comparing(Dish::getCalories)));minDish.ifPresent(System.out::println);6、去重import static java.util.Comparator.comparingLong
  • Java程式設計師必會的工具庫,讓你的代碼量減少90%!
    # Java自帶工具方法1.1 List集合拼接成以逗號分隔的字符串// 如何把list集合拼接成以逗號分隔的字符串 a,b,c List<String> list = Arrays.asList("a", "b", "c"); // 第一種方法,可以用stream流 String join = list.stream
  • JAVA泛型通配符T,E,K,V區別,T以及Class,Class的區別
    test.list.add("hello");        System.out.println(test.list);    }}和public class Test<A> {      public List<A> list = new ArrayList<A>();