集合2:List、ArrayList、LinkedList、Vector的使用及遍歷方法

  • 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 │
└───┴───┴───┴───┴───┴───┘

然後,往內部指定索引的數組位置添加一個元素,然後把size1

size=6
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘

繼續添加元素,但是數組已滿,沒有空閒位置的時候,ArrayList先創建一個更大的新數組,然後把舊數組的所有元素複製到新數組,緊接着用新數組取代舊數組:

size=6
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │   │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

現在,新數組就有了空位,可以繼續添加一個元素到數組末尾,同時size1

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來訪問ListIterator本身也是一個對象,但它是由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[])傳入一個類型相同的ArrayList內部自動把元素複製到傳入的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是線程不安全的呢?這篇博文的分享值得參考:

https://blog.csdn.net/u010416101/article/details/88720974?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase

 

創作不易,您的點贊評論轉發是我寫作的最大動力!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章