- 分析常用集合的底層的原理:
ArrayList、Vector、LinckedList、HashMap、HashSet、LinkedHashMap、LruCache、SparseArray、ConcurrentHashMap
一、ArrayList
-
最佳的做法是將
ArrayList
作為預設的首選,當你需要而外的功能的時候,或者是當程式效能由於經常需要從表中間插入和刪除而變差的時候,才會去選擇LinkedList
來源於THinking in Java
-
原始碼分析
- 最重要的兩個屬性分別是:
elementData
陣列size
的大小
transient Object[] elementData; /** * The size of the ArrayList (the number of elements it contains). * * @serial */ //以及 size 大小 private int size; 複製程式碼
transient
:java
:語言的關鍵字,變數修飾符,如果用transient宣告一個例項變數,當物件儲存時,它的值不需要維持。換句話來說就是,用transient關鍵字標記的成員變數不參與序列化過程。- 建構函式:
new ArrayList()
的時候,會指定一個Object[]
private static final Object[] EMPTY_ELEMENTDATA = {}; public ArrayList() { super(); this.elementData = EMPTY_ELEMENTDATA; } 複製程式碼
- 指定長度
public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } 複製程式碼
new Collection()
新增一個集合
public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } 複製程式碼
- 新增元素
add()
將指定的元素追加到列表的末尾
public boolean add(E e) { // 比如說加了一個元素 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e;//這裡的推算是 elementData[0]=e return true; } 複製程式碼
ensureCapacityInternal()
方法詳情,如果是add
一個元素,那麼就會走到ensureExplicitCapacity()
的方法中!同時第一次擴容的最小的值為DEFAULT_CAPACITY=10
;
private void ensureCapacityInternal(int minCapacity) { // 如果 是直接new ArrayList的話,那麼擴容的最小的值為10 if (elementData == EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } //開始擴充套件 ensureExplicitCapacity(minCapacity); } 複製程式碼
ensureExplicitCapacity(minCapacity)
,其中minCapacity
是最小的長度,如果是使用的new ArrayList<E>()
然後add(E)
,那麼這個minCapacity=10
.具體請看程式碼的邏輯
private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } 複製程式碼
grow(minCapactity)
增加容量以確保它至少能容納由最小容量引數指定的元素數量。
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //(oldCapacity >> 1)等於 oldCapacity%2 意思就是除以2,取整數 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); } 複製程式碼
- 分析上面的問題,假如第一次新增資料,那麼
oldCapacity =0
;0>>2
=0
;newCapacity - minCapacity < 0
就是 :0-10
肯定小於0
的,所以newCapacity = minCapacity;
,根據前面的分析,minCapacity=10
! minCapacity is usually close to size, so this is a win:
翻譯為:最小容量通常接近大小,所以這是一個勝利: 最後呼叫等到一個容器長度為10
的elementData
:- 最後一步在
elementData[size++] = e;
就是把elementData[0] = e;
賦值完成了,size才會++ ,等於size=1
- 關於
>>
代表右移;2
的二進位制是10
,>>代表右移,10
右移1
位是二進位制的1
,<<
代表左移,10
左移1
位是二進位制的100
,也就是十進位制的4
。
- 最重要的兩個屬性分別是:
-
往指定角標中新增元素 ,過程和新增一個元素一樣,只不過這個方法更加的高效
System.arraycopy()
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
// 首先擴容校驗。
ensureCapacityInternal(size + 1); // Increments modCount!!
// TODO: 2018/8/16 使用了 native的方法
// 複製,向後移動 接著對資料進行復制,目的是把 index 位置空出來放本次插入的資料,並將後面的資料向後移動一個位置。
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
複製程式碼
- 在
ArrayList
中自定義了writeObject
和readObject
,目的是為了:JVM
會呼叫這兩個自定義方法來實現序列化與反序列化ArrayList
只序列化(序列化 (Serialization)將物件的狀態資訊轉換為可以儲存或傳輸的形式的過程。在序列化期間,物件將其當前狀態寫入到臨時或永續性儲存區。以後,可以通過從儲存區中讀取或反序列化物件的狀態,重新建立該物件)了被使用的資料。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
...
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
...
}
複製程式碼
ArrayList
的執行緒不安全,通過下面的方式證明
final ArrayList<String> lists=new ArrayList<>();
Thread t1= new Thread(){
@Override
public void run() {
super.run();
for (int i=0;i<25;i++){
lists.add("我是i="+i);
}
}
};
Thread t2= new Thread(){
@Override
public void run() {
super.run();
for (int i=25;i<50;i++){
lists.add("我是i="+i);
}
}
};
//主執行緒休眠1秒鐘,以便t1和t2兩個執行緒將lists填裝完畢。
t1.start();
t2.start();
try {
Thread.sleep(1000);
// 即使睡完覺了,但是也有可能長度不對
for(int l=0;l<lists.size();l++){
// todo 兩個執行緒不斷的插入的話,就會導致插入的是null 我是i=34 我是i=10 我是i=35 我是i=11 null null 我是i=12 我是i=38 我是i=13 我是i=39
System.out.print(lists.get(l)+" ");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
複製程式碼
- 兩個執行緒不斷的插入的話,就會導致插入的是
null
我是i=34 我是i=10 我是i=35 我是i=11 null null 我是i=12 我是i=38 我是i=13 我是i=39
- 如果要使用安全的執行緒的話,可以通過
List<String> data=Collections.synchronizedList(new ArrayList<String>());
得到執行緒安全的集合, *Collections.synchronizedList
的原理,如下程式碼
public static <T> List<T> synchronizedList(List<T> list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list)); } 複製程式碼
- 可以在
SynchronizedList
類中方法加入了關鍵字synchronized
public E get(int index) { synchronized (mutex) {return list.get(index);} } public E set(int index, E element) { synchronized (mutex) {return list.set(index, element);} } public void add(int index, E element) { 複製程式碼
- 如果要使用安全的執行緒的話,可以通過
- 關於原型模式,
ArrayList
實現了介面Cloneable
;這個介面只有一個作用,就是在執行時候通知虛擬機器可以安全的實現,在java的虛擬機器中,只有實現了這個介面的類才可以被拷貝,否者會丟擲CloneNotSupportedException
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);transient
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
複製程式碼
-
我們可以看到這裡有個深拷貝和 淺拷貝,幸運的是
java
中大部分都容器都實現了Cloneable
這個介面,所以在程度上去實現深入拷貝不太難。- 深拷貝:就是需要拷貝的類中,所有的東西,比如說:原型類中的陣列,容器,飲用物件等
- 淺拷貝:就是隻拷貝基本東西,容器這些不拷貝
- 更多的設計模式 二十三種設計模式
-
ArrayList
遍歷的速度快,插入刪除速度慢,隨機訪問的速度快
二、Vector
- 關注
add get
方法:可以得出:使用synchronized
進行同步寫資料,但是開銷較大,所以Vector
是一個同步容器並不是一個併發容器。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
複製程式碼
- 應該避免使用
Vector
,它只存在支援遺留程式碼的類中(它能正常的工作的唯一原因是:因為為了向前相容,它被適配成為了List
) - 其他的不想多說,浪費電!
三、LinckedList
- 變數: 集合元素數量;連結串列頭節點;連結串列尾節點
//集合元素數量
transient int size = 0;
//連結串列頭節點
transient Node<E> first;
//連結串列尾節點
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;
}
}
複製程式碼
- 構造方法
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
複製程式碼
- 關注
add(E)
方法,可以看到這個返回值永遠為true
; 每次插入都是移動指標,和ArrayList
的拷貝陣列來說效率要高上不少
public boolean add(E e) {
linkLast(e);
return true;
}
複製程式碼
linkLast(E)
方法:生成新節點 並插入到 連結串列尾部, 更新last/first
節點。
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++;
}
複製程式碼
-
如果說,最後的一個結點為
null
;那麼我們新加入的元素,就是最後一個結點,如果最後一個結點不為null
,那麼我們插入的新的值就是最後結點的l.next = newNode
. -
get()
方法
public E get(int index) {
// 常看陣列角標是否越界
checkElementIndex(index);
return node(index).item;
}
複製程式碼
node(index)
的方法
Node<E> node(int index) {
//二分查詢來看 index 離 size 中間距離來判斷是從頭結點正序查還是從尾節點倒序查
// assert isElementIndex(index);
//通過下標獲取某個node 的時候,(增、查 ),會根據index處於前半段還是後半段 進行一個折半,以提升查詢效率
if (index < (size >> 1)) {
Node<E> x = first;
//不斷的往前面找 ,如果查詢的角標比linkedList的size的取餘還小的話,就通過不斷的迴圈去得到相對應的值
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;
}
}
複製程式碼
- 可以看出這是一個二分查詢,如果
index < (size >> 1)
,>>
代表右移,其實就是%2
,這裡查詢下去,知道找到為止 - 如果假如,我們查詢的
index
約接近size
的一半,那麼我們需要的次數就會越低,總結一句話:效率是非常低的,特別是當index
越接近size
的中間值。 - 來源於
gitHub
四、HashMap
- 在 1.6 1.7
hashmap
的類的程式碼一共1500
行左右,在1.8
一共有2000
行左右! 這裡直接看的是JDK1.8
的程式碼。 - 關於變數
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//左移運算子,num << 1,相當於num乘以2 最大的長度
static final int MAXIMUM_CAPACITY = 1 << 30;// 相當於把1 位移30為等於 1 + 30個0的長度
// 填充比 因為如果填充比很大,說明利用的空間很多,如果一直不進行擴容的話,連結串列就會越來越長,這樣查詢的效率很低,因為連結串列的長度很大(當然最新版本使用了紅黑樹後會改進很多),擴容之後,將原來連結串列陣列的每一個連結串列分成奇偶兩個子連結串列分別掛在新連結串列陣列的雜湊位置,這樣就減少了每個連結串列的長度,增加查詢效率
// hashMap本來是以空間換時間,所以填充比沒必要太大。但是填充比太小又會導致空間浪費。如果關注記憶體,填充比可以稍大,如果主要關注查詢效能,填充比可以稍小。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//當add一個元素到某個位桶,其連結串列長度達到8時將連結串列轉換為紅黑樹
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
複製程式碼
-
關於
Node
內部類static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; //todo 建構函式 hash值 key 和value 和 下一個結點 Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } // 是去key的hash值和 value的hash值 然後做位異運算 轉為二進位制 相同為0,不同為1 public final int hashCode() { // todo 位異或運算(^) // 運算規則是:兩個數轉為二進位制,然後從高位開始比較,如果相同則為0,不相同則為1 return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // todo 判斷兩個 node 結點是否相等,一個比較自身相等,一個是比較key和value public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } } 複製程式碼
Node
類的中儲存了hash
key
value
和下一個結點Node
,後面解釋Node
類的hashCode
是Objects.hashCode(key) ^ Objects.hashCode(value)
;位異或運算(^): 運算規則是兩個數轉為二進位制,然後從高位開始比較,如果相同則為0,不相同則為1- 判斷兩個
node
是否相等:一個比較自身相等,一個是比較key
和value
-
HashMap
的構造方法,指定容量和擴充套件因子!
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果最大的長度大於最大的話,就預設最大的
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//填充比為正
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 加入指定的容量為 10 那麼新的擴容的臨界值為 13
this.threshold = tableSizeFor(initialCapacity);
}
複製程式碼
- 關於
tableSizeFor(initialCapacity)
方法,說白了就是演算法,給你一個接近的值,設定hashmap
的長度為10,那麼他的新的擴容的臨界值=16
int cap=10;
int n = cap - 1;//9
n |= n >>> 1;//9的二進位制=1001 >>>表示無符號的右移 100 =十進位制 4 n= 1001 |= 100
System.out.println("n="+n); // n=13; 其實就是等於 n= 1001 |= 100 也就是n=1101 換成十進位制等於13
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
int i= (n < 0) ? 1 : (n >= 1000000) ? 1000000 : n + 1;
複製程式碼
-
無符號的右移(
>>>
):按照二進位制把數字右移指定數位,高位直接補零,低位移除! -
a=a|b
等於a|=b
的意思就是把a和b按位或然後賦值給a 按位或的意思就是先把a和b都換成2進位制,然後用或操作 -
比如:
9
的二進位制1001
>>>
表示無符號的右移 得到100
等於十進位制4
n
=1001 |= 100
,最後n=1101
轉化為十進位制等於n=13
。 -
上面函式的運算過程
- n |= n >>> 1;//9的二進位制=1001 >>>表示無符號的右移 100 =十進位制 4 n= 1001 |= 100
- n |= n >>> 2; // 1101 移動兩位 0011 |1101 等於1111
- n |= n >>> 4;// 1111 移動4為 0000 |1111 =1111
- n |= n >>> 8;// 1111 移動8為 0000 |1111 =1111
- n |= n >>> 16;// 1111 移動16為 0000 |1111 =1111
-
HashMap
的構造方法,設定容器的長度 但是指定的預設的擴充套件因子為0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
複製程式碼
HashMap
的構造方法,什麼都不指定 都給預設的,我們自己最常用的。
//什麼都不指定 都給預設的
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
複製程式碼
*HashMap
的構造方法, 也可以new一個 map進去,這種的方式 我們使用的比較少
public HashMap(Map<? extends K, ? extends V> m) {
//預設指定了擴充套件的因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
複製程式碼
putMapEntries()
方法,如果是建構函式到這裡來的話,就會進入到threshold = tableSizeFor(t);
這裡來,然後遍歷m
,然後一個個元素去新增,如果裝載進來的map
集合過於巨大,建議使用源map
的原型模式clone
方法克隆一個。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 如果是hashmap中填充了一個map 就會走到這裡來 table == null =true
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// t=ft
if (t > threshold)
//也就會走到這裡來
threshold = tableSizeFor(t);
} else if (s > threshold) {
// 擴容機制
resize();
}
// copy的過程 遍歷hashmap的話,這個應該是最高效的方式
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
複製程式碼
- 關鍵方法
put
,瞭解如何儲存的資料
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
複製程式碼
-
putVal
方法的詳情,假裝put
資料去分析。// 在建構函式中,也呼叫了這個方法,唯一不同的地方就是 evict=fasle 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; /*如果table的在(n-1)&hash的值是空,就新建一個節點插入在該位置*/ if ((p = tab[i = (n - 1) & hash]) == null) // todo LinkedHashMap 重新重寫了這個方法,然後使用了 LinkedHashMap.Entry 裡面多了兩個結點 Entry<K,V> before, after; tab[i] = newNode(hash, key, value, null); ///*表示有衝突,開始處理衝突*/ else { Node<K,V> e; K k; /*檢查第一個Node,p是不是要找的值*/ 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); //如果衝突的節點數已經達到8個,看是否需要改變衝突節點的儲存結構,       //treeifyBin首先判斷當前hashMap的長度,如果不足64,只進行 //resize,擴容table,如果達到64,那麼將衝突的儲存結構為紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } /*如果有相同的key值就結束遍歷*/ if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } /*就是連結串列上有相同的key值*/ if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; // todo LinkedHashMap 對其重寫 afterNodeAccess(e); return oldValue; } } ++modCount; /*如果當前大小大於門限,門限原本是初始容量*0.75*/ if (++size > threshold) resize(); // todo LinkedHashMap 對其重寫 afterNodeInsertion(evict); return null; } 複製程式碼
-
1、可以發現
table
肯定為null
,沒有初始化,所以第一個判斷條件肯定成立tab = table) == null || (n = tab.length) == 0
,這裡有個小小的問題,當tab = table) == null
成立的時候,後面||
的程式碼是不會執行的,所以不會丟擲空指標的異常。也就會執行n = (tab = resize()).length;
的程式碼transient Node<K,V>[] table;// 第一次table沒有去初始化,肯定為null 複製程式碼
-
2、關於
resize()
的方法,其實這個也是很關鍵的方法,擴容// 擴容機制 HasMap的擴容機制resize(); final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; /*如果舊錶的長度不是空*/ if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } /*把新表的長度設定為舊錶長度的兩倍,newCap=2*oldCap*/ else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) /*把新表的門限設定為舊錶門限的兩倍,newThr=oldThr*2*/ newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; /*如果舊錶的長度的是0,就是說第一次初始化表*/ else { // zero initial threshold signifies using defaults // todo 在new hashMap中的長度 ,然後呼叫了 put的方法的時候,就會發生一次擴容 ,長度為16 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor;//新表長度乘以載入因子 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) /*下面開始構造新表,初始化表中的資料*/ Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null)//說明這個node沒有連結串列直接放在新表的e.hash & (newCap - 1)位置 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; //記錄下一個結點 //新表是舊錶的兩倍容量,例項上就把單連結串列拆分為兩隊, //e.hash&oldCap為偶數一隊,e.hash&oldCap為奇數一對 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } 複製程式碼
- 擴容方法也比較複雜,帶著問題來分析,第一次,
put
資料的時候,可以得出oldCap=0
、oldThr=0
;那麼新的長度newCap = DEFAULT_INITIAL_CAPACITY=16;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)=0.75*16=12
,把新的長度賦值給threshold = newThr;
- 然後
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
,根據上面我們可以的得出newCap=16
; - 由於
oldTab==null
,所以,這幾返回一個newTab
這是一個長度為16
的Node
的陣列
- 擴容方法也比較複雜,帶著問題來分析,第一次,
-
3、回到
putVal
的方法中,那麼n = (tab = resize()).length;
也就是n=16
-
4、那麼
(p = tab[i = (n - 1) & hash]) == null
是否成立呢,其實我們可以猜測下,第一次肯定是成立的,這裡有個運算子,位與運算子&
,把做運算的兩個數都轉化為二進位制的,然後從高位開始比較,如果兩個數都是1
則為1
,否者為0
.如下面的HashMap
中的演算法int newHash=hash("test"); // 1的hash值=1 test :hash值=3556516 System.out.println( "newHash 1的hash值="+newHash); i = (16 - 1) & newHash; // i值=1 test值=4 System.out.println("newHash的 i值="+i); int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 複製程式碼
-
5、這樣就是走到這裡來
tab[i] = newNode(hash, key, value, null);
,也就是tab[0]=newNode
。這裡有個面試,面試經常問,這裡注意到tab
是resize()
方法返回的,在resize()
方法中,又把table = newTab;
,那麼我們改動tab
能否去改變table
呢?其實是能夠的,這裡傳遞是地址值,如下面的Demo
String[] newS=setTest(); newS[0]="16"; // newS =[Ljava.lang.String;@1e0b9a System.out.println("newS ="+newS); //newS =[Ljava.lang.String;@1e0b9a System.out.println("test ="+test); System.out.println("test="+test.length); System.out.println("test="+test[0]); } String[] test; public String[] setTest(){ String[] newS=new String[10]; test=newS; return newS; } 複製程式碼
-
以上就是
HashMap
第一次put
資料的完整過程。
-
-
當多次的
put
資料的時候,如果 某個位置上的hash
值相同的話,準確的講i = (n - 1) & hash
是這個值,取出來的tab
不為null
,那麼儲存的結構轉化為連結串列
for (int binCount = 0; ; ++binCount) {
/*指標為空就掛在後面*/
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果衝突的節點數已經達到8個,看是否需要改變衝突節點的儲存結構,      
//treeifyBin首先判斷當前hashMap的長度,如果不足64,只進行
//resize,擴容table,如果達到64,那麼將衝突的儲存結構為紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
/*如果有相同的key值就結束遍歷*/
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
複製程式碼
- 當一個位置上的大於
TREEIFY_THRESHOLD - 1
也就是7
的話,看是否需要改變衝突節點的儲存結構.treeifyBin
首先判斷當前hashMap
的長度,如果不足64
,只進行resize
,擴容table
,如果達到64,那麼將衝突的儲存結構為紅黑樹.如下圖的結構
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
複製程式碼
-
是所有連結串列上的資料結構都會轉,不可能在一個連結串列上,即存在紅黑樹,也存在連結串列
-
get
方法相對應就簡單了
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 不斷的去取結點,是紅黑樹就去找紅黑樹,是聊邊就去找連結串列
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
複製程式碼
HashMap
是一個執行緒不安全的容器,發生擴容時會出現環形連結串列從而導致死迴圈HashMap
是一個無序的Map
,因為每次根據key
的hashCode
對映到Entry
陣列上,所以遍歷出來的順序並不是寫入的順序。HashMap
遍歷的速度慢,底層決定了,插入刪除的速度快,隨機訪問的速度也比較快
五、ConcurrentHashMap
- 支援執行緒安全的併發容器
ConcurrentHashMap
,原理和HashMap
差不多,區別就是採用了CAS + synchronized
來保證併發安全性 putVal
加了同步鎖synchronized
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//根據 key 計算出 hashcode
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 判斷是否需要進行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//f 即為當前 key 定位出的 Node,如果為空表示當前位置可以寫入資料,利用 CAS 嘗試寫入,失敗則自旋保證成功
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); //如果當前位置的 hashcode == MOVED == -1,則需要進行擴容
else {
//如果都不滿足,則利用 synchronized 鎖寫入資料
V oldVal = null;
// todo put 資料的時候 加入了鎖
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
//如果數量大於 TREEIFY_THRESHOLD 則要轉換為紅黑樹
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
複製程式碼
get
方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//根據計算出來的 hashcode 定址,如果就在桶上那麼直接返回值
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//如果是紅黑樹那就按照樹的方式獲取值
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 就不滿足那就按照連結串列的方式遍歷獲取值
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
複製程式碼
- 基本上的變數都是被
volatile
關鍵字修飾
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile long baseCount;
...
複製程式碼
volatile
關鍵字 Java
多執行緒的三大核心
1、 原子性 :java原子性和資料庫事務的原子性差不多,一個操作要麼是全部執行成功或者是失敗.
- JVM 只保證了基本的原子性,但是類似 i++ 之類的操作,看著好像是原子的操作,其實裡面涉及到了三個步驟
- 獲取 i 的值
- 自增
- 在賦值給 i
- 這三個步驟 要實現
i++
這樣的原子操作就需要用到synchronized
或者是 了lock
進行加鎖處理。 - 如果是基礎類的自增操作可以使用
AtomicInteger
這樣的原子類來實現(其本質是利用了CPU
級別的 的CAS
指令來完成的)。AtomicInteger
是執行緒安全的 - 其中用的最多的方法就是: incrementAndGet() 以原子的方式自增
AtomicInteger atomicInteger=new AtomicInteger(); int i = atomicInteger.incrementAndGet(); System.out.println("i="+i); public final int incrementAndGet() { return U.getAndAddInt(this, VALUE, 1) + 1; } 複製程式碼
2、可見性
-
現在的計算機,由於
cpu
直接從 主記憶體中讀取資料的效率不高。所以都會對應的cpu
快取記憶體,先將主記憶體中的資料讀取到快取中,執行緒修改資料之後首先更新到快取中,之後才會更新到主記憶體。如果此時還沒有將資料更新到主記憶體其他的執行緒此時讀取就是修改之前的資料 -
volatile
關鍵字就是用於儲存記憶體的可見性,當執行緒A更新了volatite
的修飾的變數的話,他會立即重新整理到主執行緒,並且將其餘快取中該變數的值清空,導致其餘執行緒只能去主記憶體讀取最新的值
*synchronized
和加鎖也能保證可見性,實現原理就是在釋放鎖之前其餘執行緒是訪問不到這個共享變數的。但是和volatile
相比較起來開銷比較大 !
- 但是
volatile
不能夠替換synchronized
因為volatile
不能夠保證原子性 (要麼執行成功或者失敗,沒有中間的狀態)
3、順序性
int a = 100 ; //1
int b = 200 ; //2
int c = a + b ; //3
複製程式碼
-
正常的程式碼的執行順序應該是
1》》2》》3
。但是有時候JVM
為了提高整體的效率會進行指令重排導致執行順序可能是2》》1》》3
。但是JVM
也不能是 什麼都進行重排,是在保證最終結果和程式碼順序執行結果是一致的情況下才可能會進行重排
-
重排在單執行緒中不會出現問題,但是在多執行緒中就會出現順序不一致的問題
-
java
中可以使用volatile
關鍵字來保證順序性,synchronized
和lock
也可以來保證有序性,和保證 原子性的方式一樣,通過同一段時間只能一個執行緒訪問來實現的 -
除了
volatile
關鍵字顯式的保證順序之外,jvm HIA
通過happen-before
原則來隱式來保證順序性。 -
volitle
的應用,主要是在單利,個人感覺這是常用的在移動端的開發!當然可以使用內部類或者是單利去實現,更多的設計模式- 1、
volatile
實現一個雙重檢查鎖的單例模式
public class Singleton { private static volatile Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } } 複製程式碼
- 這裡的
volatile
關鍵字主要是為了防止指令重排。 如果不用volatile
,singleton = new Singleton()
;,這段程式碼其實是分為三步:- 分配記憶體空間。(1)
- 初始化物件。(2)
- 將 singleton 物件指向分配的記憶體地址。(3)
- 加上
volatile
是為了讓以上的三步操作順序執行,反之有可能第三步在第二步之前被執行就有可能導致某個執行緒拿到的單例物件還沒有初始化,以致於使用報錯。
- 1、
-
2、控制停止執行緒的標記
private volatile boolean flag ;
private void run(){
new Thread(new Runnable() {
@Override
public void run() {
doSomeThing();
}
});
}
private void stop(){
flag = false ;
}
複製程式碼
- 如果沒有用
volatile
來修飾flag
,就有可能其中一個執行緒呼叫了stop()
方法修改了flag
的值並不會立即重新整理到主記憶體中,導致這個迴圈並不會立即停止.這裡主要利用的是volatile
的記憶體可見性 .
六、HashSet
HashSet
是一個不允許儲存重複元素的集合。HashSet
的原始碼只有三百多行,原理非常簡單,主要底層還是HashMap
。map
和PERSENT
:
// map :用於存放最終資料的。
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
// PRESENT :是所有寫入 map 的 value 值。
private static final Object PRESENT = new Object();
複製程式碼
- 構造方法:底層一個hashMap
public HashSet() {
map = new HashMap<>();
}
複製程式碼
- 關鍵的就是這個
add()
方法。 可以看出它是將存放的物件當做了HashMap
的健,value
都是相同的RESENT
。由於HashMap
的key
是不能重複的,所以每當有重複的值寫入到HashSet
時,value
會被覆蓋,但key
不會受到影響,這樣就保證了HashSet
中只能存放不重複的元素。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
複製程式碼
七、LinkedHashMap
HashMap
是一個無序的Map
,每次根據key
的hashcode
對映到Entry
陣列上,所以遍歷出來的順序並不是寫入的順序。 因此JDK
推出一個基於HashMap
但具有順序的LinkedHashMap
來解決有排序需求的場景。它的底層是繼承於HashMap
實現的,由一個雙向連結串列所構成。LinkedHashMap
的排序方式有兩種:- 根據寫入順序排序。
- 根據訪問順序排序(LRU底層的原理)。 其中根據訪問順序排序時,每次
get
都會將訪問的值移動到連結串列末尾,這樣重複操作就能得到一個按照訪問順序排序的連結串列。
LinkedHashMap
中的Entry
:利用了頭節點和其餘的各個節點之間通過Entry
中的after
和before
指標進行關聯static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } 複製程式碼
- 變數
// 用於指向雙向連結串列的頭部
transient LinkedHashMap.Entry<K,V> head;
//用於指向雙向連結串列的尾部
transient LinkedHashMap.Entry<K,V> tail;
// LinkedHashMap 如何達到有序的關鍵
// todo 還有一個 accessOrder 成員變數,預設是 false,預設按照插入順序排序,為 true 時按照訪問順序排序,也可以呼叫
final boolean accessOrder;
複製程式碼
- 構造方法,
LRUchace
最近最少使用的快取底層就是這個建構函式。
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
複製程式碼
-
側重關注
put
,會走父類HashMap
中的put
方法,具體請看HashMap
put
方法的解釋- 1、 在
LinkedHashMap
重寫了,newNode
的方法。 使用了LinkedHashMap.Entry
裡面多了兩個結點Entry<K,V> before, after
;
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); //祕密就在於 new的是自己的Entry類,然後呼叫了linkedNodeLast linkNodeLast(p); return p; } 複製程式碼
- 2、實現了
afterNodeAccess()
方法,void afterNodeAccess(Node<K,V> p) { }
!此函式執行的效果就是將最近使用的Node,放在連結串列的最末尾。特別說明一下,這裡是顯示連結串列的修改後指標的情況,實際上在桶裡面的位置是不變的,只是前後的指標指向的物件變了!
// 此函式執行的效果就是將最近使用的Node,放在連結串列的最末尾 void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; //僅當按照LRU原則且e不在最末尾,才執行修改連結串列,將e移到連結串列最末尾的操作 if (accessOrder && (last = tail) != e) { //將e賦值臨時節點p, b是e的前一個節點, a是e的後一個節點 LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; //設定p的後一個節點為null,因為執行後p在連結串列末尾,after肯定為null p.after = null; //p前一個節點不存在,情況一 if (b == null) head = a; else b.after = a; if (a != null) a.before = b; //p的後一個節點不存在,情況二 else last = b; if (last == null) head = p; else { //正常情況,將p設定為尾節點的準備工作,p的前一個節點為原先的last,last的after為p p.before = last; last.after = p; } //將p設定為將p設定為尾節點 tail = p; ++modCount; // 修改計數器+1 } } 複製程式碼
- 3、
put
方法 執行的第二個步驟 ,這個方法沒什麼用盡可能刪除最老的 插入後把最老的Entry
刪除,不過removeEldestEntry
總是返回false
,所以不會刪除,估計又是一個方法給子類用的
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; // todo hashmap中移除 Node結點 removeNode(hash(key), key, null, false, true); } } // 如果對映表示快取,這是有用的:它允許通過刪除過時條目來減少記憶體消耗的對映。 protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; } 複製程式碼
- 4 、
afterNodeRemoval()
移除結點也會重寫,因為結點都不一樣
void afterNodeRemoval(Node<K,V> e) { // unlink //與afterNodeAccess一樣,記錄e的前後節點b,a LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; //p已刪除,前後指標都設定為null,便於GC回收 p.before = p.after = null; //與afterNodeAccess一樣類似,一頓判斷,然後b,a互為前後節點 if (b == null) head = a; else b.after = a; if (a == null) tail = b; else a.before = b; } 複製程式碼
- 1、 在
-
get()
方法詳情,然後呼叫父類HashMap
的getNode()
去找結點public V get(Object key) { Node<K,V> e; //呼叫HashMap的getNode的方法, if ((e = getNode(hash(key), key)) == null) return null; if (accessOrder) afterNodeAccess(e); return e.value; } 複製程式碼
HashMap
中的getNode()
方法
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } 複製程式碼
-
關於訪問順序排序的Demo,我只想說明了一下,等於用了的資料,就會放在連結串列的末尾,這個類也是安卓中
LruCache
的底層原理
LinkedHashMap<String, Integer> map1 = new LinkedHashMap<String, Integer>(10, (float) 0.75,true);
map1.put("1",1) ;
map1.put("2",2) ;
map1.put("3",3) ;
map1.put("4",4) ;
map1.put("5",5) ;
map1.put("6",6) ;
map1.put("7",7) ;
map1.put("8",8) ;
map1.put("9",9) ;
map1.put("10",10) ;
map1.get("6");
// {1=1, 2=2, 3=3, 4=4, 5=5, 7=7, 8=8, 9=9, 10=10, 6=6}
System.out.println("map1=="+map1);
複製程式碼
八、LruCache
Android
中提供了一種基本的快取策略,即LRU(least recently used)
。基於該種策略,當儲存空間用盡時,快取會清除最近最少使用的物件LRU(Least Recently Used)
最近最少使用的,看了原始碼才知道核心是LRUCache
類,這個類的核心其實是LinkedHashMap
類.- Demo 如下
LruCache<Integer,String> lruCache=new LruCache<>(5);
lruCache.put(1,"1");
lruCache.put(2,"2");
lruCache.put(3,"3");
lruCache.put(4,"4");
lruCache.put(5,"5");
lruCache.get(1);
lruCache.get(2);
lruCache.get(3);
lruCache.get(4);
Map<Integer, String> snapshot = lruCache.snapshot();
//lruCache={5=5, 1=1, 2=2, 3=3, 4=4} 5最少使用到
System.out.println("lruCache="+snapshot.toString());
//當多新增一個的話,那麼5就會被刪除,加入6上去
lruCache.put(6,"6");
// new lruCache={1=1, 2=2, 3=3, 4=4, 6=6}
Map<Integer, String> snapshot1 = lruCache.snapshot();
System.out.println(" new lruCache="+snapshot1.toString());
複製程式碼
- 構造方法,可以明顯看出,底層使用的是
LinkedHashMap
.
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
// 初始化這裡 就是 new的 true的 所以使用的順序排序
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
複製程式碼
put
方法 :重要的就是在新增過快取物件後,呼叫trimToSize()
方法,來判斷快取是否已滿,如果滿了就要刪除近期最少使用的演算法.同時執行緒也是安全的。
public final V put(K key, V value) {
//不可為空,否則丟擲異常
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
// 多執行緒 可以使用
synchronized (this) {
//插入的快取物件值加1
putCount++;
//增加已有快取的大小
size += safeSizeOf(key, value);
//向map中加入快取物件
previous = map.put(key, value);
if (previous != null) {
//如果已有快取物件,則快取大小恢復到之前
size -= safeSizeOf(key, previous);
}
}
//entryRemoved()是個空方法,可以自行實現
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//調整快取大小(關鍵方法)
trimToSize(maxSize);
return previous;
}
複製程式碼
- 1、
safeSizeOf
方法,這個sizeof
的方法,就是我們自己需要重寫的,記得圖片載入框架的設計,就會運用到他
private int safeSizeOf(K key, V value) {
// 每一個的需要快取的大小
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
protected int sizeOf(K key, V value) {
return 1;
}
複製程式碼
- 2、調整快取大小(關鍵方法)
trimToSize(maxSize);
maxSize
也就是指定的大小,當if (size <= maxSize) { break; }
這個判斷不成立的時候,就會往下走,迭代器就會去獲取第一個物件,即隊尾的元素,近期最少訪問的元素。然後把它刪除該物件,並更新快取大小map.remove(key);
private void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize) {
break;
}
//迭代器獲取第一個物件,即隊尾的元素,近期最少訪問的元素
Map.Entry<K, V> toEvict = null;
for (Map.Entry<K, V> entry : map.entrySet()) {
toEvict = entry;
}
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
//刪除該物件,並更新快取大小
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
// 空實現
entryRemoved(true, key, value, null);
}
}
複製程式碼
- 關於
get
方法!也是一個同步的方法。
public final V get(K key) {
//key為空丟擲異常
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//獲取對應的快取物件
//get()方法會實現將訪問的元素更新到佇列頭部的功能
// todo LinkedHashMap 裡面已經實現了 如果 新增到頭部去
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
...
}
複製程式碼
LruCache
使用的Demo
,這個Demo
就看看,沒吊用。
public class ImageCache {
//定義LruCache,指定其key和儲存資料的型別
private LruCache<String, Bitmap> mImageCache;
ImageCache() {
//獲取當前程式可以使用的記憶體大小,單位換算為KB
final int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);
//取總記憶體的1/4作為快取
final int cacheSize = maxMemory / 4;
//初始化LruCache
mImageCache = new LruCache<String, Bitmap>(cacheSize) {
//定義每一個儲存物件的大小
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
//獲取資料
public Bitmap getBitmap(String url) {
return mImageCache.get(url);
}
//儲存資料
public void putBitmap(String url, Bitmap bitmap) {
mImageCache.put(url, bitmap);
}
}
複製程式碼
九、SparseArray
-
SparseArray
是android
裡為<Interger,Object>
這樣的Hashmap
而專門寫的類,目的是提高效率,其核心是折半查詢函式(binarySearch
)。SparseArray
僅僅提高記憶體效率,而不是提高執行效率,所以也決定它只適用於android
系統(記憶體對android專案有多重要)SparseArray
不需要開闢記憶體空間來額外儲存外部對映,從而節省記憶體。 -
變數,核心就是兩個陣列:
mKeys
mValues
//是否可以回收,即清理mValues中標記為DELETED的值的元素
private boolean mGarbage = false;
private int[] mKeys; //儲存鍵的陣列
private Object[] mValues; //儲存值的陣列
private int mSize; //當前已經儲存的資料個數
複製程式碼
- 構造方法 :如果
initialCapacity=0
那麼mKeys,mValuse
都初始化為size=0
的陣列,當initialCapacity>0
時,系統生成length=initialCapacity
的value
陣列,同時新建一個同樣長度的key
陣列。
public SparseArray() {
this(10);
}
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
/* ArrayUtils.newUnpaddedObjectArray 的原始碼
public static Object[] newUnpaddedObjectArray(int minLen) {
return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen);
}
*/
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
mSize = 0;
}
複製程式碼
- 關於
put
方法,關鍵是通過二分查詢,查詢相對應的i
角標,如果存在的話,直接賦值新的值,如果不存在的話,取~i
位非運算子(~
): 十進位制變二進位制:原碼--反碼--加一(補碼),相當於 value +1 然後 取反 就可以了.然後就會走到mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
和mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
中,這樣就完成了賦值的過程。
public void put(int key, E value) {
// 二分查詢,這個i的值,
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果找到了,就把這個值給替換上去 ,或者是賦值上去
// 這裡 也就可以解釋出為啥 替換為最新的值
if (i >= 0) {
mValues[i] = value;
} else {
//這裡就是key要插入的位置,上面二分查詢方法提到過
//位非運算子(~)
i = ~i;
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
// 一個新的值 ,就會把key 和 value 和 i值插入到兩個陣列中
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
// todo 然後長度 加上 1 nice
mSize++;
}
}
複製程式碼
get
方法:通過二分查詢法,在mKeys
陣列中查詢key
的位置,然後返回mValues
陣列中對應位置的值,找不到則返回預設值
public E get(int key, E valueIfKeyNotFound) {
// 二分查詢 感覺不像啊 臥槽
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
複製程式碼
delete
其實就是把這個mValues[i]
標記為DELETED
.
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
/*
i>0表示,找到了key對應的下標,否則應該是負數。同時判斷mValues[i] 是不是Object這個物件,如果不是,直接替換為Object(DELETE起到標記刪除位置的作用),並標記 mGarbage=true,注意:這裡delete只操作了values陣列,並沒有去操作key陣列;
*/
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
複製程式碼
removeReturnOld
其實就是多了一步,把要刪除的值返回,其餘同delete
一樣
public E removeReturnOld(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
final E old = (E) mValues[i];
mValues[i] = DELETED;
mGarbage = true;
return old;
}
}
return null;
}
複製程式碼
clear
這裡要留意,clear
只是清空了values
陣列,並沒有操作keys
陣列,這裡也是傳遞的地址值,然後通過for
迴圈,把每個元素清空!
public void clear() {
int n = mSize;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
values[i] = null;
}
mSize = 0;
mGarbage = false;
}
複製程式碼
- 其實還有個方法
append
,新增資料的時候最好去使用它,因為它會判斷下mSize != 0 && key <= mKeys[mSize - 1]
、如果滿足了才會呼叫put
方法,不滿足,直接新增資料,而不是一上來就開始進行二分查詢。
// 要使用這個方法 好點 。
public void append(int key, E value) {
// 判斷了是否 需要 二分查詢,還是直接插入
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
if (mGarbage && mSize >= mKeys.length) {
// 通過gc的方法,把DELETED值的 values 清空
gc();
}
// 可以直接都要這裡來 ,是最節約能量
mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
mValues = GrowingArrayUtils.append(mValues, mSize, value);
mSize++;
}
複製程式碼
- 關於原型模式中的深拷貝的實現,這裡也幫我指明瞭,一定要記得拷貝類中的容器
@Override
@SuppressWarnings("unchecked")
public SparseArray<E> clone() {
SparseArray<E> clone = null;
try {
clone = (SparseArray<E>) super.clone();
// 原型模式的深拷貝 兩個容器的拷貝的過程----!!!
clone.mKeys = mKeys.clone();
clone.mValues = mValues.clone();
} catch (CloneNotSupportedException cnse) {
/* ignore */
}
return clone;
}
複製程式碼
-
其他的
SparseBooleanArray SparseIntArray SparseLongArray
的原理一樣 -
SparseArray
與HashMap
無論是怎樣進行插入,資料量相同時,前者都要比後者要省下一部分記憶體,但是效率呢?----在倒序插入的時候,SparseArray
的插入時間和HashMap
的插入時間遠遠不是一個數量級.由於SparseArray
每次在插入的時候都要使用二分查詢判斷是否有相同的值被插入.因此這種倒序的情況是SparseArray
效率最差的時候. -
附贈一個二分查詢
/**
* 二分查詢
* @param ints 需要被查詢的陣列
* @param length 陣列的長度
* @param value 查詢的值
*/
private int binarySearch(int[] ints, int length, int value) {
int i = 0;
int h = length - 1;
while (i <= h) {
/**
* >>>與>>唯一的不同是它無論原來的最左邊是什麼數,統統都用0填充。
* —比如你的例子,byte是8位的,-1表示為byte型是11111111(補碼錶示法)
* b>>>4就是無符號右移4位,即00001111,這樣結果就是15。
* 這裡相當移動一位,除以二
*/
//中間的角標
final int mid = (i + h) >>> 1;// 第一次 2 第二次 mid=3 第三次mid=4
final int midVal = ints[mid];// 第一次 3 第二次 midVal=4 第三次mid=5
if (midVal < value) {
i = mid + 1;// 第一次 3 第二次 i=4
} else if (value < midVal) {
h = mid - 1;
} else if (value == midVal) {
return mid; //第三次mid=5 返回了
}
}
// 這個取反 ,相當於 value +1 然後 取反 就可以了
return ~value;
}
複製程式碼
- 附贈
System.arraycopy()
的用法
int[] mKeys={10,5,14,5,46};
int[] newKeys=new int[5];
/*
* @param src 源陣列。
* @param srcPos 表示源陣列要複製的起始位置,
* @param dest 目的地陣列。
* @param destPos 在目標資料中的起始位置。
* @param length 要複製的陣列元素的數目。
*/
// todo source of type android.util.SparseArray is not an array
// destPsot +length 不能超過 新的陣列的長度
System.arraycopy(mKeys,0, newKeys, 2, 3);
for (Integer str : newKeys) {
System.out.print("newKeys="+str+" ");
}
複製程式碼
最後說明幾點
ArrayList
的主要消耗是陣列擴容以及在指定位置新增資料,在日常使用時最好是指定大小,儘量減少擴容。更要減少在指定位置插入資料的操作。ArrayList
遍歷的速度快,插入刪除速度慢,隨機訪問的速度快LinkedList
插入,刪除都是移動指標效率很高。查詢需要進行遍歷查詢,效率較低。二分查詢,如果查詢的index的越接近size的一半的話,這樣查詢的效率很低HashMap
是一個執行緒不安全的容器,發生擴容時會出現環形連結串列從而導致死迴圈HashMap
是一個無序的Map
,因為每次根據key
的hashCode
對映到Entry
陣列上,所以遍歷出來的順序並不是寫入的順序。HashMap
遍歷的速度慢,底層決定了,插入刪除的速度快,隨機訪問的速度也比較快ConcurrentHashMap
併發容器,區別就是採用了CAS + synchronized 來保證併發安全性- 位與運算子
&
,把做運算的兩個數都轉化為二進位制的,然後從高位開始比較,如果兩個數都是1
則為1
,否者為0
- 無符號的右移(
>>>
):按照二進位制把數字右移指定數位,高位直接補零,低位移除! a=a|b
等於a|=b
的意思就是把a
和b
按位或然後賦值給a
按位或的意思就是先把a
和b
都換成2
進位制,然後用或操作- 位異或運算(
^
): 運算規則是兩個數轉為二進位制,然後從高位開始比較,如果相同則為0
,不相同則為1
HashSet
底層其實就是HashMap
,只不過是一個value
都一樣的HashSet
.LRU(Least Recently Used)
最近最少使用的,看了原始碼才知道核心是LRUCache
類,這個類的核心其實是LinkedHashMap
類.~i
位非運算子(~
): 十進位制變二進位制:原碼--反碼--加一(補碼),相當於 value +1 然後 取反 就可以了SparseArray
SparseBooleanArray SparseIntArray SparseLongArray
的原理一樣SparseArray
與HashMap
無論是怎樣進行插入,資料量相同時,前者都要比後者要省下一部分記憶體,但是效率呢?----在倒序插入的時候,SparseArray
的插入時間和HashMap
的插入時間遠遠不是一個數量級.由於SparseArray
每次在插入的時候都要使用二分查詢判斷是否有相同的值被插入.因此這種倒序的情況是SparseArray
效率最差的時候.- 二分查詢,是當角標越接近陣列長度的一半,效率越低
- 臥槽,剛看了一下總共將近一萬字,光寫的過程用了16個小時,整理資料大概是10個小時。