- List
在集合類中,List
是最基礎的一種集合:它是一種有序列表,元素可重複。
List
的行爲和數組幾乎完全相同:List
內部按照放入元素的先後順序存放,每個元素都可以通過索引確定自己的位置,List
的索引和數組一樣,從0
開始。
數組和List
類似,也是有序結構,如果我們使用數組,在添加和刪除元素的時候,會非常不方便。例如,從一個已有的數組{'A', 'B', 'C', 'D', 'E'}
中刪除索引爲2
的元素:
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │
└───┴───┴───┴───┴───┴───┘
│ │
┌───┘ │
│ ┌───┘
│ │
▼ ▼
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ D │ E │ │ │
└───┴───┴───┴───┴───┴───┘
這個“刪除”操作實際上是把'C'
後面的元素依次往前挪一個位置,而“添加”操作實際上是把指定位置以後的元素都依次向後挪一個位置,騰出來的位置給新加的元素。這兩種操作,用數組實現非常麻煩。因此,在實際應用中,需要增刪元素的有序列表,我們使用最多的是ArrayList
。實際上,ArrayList
在內部使用了數組來存儲所有元素。例如,一個ArrayList
擁有5個元素,實際數組大小爲6
(即有一個空位):
size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │
└───┴───┴───┴───┴───┴───┘
當添加一個元素並指定索引到ArrayList
時,ArrayList
自動移動需要移動的元素:
size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘
然後,往內部指定索引的數組位置添加一個元素,然後把size
加1
:
size=6
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘
繼續添加元素,但是數組已滿,沒有空閒位置的時候,ArrayList
先創建一個更大的新數組,然後把舊數組的所有元素複製到新數組,緊接着用新數組取代舊數組:
size=6
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
現在,新數組就有了空位,可以繼續添加一個元素到數組末尾,同時size
加1
:
size=7
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │ G │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
可見,ArrayList
把添加和刪除的操作封裝起來,讓我們操作List
類似於操作數組,卻不用關心內部元素如何移動。
我們考察List<E>
接口,可以看到幾個主要的接口方法:
- 在末尾添加一個元素:
void add(E e)
- 在指定索引添加一個元素:
void add(int index, E e)
- 刪除指定索引的元素:
int remove(int index)
- 刪除某個元素:
int remove(Object e)
- 將指定的索引上的元素修改成指定值:set(int index,Object obj)
- 獲取指定索引的元素:
E get(int index)
- 獲取鏈表大小(包含元素的個數):
int size()
下面進入List的代碼示例,我們可以看到以上這些方法的使用:
public class ListDemo1 {
//List:特點是數據可重複 有序(list中有了索引 我們可以通過索引添加元素 修改元素 移除元素 得到元素)
public static void main(String[] args) {
//創建List集合對象
List list1=new ArrayList();
//因爲List是Collection的子接口 所以Collection的方法對它同樣適用
// list1.add("zhangsan");
// list1.isEmpty();
// list1.clear();
// list1.remove("zhangsan");
// list1.contains("zhangsan");
//除了使用ArrayList和LinkedList,我們還可以通過List接口提供的of()方法,根據給定元素快速創建List
List<Integer> list2 = List.of(1, 2, 5);
//但是List.of()方法不接受null值,如果傳入null,會拋出NullPointerException異常。
list1.add(1);
list1.add(null);
list1.add(1);
list1.add(2);
list1.add(3);
list1.add(4);
System.out.println(list1);
/*補充: int indexOf (Object o)
返回此列表中第一次出現的指定元素的索引;如果此列表不包含該元素,則返回 - 1。
int lastIndexOf (Object o)
返回此列表中最後出現的指定元素的索引;如果列表不包含此元素,則返回 - 1。*/
int index = list1.indexOf(1);
System.out.println(index);
int index1 = list2.lastIndexOf(4);
System.out.println(index1);
/*補充:
List還提供了boolean contains(Object o)方法來判斷List是否包含某個指定元素*/
System.out.println(list1.contains("C")); // false
System.out.println(list2.contains("5")); // true
//List 特有的方法
//1、向指定下標(索引) 添加元素 從0開始 (把小明內容添加到list1集合中1索引的位置)
list1.add(1, "小明");
list1.add(2, "張三");
System.out.println(list1);
//2、將指定這個索引的值 修改爲指定元素內容 (將list1集合中1索引的內容修改成了王五)
list1.set(1, "王五");
//collection中的移除方法 通過傳入元素本身 去移除元素內容 返回布爾類型 移除成功返回true 否則false
System.out.println("collection中remove list中繼承了"+list1.remove("小明"));
//3、刪除指定索引值的元素 (把list1 1下標的內容移除 當我們在調用這個方法的時候返回的移除元素內容本身)
System.out.println("list特有==="+list1.remove(1));
System.out.println(list1);
//4、通過索引值 返回該索引上元素的內容
System.out.println(list1.get(0));
}
}
- List遍歷
以代碼演示List遍歷的幾種方式:
//List集合遍歷
public class ListDemo2 {
public static void main(String[] args) {
List list1=new ArrayList();
list1.add("1");
list1.add("3");
list1.add("1");
list1.add("5");
list1.add("1");
fun1(list1);
fun2(list1);
fun3(list1);
}
//第一種遍歷方式 轉換成數組 toArray
//第二種foreach
public static void fun1(List list) {
for (Object obj: list) {
System.out.println("foreach======"+obj);
}
}
//第三種 可以使用for循環 collection對象不能使用for循環 而list可以使用for循環進行遍歷集合
public static void fun2(List list) {
for(int i=0;i<list.size();i++) {
System.out.println("for循環 內容====="+list.get(i));
}
}
//第四種 迭代器
public static void fun3(List list) {
//第一步 得到迭代器
Iterator iterator = list.iterator();
//第二步 結合while 開始遍歷 注意這裏採用的是正向迭代
while(iterator.hasNext()) {
System.out.println("迭代器遍歷======"+iterator.next());
}
//你當然也可以實現反向迭代 自己試試吧
}
}
這裏要說明一下,儘管我們要遍歷一個List
,完全可以用for
循環根據索引配合get(int)
方法遍歷,也就是第三種方式,但這種方式並不推薦,一是代碼複雜,二是因爲get(int)
方法只有ArrayList
的實現是高效的,換成LinkedList
後,索引越大,訪問速度越慢。
所以我們要始終堅持使用迭代器Iterator
來訪問List
。Iterator
本身也是一個對象,但它是由List
的實例調用iterator()
方法的時候創建的。Iterator
對象知道如何遍歷一個List
,並且不同的List
類型,返回的Iterator
對象實現也是不同的,但總是具有最高的訪問效率。
Iterator
對象有兩個方法:boolean hasNext()
判斷是否有下一個元素,E next()
返回下一個元素。因此,使用Iterator
遍歷List
代碼如下:
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
}
}
有人可能覺得使用Iterator
訪問List
的代碼比使用索引更復雜。但是,要記住,通過Iterator
遍歷List
永遠是最高效的方式。並且,由於Iterator
遍歷是如此常用,所以,Java的for each
循環本身就可以幫我們使用Iterator
遍歷。把上面的代碼再改寫如下:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (String s : list) {
System.out.println(s);
}
}
}
上述代碼就是我們編寫遍歷List
的常見代碼。
實際上,只要實現了Iterable
接口的集合類都可以直接用for each
循環來遍歷,Java編譯器本身並不知道如何遍歷集合對象,但它會自動把for each
循環變成Iterator
的調用,原因就在於Iterable
接口定義了一個Iterator<E> iterator()
方法,強迫集合類必須返回一個Iterator
實例。
- List和Array的轉換
把List
變爲Array
有三種方法,第一種是調用toArray()
方法直接返回一個Object[]
數組:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
Object[] array = list.toArray();
for (Object s : array) {
System.out.println(s);
}
}
}
這種方法會丟失類型信息,所以實際應用很少。
第二種方式是給toArray(T[])
傳入一個類型相同的Array
,List
內部自動把元素複製到傳入的Array
中:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Integer[] array = list.toArray(new Integer[3]);
for (Integer n : array) {
System.out.println(n);
}
}
}
/*注意到這個toArray(T[])方法的泛型參數<T>並不是List接口定義的泛型參數<E>,所以,我們實際上可以傳入其他類型的數組,例如我們傳入Number類型的數組,返回的仍然是Number類型:
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Number[] array = list.toArray(new Number[3]);
for (Number n : array) {
System.out.println(n);
}
}
}
*/
但是,如果我們傳入類型不匹配的數組,例如,String[]
類型的數組,由於List
的元素是Integer
,所以無法放入String
數組,這個方法會拋出ArrayStoreException
。
如果我們傳入的數組大小和List
實際的元素個數不一致怎麼辦?根據List接口的文檔,我們可以知道:
如果傳入的數組不夠大,那麼List
內部會創建一個新的剛好夠大的數組,填充後返回;如果傳入的數組比List
元素還要多,那麼填充完元素後,剩下的數組元素一律填充null
。
實際上,最常用的是傳入一個“恰好”大小的數組:
Integer[] array = list.toArray(new Integer[list.size()]);
反過來,把Array
變爲List
就簡單多了,通過List.of(T...)
方法最簡單:
Integer[] array = { 1, 2, 3 };
List<Integer> list = List.of(array);
對於JDK 11之前的版本,可以使用Arrays.asList(T...)
方法把數組轉換成List
。
要注意的是,返回的List
不一定就是ArrayList
或者LinkedList
,因爲List
只是一個接口,如果我們調用List.of()
,它返回的是一個只讀List,
對只讀List
調用add()
、remove()
方法會拋出UnsupportedOperationException
:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
list.add(999); // UnsupportedOperationException
}
}
- ArrayList
ArrayList 是List的一個實現子類,從第一節關於數據結構的簡要討論中,我們也可以看到ArrayList底層數據結構是數組,是線程不安全的,關於它添加元素的細節也得知了,它採用順序存儲,並且是非同步的,允許相同元素和null,實現了動態大小的數組,遍歷效率高,使用頻率高,但是插入類操作慢。
//ArrayList:數組實現 順序存儲 遍歷速度快 插入速度慢
public class ArrayListDemo {
public static void main(String[] args) {
//創建arraylist集合對象 ArrayList 用法與List相同 方法同樣適用
ArrayList al =new ArrayList();
al.add("123");
al.add("123");
al.add("123");
al.add("123");
al.add(1, "ddd");
System.out.println(al);
//列表迭代器 也可以指定從哪個下標開始 也可以不指定 ==》無參
ListIterator listIterator = al.listIterator(1);
//使用列表迭代器對ArrayList進行正序和倒序遍歷(即正向和反向迭代)
//反向迭代
/*
ListIterator 中的特有的方法 可以進行反向迭代
boolean hasPrevious ()
如果以逆向遍歷列表,列表迭代器有多個元素,則返回 true。
E previous ()
返回列表中的前一個元素。*/
//但是隻有先進行正向迭代,才能進行反向迭代 因爲此時使用的是同一個迭代器對象
while(listIterator.hasNext()) {
System.out.println("正序"+listIterator.next());
}
//正序while 循環結束後 光標在最後一個元素後面的位置
//注意光標位置
while(listIterator.hasPrevious()) {
System.out.println("倒序"+listIterator.previous());
}
}
}
- LinkedList
實現List
接口並非只能通過數組(即ArrayList
的實現方式)來實現,另一種LinkedList
通過“鏈表”也實現了List接口。在LinkedList
中,它的內部每個元素都指向下一個元素:
┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
HEAD ──>│ A │ ●─┼──>│ B │ ●─┼──>│ C │ ●─┼──>│ D │ │
└───┴───┘ └───┴───┘ └───┴───┘ └───┴───┘
LinkedList採取節點實現鏈式存儲( 數據結構爲雙鏈表結構),即是說每個元素都存儲在一個節點中,節點除了存儲元素本身之外,還需要存儲下一個元素的內存地址,以及上一個元素的內存地址。
來看源碼,我們主要看LinkedList是如何實現鏈表的:
//鏈表的長度
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
//鏈表的頭
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
//鏈表的尾
transient Node<E> last;
//鏈表維護的Node的結構
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
由此,我們可以看到,LinkedList維護的是一個first和last指針,而每個節點有item自身、prev和next兩個節點來維護雙鏈表的關係。
它的特點是:
- 查詢速度慢 :需要根據前面的節點來獲取後一個節點地址,查詢時要將前面的所有節點都需訪問一遍,節點數量越多,查詢速度就越慢
- 增刪插速度快:增刪一個元素,只需要修改新增元素前後兩個節點引用 ,與集合本身元素個數無關,其中,在鏈表首尾添加元素很高效,在中間添加元素比較低效,首先要找到插入位置的節點,再修改前後節點的指針。
下面以代碼演示LinkedList的基本方法:
public class LinkedListDemo {
public static void main(String[] args) {
LinkedList ll =new LinkedList<>();
ll.add("dd");
//添加元素到集合最後一個索引位置
ll.addLast("dd1");
//添加元素到集合第一個索引位置
ll.addFirst("dd2");
//打印的結果爲[dd2,dd,dd1]
//得到第一個元素
System.out.println(ll.getFirst());
//得到最後一個元素
System.out.println(ll.getLast());
//移除第一個元素
ll.removeFirst();
//移除最後一個元素
ll.removeLast();
System.out.println(ll);
//其他方法跟List 一樣使用
}
}
關於 LinkedList的更多知識及方法操作,我看到一篇博文講的非常詳細,尤其是細緻的討論了節點爲null以及越界的問題,在這裏分享給大家:
https://www.cnblogs.com/yijinqincai/p/10964188.html
- Vector
1.jdk1.0版本出現的。現在這個類已經過時,在jdk1.2後被ArrayList取代
2.特點:
- 線程安全 執行效率低
- 順序存儲 增刪較慢
3.特有方法:
- addElement() 添加元素
- removeElement() 移除元素
- elements() 用於遍歷
- 對比
Collection
├List (有序集合,允許相同元素和null)
├LinkedList (非同步,允許相同元素和null,遍歷/查詢效率低,插入和刪除效率高)
├ArrayList (非同步,允許相同元素和null,實現了動態大小的數組,遍歷/查詢效率高,增刪效率低,用的多)
└Vector(同步,允許相同元素和null,底層數據結構是數組,遍歷/查詢快,增刪慢,效率低)
└Stack(繼承自Vector,實現一個後進先出的堆棧)
這裏的同步又可以理解爲線程安全。既然線程不安全爲什麼要使用?因爲這個ArrayList比線程安全的Vector效率高。
關於爲什麼說ArrayList是線程不安全的呢?這篇博文的分享值得參考:
創作不易,您的點贊評論轉發是我寫作的最大動力!