Java入門之集合框架詳解
學(xué)習(xí)目標(biāo)
- 掌握集合框架體系結(jié)構(gòu)
- 掌握List接口存儲特點(diǎn)
- 掌握ArrayList類常用方法
- 了解泛型的定義和作用
- 掌握使用集合時使用泛型來約束元素類型
一、集合框架體系
思考:既然數(shù)組可以存儲多個數(shù)據(jù),為什么要出現(xiàn)集合?
- 數(shù)組的長度是固定的,集合的長度是可變的。
- 使用Java類封裝出一個個容器類,開發(fā)者只需要直接調(diào)用即可,不用再手動創(chuàng)建容器類。
1.1、集合框架概述
集合是Java中提供的一種容器,可以用來存儲多個數(shù)據(jù),根據(jù)不同存儲方式形成的體系結(jié)構(gòu),就叫做集合框架體系(掌握)。集合也時常被稱為容器。
List、Set集合是繼承了Colection接口,跟Collection是繼承關(guān)系
而ArrayList、LinkedList跟List是繼承關(guān)系,HashSet、TreeSet跟Set也是繼承關(guān)系,HashMap、TreeMap跟Map也是繼承關(guān)系
- Collection接口:泛指廣義上集合,主要表示List和Set兩種存儲方式。
- List接口:表示列表,規(guī)定了允許記錄添加順序,允許元素重復(fù)的規(guī)范。
- Set接口:表示狹義上集合,規(guī)定了不記錄添加順序,不允許元素重復(fù)的規(guī)范。
- Map接口:表示映射關(guān)系,規(guī)定了兩個集合映射關(guān)系的規(guī)范。
1.2、集合的分類
根據(jù)容器的存儲特點(diǎn)的不同,可以分成三種情況:
- List(列表):允許記錄添加順序,允許元素重復(fù)。=> 元素有序且可重復(fù)
- Set(數(shù)據(jù)集):不記錄添加順序,不允許元素重復(fù)。=> 元素?zé)o序且唯一
二、List接口
List接口是Collection接口子接口,List接口定義了一種規(guī)范,要求該容器允許記錄元素的添加順序,也允許元素重復(fù)。那么List接口的實現(xiàn)類都會遵循這一種規(guī)范。
List集合存儲特點(diǎn):
- 允許元素重復(fù)
- 允許記錄元素的添加先后順序
該接口常用的實現(xiàn)類有:
- ArrayList類:數(shù)組列表,表示數(shù)組結(jié)構(gòu),底層采用數(shù)組實現(xiàn),開發(fā)中使用對多的實現(xiàn)類,重點(diǎn)。
- LinkedList類:鏈表,表示雙向列表和雙向隊列結(jié)構(gòu),采用鏈表實現(xiàn),使用不多。
- Stack類:棧,表示棧結(jié)構(gòu),采用數(shù)組實現(xiàn),使用不多。
- Vector類:向量,其實就是古老的ArrayList,采用數(shù)組實現(xiàn),使用不多。
一般來說,集合接口的實現(xiàn)類命名規(guī)則:(底層數(shù)據(jù)結(jié)構(gòu) + 接口名)例如:ArrayList
2.1、ArrayList類
ArrayList類,基于數(shù)組算法的列表,通過查看源代碼會發(fā)現(xiàn)底層其實就是一個Object數(shù)組。
ArrayList常用方法的API:
public class ArrayListDemo1 { public static void main(String[] args) { // 創(chuàng)建一個默認(rèn)長度的列表對象 List list = new ArrayList(); // 打印集合中元素的個數(shù) System.out.println("元素數(shù)量:"+list.size());//0 // 添加操作:向列表中添加4個元素 list.add("Will"); list.add(100); list.add(true); list.add("Lucy"); // 查詢操作: System.out.println("列表中所有元素:"+list);//輸出:[Will, 100, true, Lucy] System.out.println("元素數(shù)量:"+list.size());//4 System.out.println("第一個元素:"+list.get(0));//Will // 修改操作:把索引為2的元素,替換為wolfcode list.set(2, "wolfcode"); System.out.println("修改后:"+list);//輸出:[Will, 100, wolfcode, Lucy] // 刪除操作:刪除索引為1的元素 list.remove(1); System.out.println("刪除后:"+list);//輸出:[Will, wolfcode, Lucy] } }
2.2、LinkedList類
LinkedList類,底層采用鏈表算法,實現(xiàn)了鏈表,隊列,棧的數(shù)據(jù)結(jié)構(gòu)。無論是鏈表還是隊列主要操作的都是頭和尾的元素,因此在LinkedList類中除了List接口的方法,還有很多操作頭尾的方法。
LinkedList常用方法的API:
其余方法:
- boolean offerFirst(Object e) 在此列表的開頭插入指定的元素。
- boolean offerLast(Object e) 在此列表末尾插入指定的元素。
- Object peekFirst() 獲取但不移除此列表的第一個元素;如果此列表為空,則返回 null。
- Object peekLast() 獲取但不移除此列表的最后一個元素;如果此列表為空,則返回 null。
- Object pollFirst() 獲取并移除此列表的第一個元素;如果此列表為空,則返回 null。
- Object pollLast() 獲取并移除此列表的最后一個元素;如果此列表為空,則返回 null。
- void push(Object e) 將元素推入此列表所表示的棧。
- Object pop() 從此列表所表示的棧處彈出一個元素。
- Object peek() 獲取但不移除此列表的頭(第一個元素)。
LinkedList之所以有這么多方法,是因為自身實現(xiàn)了多種數(shù)據(jù)結(jié)構(gòu),而不同的數(shù)據(jù)結(jié)構(gòu)的操作方法名稱不同,在開發(fā)中LinkedList使用不是很多,知道存儲特點(diǎn)就可以了。
public class LinkedListDemo { public static void main(String[] args) { LinkedList list = new LinkedList(); //添加元素 list.addFirst("A"); list.addFirst("B"); System.out.println(list); list.addFirst("C"); System.out.println(list); list.addLast("D"); System.out.println(list); //獲取元素 System.out.println("獲取第一個元素:" + list.getFirst());//C System.out.println("獲取最后一個元素:" + list.getLast());//D //刪除元素 list.removeFirst(); System.out.println("刪除第一個元素后:" + list);//[B, A, D] list.removeLast(); System.out.println("刪除最后一個元素后:" + list);//[B, A] } }
運(yùn)行結(jié)果
[B, A]
[C, B, A]
[C, B, A, D]
獲取第一個元素:C
獲取最后一個元素:D
刪除第一個元素后:[B, A, D]
刪除最后一個元素后:[B, A]
三、泛型
3.1、什么是泛型
其實就是一種類型參數(shù),主要用于某個類或接口中數(shù)據(jù)類型不確定時,可以使用一個標(biāo)識符來表示未知的數(shù)據(jù)類型,然后在使用該類或接口時指定該未知類型的真實類型。
泛型可用到的接口、類、方法中,將數(shù)據(jù)類型作為參數(shù)傳遞,其實更像是一個數(shù)據(jù)類型模板。
如果不使用泛型,從容器中獲取出元素,需要做類型強(qiáng)轉(zhuǎn),也不能限制容器只能存儲相同類型的元素。
List list = new ArrayList(); list.add("A"); list.add("B"); String ele = (String) list.get(0);
3.2、自定義和使用泛型
定義泛型:使用一個標(biāo)識符,比如T在類中表示一種未知的數(shù)據(jù)類型。
使用泛型:一般在創(chuàng)建對象時,給未知的類型設(shè)置一個具體的類型,當(dāng)沒有指定泛型時,默認(rèn)類型為Object類型。
需求:定義一個類Point,x和y表示橫縱坐標(biāo),分別使用String、Integer、Double表示坐標(biāo)類型。
如果沒有泛型需要設(shè)計三個類,如下:
定義泛型:
在類上聲明使用符號T,表示未知的類型
//在類上聲明使用符號T,表示未知的類型 public class Point<T> { private T x; private T y; //省略getter/setter }
使用泛型:
//沒有使用泛型,默認(rèn)類型是Object Point p1 = new Point(); Object x1 = p1.getX(); //使用String作為泛型類型 Point<String> p2 = new Point<String>(); String x2 = p2.getX(); //使用Integer作為泛型類型 Point<Integer> p3 = new Point<Integer>(); Integer x3 = p3.getX();
注意:這里僅僅是演示泛型類是怎么回事,并不是要求定義類都要使用泛型。
3.3、在集合中使用泛型
拿List接口和ArrayList類舉例。
class ArrayList<E>{ public boolean add(E e){ } public E get(int index){ } }
此時的E也僅僅是一個占位符,表示元素(Element)的類型,那么當(dāng)使用容器時給出泛型就表示該容器只能存儲某種類型的數(shù)據(jù)。
//只能存儲String類型的集合 List<String> list1 = new ArrayList<String>(); list1.add("A"); list1.add("B"); //只能存儲Integer類型的集合 List<Integer> list2 = new ArrayList<Integer>(); list2.add(11); list2.add(22);
因為前后兩個泛型類型相同(也必須相同),泛型類型推斷:
List<String> list1 = new ArrayList<String>(); // 可以簡寫為 List<String> list1 = new ArrayList<>();
通過反編譯工具,會發(fā)現(xiàn)泛型其實是語法糖,也就是說編譯之后,泛型就不存在了。
注意:泛型必須是引用類型,不能是基本數(shù)據(jù)類型(錯誤如下):
List<int> list = new ArrayList<int>();//編譯錯誤
泛型不存在繼承的關(guān)系(錯誤如下):
List<Object> list = new ArrayList<String>(); //錯誤的
四、集合遍歷
4.1、集合元素遍歷
對集合中的每一個元素獲取出來。
List<String> list = new ArrayList<>(); list.add("西施"); list.add("王昭君"); list.add("貂蟬"); list.add("楊玉環(huán)");
使用for遍歷
for (int index = 0; index < list.size(); index++) { String ele = list.get(index); System.out.println(ele); }
使用迭代器遍歷
Iterator表示迭代器對象,迭代器中擁有一個指針,默認(rèn)指向第一個元素之前,
- boolean hasNext():判斷指針后是否存在下一個元素
- Object next():獲取指針位置下一個元素,獲取后指針向后移動一位
Iterator<String&ggt; it = list.iterator(); while(it.hasNext()) { String ele = it.next(); System.out.println(ele); }
操作原理如下圖:
注意: 此時,就不能使用兩次迭代器,因為第一個迭代器就已經(jīng)將指針指向了最后一個元素,這樣的話第二個迭代器開始的指針一直都是最后一個
使用for-each遍歷(推薦使用)
for (String ele : list) { System.out.println(ele); }
通過反編譯工具會發(fā)現(xiàn),for-each操作集合時,其實底層依然是Iterator,我們直接使用for-each即可。
4.2、并發(fā)修改異常
需求:在迭代集合時刪除集合元素,比如刪除王昭君。
List<String> list = new ArrayList<>(); list.add("西施"); list.add("王昭君"); list.add("貂蟬"); list.add("楊玉環(huán)"); System.out.println(list); for (String ele : list) { if("王昭君".equals(ele)) { list.remove(ele); } } System.out.println(list);
此時報錯java.util.ConcurrentModificationException,并發(fā)修改異常。
造成該錯誤的原因是,不允許在迭代過程中改變集合的長度(不能刪除和增加)。如果要在迭代過程中刪除元素,就不能使用集合的remove方法,只能使用迭代器的remove方法,此時只能使用迭代器來操作,不能使用foreach。
List<String> list = new ArrayList<>(); list.add("西施"); list.add("王昭君"); list.add("貂蟬"); list.add("楊玉環(huán)"); System.out.println(list); //獲取迭代器對象 Iterator<String> it = list.iterator(); while(it.hasNext()) { String ele = it.next(); if("王昭君".equals(ele)) { it.remove(); } } System.out.println(list);
五、Set接口
? Set是Collection子接口,Set接口定義了一種規(guī)范,也就是該容器不記錄元素的添加順序,也不允許元素重復(fù),那么Set接口的實現(xiàn)類都遵循這一種規(guī)范。
Set集合存儲特點(diǎn):
- 不允許元素重復(fù)
- 不會記錄元素的添加先后順序
- Set只包含從Collection繼承的方法,不過Set無法記住添加的順序,不允許包含重復(fù)的元素。當(dāng)試圖添加兩個相同元素進(jìn)Set集合,添加操作失敗,add()方法返回false。
Set接口定義了一種規(guī)范,也就是該容器不記錄元素的添加順序,也不允許元素重復(fù)。
Set接口常用的實現(xiàn)類有:
- HashSet類:底層采用哈希表實現(xiàn),開發(fā)中使用對多的實現(xiàn)類,重點(diǎn)。
- TreeSet類:底層采用紅黑樹實現(xiàn),可以對集合中元素排序,使用不多。
HashSet和TreeSet的API都是一樣的:
5.1、HashSet類
HashSet 是Set接口的實現(xiàn)類,底層數(shù)據(jù)結(jié)構(gòu)是哈希表,集合容器不記錄元素的添加順序,也不允許元素重復(fù)。通常我們也說HashSet中的元素是無序的、唯一的。
public class HashSetDemo { public static void main(String[] args) { Set<String> set = new HashSet<>(); //添加操作:向列表中添加4個元素 set.add("Will"); set.add("wolf"); set.add("code"); set.add("Lucy"); //查詢操作: System.out.println("集合中所有元素:" + set);//[code, wolf, Will, Lucy] System.out.println("元素數(shù)量:" + set.size());//4 System.out.println("是否存在某個元素:" + set.contains("code"));//true System.out.println("是否存在某個元素:" + set.contains("code2"));//false //刪除操作:刪除code元素 set.remove("code"); System.out.println("刪除后:" + set);//[wolf, Will, Lucy] //使用for-each遍歷 for (String ele : set) { System.out.println(ele); } //使用迭代器遍歷 Iterator<String> it = set.iterator(); while (it.hasNext()) { Object ele = it.next(); System.out.println(ele); } } }
哈希表工作原理:
HashSet底層采用哈希表實現(xiàn),元素對象的hashCode值決定了在哈希表中的存儲位置。
向HashSet添加元素時,經(jīng)過以下過程:
- step 1 : 首先計算要添加元素e的hashCode值,根據(jù) y = k( hashCode ) 計算出元素在哈希表的存儲位置,一般計算位置的函數(shù)會選擇 y = hashcode % 9, 這個函數(shù)稱為散列函數(shù),可見,散列的位置和添加的位置不一致。
- step 2:根據(jù)散列出來的位置查看哈希表該位置是否有無元素,如果沒有元素,直接添加該元素即可。如果有元素,查看e元素和此位置所有元素的是否相等,如果相等,則不添加,如果不相同,在該位置連接e元素即可。
在哈希表中元素對象的hashCode和equals方法的很重要。
每一個存儲到哈希表中的對象,都得覆蓋hashCode和equals方法用來判斷是否是同一個對象,一般的,根據(jù)對象的字段數(shù)據(jù)比較來判斷,通常情況下equals為true的時候hashCode也應(yīng)該相等。
5.2、TreeSet類
TreeSet類底層才有紅黑樹算法,會對存儲的元素對象默認(rèn)使用自然排序(從小到大)。
注意:必須保證TreeSet集合中的元素對象是相同的數(shù)據(jù)類型,否則報錯。
public class TreeSetDemo{ public static void main(String[] args) { Set<String> set = new TreeSet<>(); set.add("wolf"); set.add("will"); set.add("sfef"); set.add("allen"); System.out.println(set);// [allen, sfef, will, wolf] } }
第一步:插入第一個節(jié)點(diǎn),無須比較,直接作為根節(jié)點(diǎn)。
第二步:插入第二個節(jié)點(diǎn),和wolf節(jié)點(diǎn)做比較,較小,往左移動。
第三步:插入第三個節(jié)點(diǎn),和wolf節(jié)點(diǎn)比較,較小,左移,再和will節(jié)點(diǎn)比較,較小,再左移。
第四步:由于TreeSet是平衡二叉樹,如果樹不平衡,會對節(jié)點(diǎn)進(jìn)行調(diào)整。
第五步:插入第四個節(jié)點(diǎn),此時和will節(jié)點(diǎn)作比較,較小,左移,再和stef節(jié)點(diǎn)做比較,較小,左移。
六、Map接口
6.1、認(rèn)識Map
Map,翻譯為映射,在數(shù)學(xué)中的解釋為:
設(shè)A、B是兩個非空集合,如果存在一個法則f,使得A中的每個元素a,按法則f,在B中有唯一確定的元素b與之對應(yīng),則稱f為從A到B的映射,記作f:A→B。
也就是說映射表示兩個集合之間各自元素的一種“對應(yīng)”的關(guān)系,在面向?qū)ο笾形覀兪褂肕ap來封裝和表示這種關(guān)系。
從定義和結(jié)構(gòu)圖上,可以看出Map并不是集合,而表示兩個集合之間的一種關(guān)系,故Map沒有實現(xiàn)Collection接口。
在Map中,要求A集合中的每一個元素都可以在B集合中找到唯一的一個值與之對應(yīng)。這句話可以解讀為一個A集合元素只能對應(yīng)一個B集合元素,也就說A集合中的元素是不允許重復(fù)的,B集合中的元素可以重復(fù),也可不重復(fù)。那么不難推斷出A集合應(yīng)該是一個Set集合,B集合應(yīng)該是List集合。
我們把A集合中的元素稱之為key,把B集合中的元素稱之為value。
其實能看出一個Map其實就有多個key-value(鍵值對)組成的,每一個鍵值對我們使用Entry表示。
不難發(fā)現(xiàn),一個Map結(jié)構(gòu)也可以理解為是Entry的集合,即Set。
一般的,我們依然習(xí)慣把Map稱之為集合,不過要區(qū)分下,Set和List是單元素集合,Map是雙元素集合。
- 單元素集合:每次只能存儲一個元素,比如Set和List。
- 雙元素集合:每次需要存儲兩個元素(一個key和一個value),比如Map。
注意:
- Map接口并沒有繼承于Collection接口也沒有繼承于Iterable接口,所以不能直接對Map使用for-each操作。
- 如果不能理解Map的結(jié)構(gòu),就直接記住Map每次需要存儲兩個值,一個是key,一個是value,其中value表示存儲的數(shù)據(jù),而key就是這一個value的名字。
6.2、Map常用的API
添加操作
- boolean put(Object key,Object value):存儲一個鍵值對到Map中
- boolean putAll(Map m):把m中的所有鍵值對添加到當(dāng)前Map中
刪除操作
- Object remove(Object key):從Map中刪除指定key的鍵值對,并返回被刪除key對應(yīng)的value
修改操作
- 無專門的方法,可以調(diào)用put方法,存儲相同key,不同value的鍵值對,可以覆蓋原來的。
查詢操作
- int size():返回當(dāng)前Map中鍵值對個數(shù)
- boolean isEmpty():判斷當(dāng)前Map中鍵值對個數(shù)是否為0.
- Object get(Object key):返回Map中指定key對應(yīng)的value值,如果不存在該key,返回null
- boolean containsKey(Object key):判斷Map中是否包含指定key
- boolean containsValue(Object value):判斷Map中是否包含指定value
- Set keySet():返回Map中所有key所組成的Set集合
- Collection values():返回Map中所有value所組成的Collection集合
- Set entrySet():返回Map中所有鍵值對所組成的Set集合
注意,標(biāo)紅的是重度使用的方法。
6.3、HashSet
HashMap底層基于哈希表算法,Map中存儲的key對象的hashCode值決定了在哈希表中的存儲位置,因為Map中的key是Set,所以不能保證添加的先后順序,也不允許重復(fù)。
HashMap key的底層數(shù)據(jù)結(jié)構(gòu)是哈希表
案例: 統(tǒng)計一個字符串中每個字符出現(xiàn)次數(shù)
public class HashMapDemo2{ public static void main(String[] args) { String str = "ABCDEFABCDEABCDABCABA"; //把字符串轉(zhuǎn)換為char數(shù)組 char[] charArray = str.toCharArray(); //Map的key存儲字符,value存儲出現(xiàn)的次數(shù) Map<Character, Integer> map = new HashMap<>(); //迭代每一個字符 for (char ch : charArray) { //判斷Map中是否已經(jīng)存儲該字符 if (map.containsKey(ch)) { Integer count = map.get(ch); //如果已經(jīng)存儲該字符,則把出現(xiàn)次數(shù)加上1 map.put(ch, count+1); }else { //如果沒有存儲該字符,則把設(shè)置次數(shù)為1 map.put(ch, 1); } } System.out.println(map); } }
6.4、TreeMap
TreeMap key底層基于紅黑樹算法,因為Map中的key是Set,所以不能保證添加的先后順序,也不允許重復(fù),但是Map中存儲的key會默認(rèn)使用自然排序(從小到大),和TreeSet一樣,除了可以使用自然排序也可以自定義排序。
需求:測試HashMap和TreeMap中key的順序
public class App { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); map.put("girl4", "楊玉環(huán)"); map.put("girl2", "王昭君"); map.put("key1", "西施"); map.put("key3", "貂蟬"); System.out.println(map); //------------------------------------------- map = new TreeMap<>(map); System.out.println(map); } }
輸出結(jié)果:
{key1=西施, girl4=楊玉環(huán), key3=貂蟬, girl2=王昭君}
{girl2=王昭君, girl4=楊玉環(huán), key1=西施, key3=貂蟬}
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
解決Spring Cloud Gateway獲取body內(nèi)容,不影響GET請求的操作
這篇文章主要介紹了解決Spring Cloud Gateway獲取body內(nèi)容,不影響GET請求的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12