java源碼解析


String深入解析

String具有不變性的原因:

  1. String被final修飾,它不可能被繼承,也就是任何對String的操作方法,都不會被繼承覆寫
  2. 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;
}
  1. 判斷類型是否相同
  2. 判斷長度是否相同
  3. 比較每一個字符是否相等
    當上述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三者異同
相同點:

  1. 在特定情況下,都使用紅黑樹
  2. 底層的hash算法相同
  3. 在迭代過程中,如果Map的數據結果被改動,都會報ConcurrentModificationException
    不同點:
  4. HashMap以數組為主,查詢快,TreeMap以紅黑樹為主,利用紅黑樹特點進行排序,LinkedHashMap在HashMap上增加鏈表結構,實現順序插入和最少訪問刪除
  5. 三者使用場景不同
  6. 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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM