java面試基礎(一)容器

一、List

ArrayList

1、 ArrayList中的元素由底層數組承載
2、 如果使用午餐構造方法,那麼默認調用this(10),即,若不指定list長度,默認爲10

   public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this(10);
    }

3、ArrayList參數爲Collection的構造方法底層調用的是Arrays.copyOf=>System.arraycopy方法
4、ArrayList通過size成員變量來記錄元素個數
5、indexOf方法需要注意,如果傳入的參數爲null,那麼會返回數組中第一個爲null的索引,如果不存在,則返回-1

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

6、數組長度擴容,如果需要擴容,則默認擴容爲原來的1.5倍

   private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //這裏就是擴容1.5倍的地方
        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);
    }

7、每次add或者remove,底層都會調用System.ayyayCopy方法,這也就是爲什麼ArrayList add和remove方法效率低下的原因
8、如果需要在循環的時候add或者remove,記住使用Iterator
至於爲什麼??以後再去深究
9、ArrayList沒有加鎖,併發時,會存在問題

LinkedList

1、底層由鏈表實現,維護了頭指針和尾指針
2、add方法會將新Node加到鏈表的末尾

    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++;
    }

3、indexOf 參數若是null,返回鏈表中第一個爲null的索引,若不存在,返回-1
4、toArray使用的是new Object[size]結合for循環完成的
5、未使用synchronize,線程不安全。

vector

1、ArrayList的線程安全版,通過對方法加鎖實現
2、擴容時的策略與ArrayList略有不同,vector維護了一個capacityIncrement變量用來控制每次擴容時擴容量,若爲0,則默認擴充爲原來的2倍,ArrayList選擇默認擴容爲原來的1.5倍

stack

繼承了Vector,使用數組構造的堆結構

2、Set

HashSet

底層維護一個HashMap,只使用HashMap的key部分,value部分置爲Object對象

TreeSet

底層維護一個TreeMap

3、Map

HashMap

1、底層由Entry數組和鏈表組成
2、實例化HashMap需要指定初始容量(默認16)和初始加載因子(默認0.75)
這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中桶的數量,初始容量是創建哈希表時的容量。
加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度,它衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。
對於使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是查找效率的降低;
如果負載因子太小,那麼散列表的數據將過於稀疏,對空間造成嚴重浪費。系統默認負載因子爲0.75,一般情況下我們是無需修改的。
3、如果put方法的鍵值對中,key爲null,對索引爲0的鏈表進行for循環,如果沒找到則將這個元素添加到talbe[0]鏈表的表頭。
4、put方法(key非null)過程
計算key的hash值(通過擾動函數保留高位信息,避免h^(length-1)出現大概率碰撞)—>通過indexFor方法計算索引—>查找相關索引對應的鏈表是否含有key,若已存在,則覆蓋value,若不存在,則將新鍵值對置於鏈表第一個元素之後
5、put擴容
若出現 size >= threshold的情況,默認會將容量擴容爲原來的兩倍;

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

擴容結束後,需要重新計算hash,並將鍵值對置於新的Entry數組的相應位置上。

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

6、解決hash碰撞常用的方法:鏈地址法(即HashMap所採用)、開放地址法(在原有hash結果的基礎上+d,然後再取餘)、再hash(即使用多個hash方法)

ConcurrentHashMap

1、ConcurrentHashMap沒有鎖住整個hash表,而是使用分段鎖。其內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的Hashtable,它們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以併發進行。
2、有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。這裏“按順序”是很重要的,否則極有可能出現死鎖
3、對於一個key,需要經過三次hash操作,才能最終定位這個元素的位置,這三次hash分別爲:

  • 對於一個key,先進行一次hash操作,得到hash值h1,也即h1 = hash1(key);
  • 將得到的h1的高几位進行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通過h2能夠確定該元素的放在哪個Segment;
  • 將得到的h1進行第三次hash,得到hash值h3,也即h3 = hash3(h1),通過h3能夠確定該元素放置在哪個HashEntry。

4、concurrencyLevel、sshift、ssize、segmentShift 、segmentMask

  • concurrencyLevel 併發級別
    確定segment[]長度(ssize,ssize是大於等於concurrencyLevel的第一個2的n次方數)
  • sshift 即2的sshift次方 = ssize
  • segmentShift = 32-sshift
  • segmentMask = ssize - 1
    5、獲取segment數組索引
    int j = (hash >>> segmentShift) & segmentMask;

6、put方法加鎖,get方法不加鎖(通過volatile關鍵字控制HashEntry中value的值來保證線程安全)
7、size、containsValue方法加鎖機制:前面三次不對segment加鎖,若出現modCount(s1) != modCount(s2) !=modCount(s3),則對所有segment加鎖,來獲取size或者進行其他操作
8、ConcurrentHashMap中的key和value值都不能爲null

LinkedHashMap

1、LinkedHashMap可以看做是HashMap和LinkedList的融合體,它在自己實現的Entry中新增了befor和after兩個成員,用來維護雙向鏈表
2、可以用來實現LRU緩存
3、可以記錄訪問和插入的順序(默認記錄插入順序)

HashTable

1、HashTable不支持null 鍵(若爲null,會自動拋異常)
2、HashTable默認擴容爲原來的2n+1,這是因爲其與HashMap策略不同,HashTable選擇素數,以降低碰撞率,但是這樣會導致,求Entry[]索引時比HashTable效率低(因爲除法運算速度遠遠小於位運算速度)
3、使用了synchronized保證了線程安全

TreeMap

1、TreeMap基於紅黑樹實現。紅黑樹是一種二叉搜索樹,紅黑樹與AVL(平衡二叉樹)相比,其旋轉次數較少。
2、紅黑樹的時間複雜度爲O(log(n)),因爲給定n個節點,紅黑樹的高度最大爲2(log(n+1))=>log(n)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章