java集合框架是jdk提供的一種容器,用來存儲對象信息
所有集合類都位於java.util包下,支持多線程的集合類位於java.util.concurrent包
java集合框架裡封裝了很多的算法和操作,提供了良好的接口,讓我們的學習十分方便
在下面我們不在稱呼Collection集合,而是稱呼Collection容器
我們操作其中數據也不外乎增刪查改,外加排序(排序暫時先不寫),我們下面就從這幾個角度開始學習
初學集合框架,我們只需要學習Collection和Map及其常用的實現類,
ok,上圖
簡單的看一下,常用的其實也就是這些東西了.
Collection接口有兩個有兩個子接口,分別是set和list,我們知道,接口代表一種規範,現在來看一下這兩個子接口自己的規範
set:這個接口就可以理解為數學意義上的集合了, 裡面存儲的元素,無序不可重複
list:這個接口可以叫做列表,裡面存儲的元素有序可以重複
先理解到這兒,我們再看看他們具體的實現類
ListArrayList實現類這個圖就比較詳細了,現在看不懂也沒事,略過
這些都是List的實現類,可以看到有很多,我們只關心現在要學的就行了
這個類的底層採用數組的存儲方式
我們知道數組是在內存裡開闢了一塊連續的內存空間,有索引.所以ArrayList在查找元素的時候可以通過索引訪問,速度很快。但是增刪元素的話(若增刪中間位置的元素),後面的所有元素都要進行一次移位,數據量一旦一大,速度立馬就慢下來了。
特點:ArrayList的底層採用數組的結構,增刪慢查詢快。
排序的話需要傳入一個比較器,我們稍後再說
代碼示例:(這個代碼有很多不規範的地方,比如泛型,這是為了儘量不出現未知的內容才這樣做的往後代碼也是)
這個類還有很多api,想要了解的同學可以通過
https://www.matools.com/api/java8
在線查看學習
源碼分析:我們從創建空arratlist開始,到添加一個元素,看看它內部是怎麼做的
它內部也是創建了一個空數組,元素的數量size沒有定義,默認是0,但是這個默認的初始容量是10,這個又是什麼呢?這個呀,就是在內存中實際開闢的空間。但是如果我們不添加元素的話是不會生效的,只要我們添加了一個元素,這個arraylist就在內存中開闢10個單位的空間了。
那麼添加的元素如果超出了這個容量怎麼辦?這個就涉及到arraylist的擴容問題了,我們稍後再看
transient的意思是不參加序列化和反序列化,不知道啥意思的就忽略
private static final int DEFAULT_CAPACITY = 10; private int size;transient Object[] elementDataprivate static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}現在我們來添加一個元素,他內部是怎麼幫我們實現的,這些方法的調用關係從上到下我給大家排序好了,接下來跟著我走
1,我們在調用add方法的時候,首先會調用ensureCapacityInternal(size + 1)方法,在這個方法裡又調用了ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
2,calculateCapacity()會判斷現在數組和原來的空數組是否相同,如果是相同的就返回默認容量和現在數組的長度中最大的一個,注意我們現在是第一次添加,也就是我們添加一個元素,數組的容量直接就變成了10。這個函數的作用就是計算添加這個元素所需要的容量
3,ensureExplicitCapacity()會判斷現在的容量夠不夠添加這個元素所需要的容量,如果夠的話就什麼也不做,不夠的話就調用grow()方法進行擴容
4, grow()方法用來對數組進行擴容,仔細看。他是先拿到老數組的長度,然後給老數組長度增加0.5倍變成新數組長度。
下面代碼第二行是二進位移位操作,二進位中向右移位1,在十進位中就是除2。所以增長了0.5倍
int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);然後判斷新數組長度夠不夠用,如果還是不夠,就將添加這個元素所需的容量設置為新數組的容量
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;接下來沒有什可看的了,因為接下來是判斷這個長度有沒有長處數組的最大長度,超出了就報錯,現在這麼理解就行了,想看具體細節就去看源碼。
如果長度合法的話就通過Arrays.copyOf()方法,為數組設置新的容量
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity);原始碼貼上:
public boolean add(E e) { ensureCapacityInternal(size + 1); elementData[size++] = e; return true; }
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount++;
if (minCapacity - elementData.length > 0) grow(minCapacity); }
private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }這個添加的方法,其他的操作也可以通過這個思路去理解源碼。此處的性能優化就只有避免或減少擴容操作。
LinkedList實現類
前置,再了解LinkedList之前先說先什麼是鍊表,鍊表是一種數據結構,就跟鏈條一樣,有一個個的節點。鍊表在內存中並不是一組連續的內存空間,而是散亂的,通過指針將這些散亂的數據連接起來。鍊表有頭節點和尾節點,除過尾節點,剩下每個節點都有一個指針指向下一個節點,這樣就像數據串聯了起來,像一條鏈子一樣。每個內存單元都存有數據和一個指針。
下圖這個就是典型的一個單向鍊表,
如果每個內存單元有兩個指針分別指向上一個元素和下一個元素,最兩端的節點即使頭也是尾,這個就是雙向鍊表。
如果將單鍊表的尾節點的指針指向頭,形成一個環形,這個就是循環鍊表。這倆都是鍊表的不同形式。
我們很多的數據結構都是基於鍊表來的,比如棧,隊列等
關係圖:
雙鍊表實現了List和Deque接口。實現所有可選列表操作,並允許所有元素(包括null )。
所有的操作都能像雙向列表一樣預期。索引到列表中的操作將從開始或結束遍歷列表,以更接近指定的索引為準。
請注意,此實現不同步。 如果多個線程同時訪問連結列表,並且至少有一個線程在結構上修改列表,則必須在外部進行同步。(結構修改是添加或刪除一個或多個元素的任何操作;僅設置元素的值不是結構修改。)這通常通過在自然封裝列表的對象上進行同步來實現。如果沒有這樣的對象存在,列表應該使用Collections.synchronizedList方法「包裝」。這最好在創建時完成,以防止意外的不同步訪問列表:
List list = Collections.synchronizedList(new LinkedList(...));這個類的iterator和listIterator方法返回的迭代器是故障快速的 :如果列表在迭代器創建之後的任何時間被結構化地修改,除了通過迭代器自己的remove或add方法之外,迭代器將會拋出一個ConcurrentModificationException 。因此,面對並發修改,迭代器將快速而乾淨地失敗,而不是在未來未確定的時間冒著任意的非確定性行為。
LinkedList 繼承了 AbstractSequentialList 類。
LinkedList 實現了 Queue 接口,可作為隊列使用。
LinkedList 實現了 List 接口,可進行列表的相關操作。
LinkedList 實現了 Deque 接口,可作為隊列使用。
LinkedList 實現了 Cloneable 接口,可實現克隆。
LinkedList 實現了 java.io.Serializable 接口,即可支持序列化,能通過序列化去傳輸
怎麼樣,這個很牛逼吧,但是不怎麼常用,剛開始咱知道就好了
LinekList的Api有多有豐富,基本上常用的需求都能滿足,有很多api功能是一樣地 ,但是名字卻體現了不同數組結構的特性,底層的封裝也略有不同,所以你能應用的方法就是有這些:
想要了解的同學可以通過:
https://www.matools.com/api/java8
在線查看學習,機翻過來的中文不太準確,如果英文好,就去看原版吧,我是個英語渣
我們看到上述方法的參數大概就可以知道了,這個LinkedList對首尾的操作很豐富,也能用索引進行操作。
特點:
因為鍊表的增刪只是改變了指針的指向而已,更改或增刪一個元素一般只更改一兩個指針,不想ArrayList那樣牽一髮而動全身。所以鍊表的增刪性能高
如果查詢LinkedList的話,就不能像array那樣通過索引直接拿到了,LinkedList的查找是通過指針下移實現的,也就是說,每次都下移指針,然後判斷當前元素。所以鍊表查改效率比array低下
總結:與 ArrayList 相比,LinkedList 的增加和刪除的操作效率更高,而查找和修改的操作效率較低。
源碼分析:簡單看一下源碼啊,了解了解,有兩個成員變量分別是頭節電和尾節點(transient的意思是不參加序列化和反序列化,不知道啥意思的就忽略),還有一個當前鍊表長度(在這個地方就沒有什麼Capacity的概念了,注意哈)
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
public LinkedList() { }為什麼前面說:「所有的操作都能像雙向列表一樣預期。」呢(引自java文檔)看下面這段代碼,這是LinkedList的一個靜態內部類
item表示當前的元素,(E是泛型的意思,不懂的略過,後面說)
next和prev的類型也是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之後,肯定就是要添加元素了,源碼很多我們只分析add()方法,其他操作方法同理進行分析就行
當我們添加元素的時候,會默認添加到尾部。
public boolean add(E e) { linkLast(e); return true; }添加到尾部的具體實現:手寫過鍊表的很這就很簡單了,沒寫過沒事,我後面會寫
拿到當前鍊表的最後一個元素 l
創建一個新的節點,新節點的上一個元素引用 l,並將當前值存入,然後讓下一個節點的引用尾null,也就是說現在我們添加的節點就是尾節點了
判斷添加前的最後一個節點是不是空,如果是空的話那麼這個鍊表就只有一個元素,當前節點即是頭節點也是尾節點
若添加前的最後一個節點是不是空,那麼添加前的最後一個節點的下一個節點就引用當前節點
長度加一,modCount++不用管,這個是用來記錄改變次數的。
我說的不清楚,但是邏輯確實很簡單,仔細看一下源碼
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++; }addFirst():這個是添加元素到第一個邏輯也很簡單
public void addFirst(E e) { linkFirst(e); }
private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; }比較複雜的add()是添加到指定的索引位置
public void add(int index, E element) { checkPositionIndex(index);
if (index == size) linkLast(element); else linkBefore(element, node(index)); }
private void checkPositionIndex(int index) { if (!isPositionIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
private boolean isPositionIndex(int index) { return index >= 0 && index <= size; }
void linkBefore(E e, Node<E> succ) { 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++; }
Node<E> node(int index) {
if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }看著挺多的,現在進行分解:
public void add(int index, E element) { checkPositionIndex(index);
if (index == size) linkLast(element); else linkBefore(element, node(index)); }首先會調用checkPositionIndex(index)檢查角標是否合法
private void checkPositionIndex(int index) { if (!isPositionIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private boolean isPositionIndex(int index) { return index >= 0 && index <= size; }
if (index == size) linkLast(element);若不符合上述情況,就執行linkBefore(element, node(index));
先看node()
void linkBefore(E e, Node<E> succ) { 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++; } Node<E> node(int index) { if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }好了,就這樣。我們就完成了在鍊表中間插入元素了。breeze!
關於LinkedList的其他玩法,喜歡就自己再找找,如果實在不會,那就跟著教程手寫一個鍊表,就啥都清楚了。
Setset容器的話呢,就是數學意義上的集合,元素無序不重複,而且不能通過索引來訪問元素,只能通過元素本身來訪問自己
HashSet實現類HashSet的底層是什麼呢,就是HashMap。為什麼呢,因為HashSet就是HashMap的key,而HashMap的值都指向同一個地方罷了,現在聽不懂這句話沒有關係。就先略過。
api:HashSet的api很少
booleanadd(E e)
將指定的元素添加到此集合(如果尚未存在)。
voidclear()
從此集合中刪除所有元素。
Objectclone()
返回此 HashSet實例的淺層副本:元素本身不被克隆。
booleancontains(Object o)
如果此集合包含指定的元素,則返回 true 。
booleanisEmpty()
如果此集合不包含元素,則返回 true 。
Iterator<E>iterator()
返回此集合中元素的迭代器。
booleanremove(Object o)
如果存在,則從該集合中刪除指定的元素。
intsize()
返回此集合中的元素數(其基數)。
Spliterator<E>spliterator()
在此集合中的元素上創建late-binding和故障快速 Spliterator 。源碼分析:
首先創建對象,我們來看看無參的構造器
發現底層還是創建了HashMap()
PRESENT成員變量就是HashSet底層的HashMap的值的引用對象,所有的值都引用這一個對象
而且源碼有說到,這個HashMap的初始容量是16,負載因子是0.75.
初始容量我們已經理解,但是負載因子是啥呢,我們後面再說
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public HashSet() { map = new HashMap<>(); }接下來我們該到添加元素了,看一下add()的實現:還是調用了HashMap的添加元素的put方法。
public boolean add(E e) { return map.put(e, PRESENT)==null;}Map接口及其實現Map與List、Set接口不同,它是由一系列鍵值對組成的集合,提供了key到Value的映射。同時它也沒有繼承Collection。在Map中它保證了key與value之間的一一對應關係。也就是說一個key對應一個value,所以它不能存在相同的key值,當然value值可以相同。
這個key就像函數中的x,value就像函數中的y。
多個y可以對應一個x,一個x只能對應一個y
HashMap實現類api:那個keySet()方法的返回值就是一個Set。這下就明白了,HashSet和HashMap的關係了
這些api就不做演示了,沒啥意思
在看源碼之前,先了解一下Hash(哈希)
什麼是 HashHash(哈希),又稱「散列」。
散列(hash)英文原意是「混雜」、「拼湊」、「重新表述」的意思。
在某種程度上,散列是與排序相反的一種操作,排序是將集合中的元素按照某種方式比如字典順序排列在一起,而散列通過計算哈希值,打破元素之間原有的關係,使集合中的元素按照散列函數的分類進行排列。
在介紹一些集合時,我們總強調需要重寫某個類的 equlas() 方法和 hashCode() 方法,確保唯一性。這裡的 hashCode() 表示的是對當前對象的唯一標示(一般是地址)。計算 hashCode 的過程就稱作 哈希。
為什麼要有 Hash我們通常使用數組或者鍊表來存儲元素,一旦存儲的內容數量特別多,需要佔用很大的空間,而且在查找某個元素是否存在的過程中,數組和鍊表都需要挨個循環比較,而通過 哈希 計算,可以大大減少比較次數。
Hash衝突如果兩個數據算出了一個哈希值,就叫做Hash衝突,也叫Hash碰撞
負載因子
我們不深入研究Hash函數怎麼寫,現在知道這些就暫時夠用了,記住一下特點:
同一個value只會算出一個hash值。
不同的value可能會算出同一個hash值。
一個value絕對不可能算出不同的hash值
原理:
HashMap的實現原理
HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。(其實所謂Map其實就是保存了兩個對象之間的映射關係的一種集合)HashMap由數組+鍊表組成的,數組是HashMap的主體,鍊表則是主要為了解決哈希衝突而存在的,如果定位到的數組位置不含鍊表(當前entry的next指向null),那麼查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鍊表,對於添加操作,首先遍歷鍊表,存在即覆蓋,否則新增;對於查找操作來講,仍需遍歷鍊表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鍊表出現越少,性能才會越好。調整HashMap的參數使其更適合相應場景,這便是調優。
簡單源碼分析:
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; }
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }好了,HashMap了解到這裡就ok,但是HashMap的存取順序可不一樣,如果我們要保證存取順序一致的話,就使用LinkedHashMap就行了,至於HashTable的話,官方都不建議使用了,我也就不看了
泛型泛型的使用很簡單,就不看源碼了
泛型類public class MyGeneric<T>{ T t; public void show(T t){ sout(t);} public T getT(){ return t;}}public class TestGeneric{ public static void main(String[] args){ MyGeneric<String> myGeneric = new MyGeneric<String>(); myGeneric.t = "hello"; myGeneric.show("hello world!"); String string = myGeneric.getT(); MyGeneric<Integer> myGeneric2 = new MyGeneric<Integer>(); myGeneric2.t = 100; myGeneric2.show(200); Integer integer = myGeneric2.getT(); }}
泛型接口
語法:接口名
COPYpublic interface MyInterface<T> { String nameString="tang"; T server(T t);}COPYpublic class MyInterfaceImpl implements MyInterface<String>{@Overridepublic String server(String t) {System.out.println(t);return t;}}COPYMyInterfaceImpl myInterfaceImpl=new MyInterfaceImpl();myInterfaceImpl.server("xxx");COPYpublic class MyInterfaceImpl2<T> implements MyInterface<T>{@Overridepublic T server(T t) {System.out.println(t);return t;}}COPYMyInterfaceImpl2<Integer> myInterfaceImpl2=new MyInterfaceImpl2<Integer>();myInterfaceImpl2.server(2000);注意:不能泛型靜態常量
泛型方法語法:返回值類型
public class MyGenericMethod{ public <T> T show(T t){ sout("泛型方法" + t); return t;}}
MyGenericMethod myGenericMethod = new MyGenericMethod();myGenericMethod.show("字符串");myGenericMethod.show(200);myGenericMethod.show(3.14);泛型集合
概念:參數化類型、類型安全的集合,強制集合元素的類型必須一致
public static void main(String[] args) { HashMap<Integer,String> map = new HashMap(); map.put(1,"Str1"); }特點:
編譯時即可檢查,而非運行時拋出異常
訪問時,不必類型轉換(拆箱)
不同泛型之間應用不能相互賦值,泛型不存在多態
迭代器和工具類Java Iterator(迭代器)不是一個集合,它是一種用於訪問集合的方法,可用於迭代 ArrayList 和 HashSet 等集合
import java.util.ArrayList;import java.util.Iterator;public class QianniuTest {public static void main(String[] args) {
ArrayList<String> sites = new ArrayList<String>(); sites.add("qianniu1"); sites.add("qianniu2"); sites.add("qianniu3"); sites.add("qianniu4");
Iterator<String> it = sites.iterator();
while(it.hasNext()) { System.out.println(it.next()); }}
迭代器 it 的兩個基本操作是 next 、hasNext 和 remove。
調用 it.next() 會返回迭代器的下一個元素,並且更新迭代器的狀態。
調用 it.hasNext() 用於檢測集合中是否還有元素。
調用 it.remove() 將迭代器返回的元素刪除。
另外還有兩個工具類Arrays和Collections
這兩個工具類分別用來操作數組和集合,提供了一些常用的其他功能。十分的簡單。
另外還有TreeSet和TreeMap之類的集合沒有記,因為這些涉及到了紅黑樹,函數式接口等知識。
另外關於迭代器遍歷和普通for遍歷的區別:迭代器只能遍歷一次,而且不能更改集合中的值,因為迭代器用的指針。但是for你就可以隨便整了
那為什麼要用迭代器呢,當然是因為性能,10000個1的集合。迭代器比for差不多能快三倍。forEach的底層就是迭代器。
額,應該沒啥了,常用的也差不多了。
如有錯誤,懇請指正。
如能相助,萬分榮幸。