String深入解析
String具有不變性的原因:
- String被final修飾,它不可能被繼承,也就是任何對String的操作方法,都不會被繼承覆寫
- String中保存數據的是一個char數組的value,它被final修飾,它的內存地址一旦賦值無法修改
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
String相等判斷源碼
public boolean equals(Object anObject) {
// 判斷內存地址是否相同
if (this == anObject) {
return true;
}
// 待比較的對象是否是 String,如果不是 String,直接返回不相等
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
// 兩個字符串的長度是否相等,不等則直接返回不相等
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 依次比較每個字符是否相等,若有一個不等,直接返回不相等
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
- 判斷類型是否相同
- 判斷長度是否相同
- 比較每一個字符是否相等
當上述3個條件均成立時,兩個String相等
String的replace和replaceAll區別在於前者入參可以是char也可以是String,此外replaceAll是支持正則表達式的,效率上replace高效
Long
Long自己實現了一種緩存機制,緩存了-128到127內的所有Long,如果值在Long,則從緩存中取,不會被初始化
Long中的靜態內部類實現了LongCache
private static class LongCache {
private LongCache(){}
// 緩存,范圍從 -128 到 127,+1 是因為有個 0
static final Long cache[] = new Long[-(-128) + 127 + 1];
// 容器初始化時,進行加載
static {
// 緩存 Long 值,注意這里是 i - 128 ,所以再拿的時候就需要 + 128
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
使用Long時,推薦valueOf,少用parseLong方法,因為前者如果緩存命中從緩存取值,而后者不會
static
static修飾的方法時,要注意線程安全,static修飾的工具類方法要避免使用到方法外部的公共變量。工具類方法建議使用public final static
Arrays
Arrays二分查詢方法能快速定位元素
其二分查找底層如下:
// a:我們要搜索的數組,fromIndex:從那里開始搜索,默認是0; toIndex:搜索到何時停止,默認是數組大小
// key:我們需要搜索的值 c:外部比較器
private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex,
T key, Comparator<? super T> c) {
// 如果比較器 c 是空的,直接使用 key 的 Comparable.compareTo 方法進行排序
// 假設 key 類型是 String 類型,String 默認實現了 Comparable 接口,就可以直接使用 compareTo 方法進行排序
if (c == null) {
// 這是另外一個方法,使用內部排序器進行比較的方法
return binarySearch0(a, fromIndex, toIndex, key);
}
int low = fromIndex;
int high = toIndex - 1;
// 開始位置小於結束位置,就會一直循環搜索
while (low <= high) {
// 假設 low =0,high =10,那么 mid 就是 5,所以說二分的意思主要在這里,每次都是計算索引的中間值
int mid = (low + high) >>> 1;
T midVal = a[mid];
// 比較數組中間值和給定的值的大小關系
int cmp = c.compare(midVal, key);
// 如果數組中間值小於給定的值,說明我們要找的值在中間值的右邊
if (cmp < 0)
low = mid + 1;
// 我們要找的值在中間值的左邊
else if (cmp > 0)
high = mid - 1;
else
// 找到了
return mid; // key found
}
// 返回的值是負數,表示沒有找到
return -(low + 1); // key not found.
}
Arrays提供了很多parallel開頭的方法,比如parallelSort方法,方法底層有判斷,只有數據量大於8192時,才會真正走並行的實現。
Collections
Colletcions.max
ArrayList
ArrayList無參構造器初始化時,默認大小是空數組,當第1次add的時候數組擴容到大小為10。如果第一次初始化如果大小<10,那么初始化大小按10計算
擴容后的大小是原來的1.5倍
擴容源碼如下
private void ensureCapacityInternal(int minCapacity) {
//如果初始化數組大小時,有給定初始值,以給定的大小為准,不走 if 邏輯
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//確保容積足夠
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//記錄數組被修改
modCount++;
// 如果我們期望的最小容量大於目前數組的長度,那么就擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//擴容,並把現有數據拷貝到新的數組里面去
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// oldCapacity >> 1 是把 oldCapacity 除以 2 的意思
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果擴容后的值 < 我們的期望值,擴容后的值就等於我們的期望值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果擴容后的值 > jvm 所能分配的數組的最大值,那么就用 Integer 的最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 通過復制進行擴容
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList無參構造器構造,現在add一個值進去,此處的數組大小是1,下一次擴容前最大可用大小是10。
數組初始化,初加入一個值后,如果使用addAll方法,一下子加入15個值,那么最終數組的大小是16。因為
// newCapacity 本次擴容的大小,minCapacity 我們期望的數組最小大小
// 如果擴容后的值 < 我們的期望值,我們的期望值就等於本次擴容的大小
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
toArray方法
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Integer[] a1 = new Integer[3];
Integer[] a2 = list.toArray(a1);
System.out.println(a1.hashCode());
System.out.println(a2.hashCode());
System.out.println("================================");
Integer[] a3 = new Integer[2];
Integer[] a4 = list.toArray(a3);
System.out.println(a3.hashCode());
System.out.println(a4.hashCode());
}
可以發現toArray方法,如果入參數組大小小於集合大小,那么它就會返回一個新的數組,否則返回則就是入參數組
LinkedList
從頭節點增加
// 從頭部追加
private void linkFirst(E e) {
// 頭節點賦值給臨時變量
final Node<E> f = first;
// 新建節點,前一個節點指向null,e 是新建節點,f 是新建節點的下一個節點,目前值是頭節點的值
final Node<E> newNode = new Node<>(null, e, f);
// 新建節點成為頭節點
first = newNode;
// 頭節點為空,就是鏈表為空,頭尾節點是一個節點
if (f == null)
last = newNode;
//上一個頭節點的前一個節點指向當前節點
else
f.prev = newNode;
size++;
modCount++;
}
從頭節點刪除
//從頭刪除節點 f 是鏈表頭節點
private E unlinkFirst(Node<E> f) {
// 拿出頭節點的值,作為方法的返回值
final E element = f.item;
// 拿出頭節點的下一個節點
final Node<E> next = f.next;
//幫助 GC 回收頭節點
f.item = null;
f.next = null;
// 頭節點的下一個節點成為頭節點
first = next;
//如果 next 為空,表明鏈表為空
if (next == null)
last = null;
//鏈表不為空,頭節點的前一個節點指向 null
else
next.prev = null;
//修改鏈表大小和版本
size--;
modCount++;
return element;
}
節點查詢,如果 index 處於隊列的前半部分,從頭開始找,size >> 1 是 size 除以 2 的意思。否則后半部分查詢
// 根據鏈表索引位置查詢節點
Node<E> node(int index) {
// 如果 index 處於隊列的前半部分,從頭開始找,size >> 1 是 size 除以 2 的意思。
if (index < (size >> 1)) {
Node<E> x = first;
// 直到 for 循環到 index 的前一個 node 停止
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {// 如果 index 處於隊列的后半部分,從尾開始找
Node<E> x = last;
// 直到 for 循環到 index 的后一個 node 停止
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
LinkedList實現了Queue接口,在鏈表為空時返回值也不太一樣
因為LinkedList實現雙向的迭代方向,新增了一個迭代接口:ListIterator
ArrayList有最大容量,為Integer的最大值,LinkedList底層是雙向鏈表,理論可無限大,但源碼中LinkedList實際大小用int類型,這也說明了LinkedList不能超過Integer的最大值。
HashMap
底層數據結構是:數組+鏈表+紅黑樹。其中當鏈表的長度大於等於8時,鏈表會轉化成紅黑樹,當紅黑樹大小小於等於6時,紅黑樹轉化成鏈表。
HashMap允許null值。
//初始容量為 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//負載因子默認值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶上的鏈表長度大於等於8時,鏈表轉化成紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//桶上的紅黑樹大小小於等於6時,紅黑樹轉化成鏈表
static final int UNTREEIFY_THRESHOLD = 6;
//當數組容量大於 64 時,鏈表才會轉化成紅黑樹
static final int MIN_TREEIFY_CAPACITY = 64;
//記錄迭代過程中 HashMap 結構是否發生變化,如果有變化,迭代時會 fail-fast
transient int modCount;
//HashMap 的實際大小,可能不准(因為當你拿到這個值的時候,可能又發生了變化)
transient int size;
//存放數據的數組
transient Node<K,V>[] table;
// 擴容的門檻,有兩種情況
// 如果初始化時,給定數組大小的話,通過 tableSizeFor 方法計算,數組大小永遠接近於 2 的冪次方,比如你給定初始化大小 19,實際上初始化大小為 32,為 2 的 5 次方。
// 如果是通過 resize 方法進行擴容,大小 = 數組容量 * 0.75
int threshold;
//鏈表的節點
static class Node<K,V> implements Map.Entry<K,V> {
//紅黑樹的節點
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key 在數組中的位置公式:tab[(n - 1) & hash]
計算hash值時,為什么需要右移16位
hash算法是h^(h>>>16),這是為了h的高低16位都能參與計算,計算出的hash更分散
為什么把數組長度取模操作換成了&操作
取模操作處理器計算慢,處理器對&操作比較擅長
為什么提倡數組大小是2的冪次方
因為只有大小是2的冪次方,才能使hash值%n(數組大小)==(n-1)&hash
hashmap的添加元素
// 入參 hash:通過 hash 算法計算出來的值。
// 入參 onlyIfAbsent:false 表示即使 key 已經存在了,仍然會用新值覆蓋原來的值,默認為 false
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// n 表示數組的長度,i 為數組索引下標,p 為 i 下標位置的 Node 值
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果數組為空,使用 resize 方法初始化
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);
// 如果當前索引位置有值的處理方法,即我們常說的如何解決 hash 沖突
else {
// e 當前節點的臨時變量
Node<K,V> e; K k;
// 如果 key 的 hash 和值都相等,直接把當前下標位置的 Node 值賦值給臨時變量
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) {
// e = p.next 表示從頭開始,遍歷鏈表
// p.next == null 表明 p 是鏈表的尾節點
if ((e = p.next) == null) {
// 把新節點放到鏈表的尾部
p.next = newNode(hash, key, value, null);
// 當鏈表的長度大於等於 8 時,鏈表轉紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 鏈表遍歷過程中,發現有元素和新增的元素相等,結束循環
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//更改循環的當前元素,使 p 在遍歷過程中,一直往后移動。
p = e;
}
}
// 說明新節點的新增位置已經找到了
if (e != null) {
V oldValue = e.value;
// 當 onlyIfAbsent 為 false 時,才會覆蓋值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回老值
return oldValue;
}
}
// 記錄 HashMap 的數據結構發生了變化
++modCount;
//如果 HashMap 的實際大小大於擴容的門檻,開始擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashMap、TreeMap、LinkedHashMap三者異同
相同點:
- 在特定情況下,都使用紅黑樹
- 底層的hash算法相同
- 在迭代過程中,如果Map的數據結果被改動,都會報ConcurrentModificationException
不同點: - HashMap以數組為主,查詢快,TreeMap以紅黑樹為主,利用紅黑樹特點進行排序,LinkedHashMap在HashMap上增加鏈表結構,實現順序插入和最少訪問刪除
- 三者使用場景不同
- api上層略有不同
HashSet
它底層基於HashMap實現,key為HashMap的key,value為
private static final Object PRESENT = new Object();
// 對 HashMap 的容量進行了計算
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
HashSet的初始大小為取最大值(期望的值 / 0.75 + 1,默認值 16),expectSize * 0.75 + 1剛好不用擴容,如果對HashMap的初始化大小值選取,可借鑒此公式。
CopyOnWriteArrayList
CopyOnWriteArrayList通過鎖+數組拷貝+volatile保證線程安全,它線程安全的原因是每次操作都是在新拷貝數組上操作,當操作完成后,再把新數組引用賦值給原數組。當用迭代器操作時,如果有另一線程對CopyOnWriteArrayList進行增刪操作,迭代器未結束時持有的仍是舊數組,所以它是線程安全的。
注意:volatile直接修飾數組時,當數組某一元素改變仍無法保證其可見性,只有當數組引用發生改變時才保證其可見性,這就是為什么源碼setArray方法是修改數組的引用
CopyOnWriteArrayList的add方法
// 添加元素到數組尾部
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 加鎖
lock.lock();
try {
// 得到所有的原數組
Object[] elements = getArray();
int len = elements.length;
// 拷貝到新數組里面,新數組的長度是 + 1 的,因為新增會多一個元素
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 在新數組中進行賦值,新元素直接放在數組的尾部
newElements[len] = e;
// 替換掉原來的數組
setArray(newElements);
return true;
// finally 里面釋放鎖,保證即使 try 發生了異常,仍然能夠釋放鎖
} finally {
lock.unlock();
}
}
批量刪除
// 批量刪除包含在 c 中的元素
public boolean removeAll(Collection<?> c) {
if (c == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 說明數組有值,數組無值直接返回 false
if (len != 0) {
// newlen 表示新數組的索引位置,新數組中存在不包含在 c 中的元素
int newlen = 0;
Object[] temp = new Object[len];
// 循環,把不包含在 c 里面的元素,放到新數組中
for (int i = 0; i < len; ++i) {
Object element = elements[i];
// 不包含在 c 中的元素,從 0 開始放到新數組中
if (!c.contains(element))
temp[newlen++] = element;
}
// 拷貝新數組,變相的刪除了不包含在 c 中的元素
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
源碼思想借鑒:批量刪除是把先找出已存在的元素組成數組,然后再把原數組引用指向新數組。避免直接循環單個元素刪除造成頻繁數組拷貝
ConcurrentHashMap
添加元素
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//計算hash
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//table是空的,進行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果當前索引位置沒有值,直接創建
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//cas 在 i 位置創建新的元素,當 i 位置是空時,即能創建成功,結束for自循,否則繼續自旋
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果當前槽點是轉移節點,表示該槽點正在擴容,就會一直等待擴容完成
//轉移節點的 hash 值是固定的,都是 MOVED
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//槽點上有值的
else {
V oldVal = null;
//鎖定當前槽點,其余線程不能操作,保證了安全
synchronized (f) {
//這里再次判斷 i 索引位置的數據沒有被修改
//binCount 被賦值的話,說明走到了修改表的過程里面
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;
}
}
}
//紅黑樹,這里沒有使用 TreeNode,使用的是 TreeBin,TreeNode 只是紅黑樹的一個節點
//TreeBin 持有紅黑樹的引用,並且會對其加鎖,保證其操作的線程安全
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//滿足if的話,把老的值給oldVal
//在putTreeVal方法里面,在給紅黑樹重新着色旋轉的時候
//會鎖住紅黑樹的根節點
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//binCount不為空,並且 oldVal 有值的情況,說明已經新增成功了
if (binCount != 0) {
// 鏈表是否需要轉化成紅黑樹
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
//這一步幾乎走不到。槽點已經上鎖,只有在紅黑樹或者鏈表新增失敗的時候
//才會走到這里,這兩者新增都是自旋的,幾乎不會失敗
break;
}
}
}
//check 容器是否需要擴容,如果需要去擴容,調用 transfer 方法去擴容
//如果已經在擴容中了,check有無完成
addCount(1L, binCount);
return null;
}
LinkedBlockingQueue
put方法
// 把e新增到隊列的尾部。
// 如果有可以新增的空間的話,直接新增成功,否則當前線程陷入等待
public void put(E e) throws InterruptedException {
// e 為空,拋出異常
if (e == null) throw new NullPointerException();
// 預先設置 c 為 -1,約定負數為新增失敗
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 設置可中斷鎖
putLock.lockInterruptibly();
try {
// 隊列滿了
// 當前線程阻塞,等待其他線程的喚醒(其他線程 take 成功后就會喚醒此處被阻塞的線程)
while (count.get() == capacity) {
// await 無限等待
notFull.await();
}
// 隊列沒有滿,直接新增到隊列的尾部
enqueue(node);
// 新增計數賦值,注意這里 getAndIncrement 返回的是舊值
// 這里的 c 是比真實的 count 小 1 的
c = count.getAndIncrement();
// 如果鏈表現在的大小 小於鏈表的容量,說明隊列未滿
// 可以嘗試喚醒一個 put 的等待線程
if (c + 1 < capacity)
notFull.signal();
} finally {
// 釋放鎖
putLock.unlock();
}
// c==0,代表隊列里面有一個元素
// 會嘗試喚醒一個take的等待線程
if (c == 0)
signalNotEmpty();
}
// 入隊,把新元素放到隊尾
private void enqueue(Node<E> node) {
last = last.next = node;
}