面試題(一)Java容器——HashMap HashTable ArrayList LinkedList源碼解讀

1、HashMap 和 HashTable 有什麼區別?

HashMap:

繼承AbstractMap<K,V>類,實現了Map<K,V>, Cloneable, Serializable接口

採用數組+鏈表+紅黑樹實現(jdk1.8後,採用紅黑樹)

  1. 非線程安全
  2. Key可以爲null,但只允許有一個,value可以爲null,不限個數
  3. 默認初始容量爲16,每次擴充,容量變爲原來的2倍
  4. hash計算方式:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  5. 數組下標計算方式:int index = (n - 1) & hash
  6. HashMap使用Iterator進行遍歷

Hashtable

繼承了Dictionary類,實現了Map<K,V>, Cloneable, java.io.Serializable接口

底層結構是Entry數組

  1. 幾乎所有方法都採用synchronized修飾,線程安全

  2. 不允許key或者value爲null,value爲空會直接拋出NPE,Hashtable中key的hash計算方法是直接調用Object中的hashCode()方法,所以key也不能爲空

  3. 默認的初始大小爲11,之後每次擴充,容量變爲原來的2n+1:int newCapacity = (oldCapacity << 1) + 1;

  4. hash計算方法:key.hashCode()

  5. 數組下標計算方法:(hash & 0x7FFFFFFF) % tab.length;

  6. HashTable使用Enumeration遍歷

HashMap:

HashMap主要成員變量:
  1. transient Node<K,V>[] table:這是一個Node類型的數組(也有稱作Hash桶),可以從下面源碼中看到靜態內部類Node在這邊可以看做就是一個節點,多個Node節點構成鏈表,當鏈表長度大於8的時候轉換爲紅黑樹。

    		/**
         * The table, initialized on first use, and resized as
         * necessary. When allocated, length is always a power of two.
         * (We also tolerate length zero in some operations to allow
         * bootstrapping mechanics that are currently not needed.)
         */
        transient Node<K,V>[] table;	//哈希桶
    
  2. transient int size:表示當前HashMap包含的鍵值對數量

  3. transient int modCount:表示當前HashMap修改次數

  4. int threshold:表示當前HashMap能夠承受的最多的鍵值對數量,一旦超過這個數量HashMap就會進行擴容

  5. final float loadFactor:負載因子,用於擴容

  6. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4:默認的table初始容量

  7. static final float DEFAULT_LOAD_FACTOR = 0.75f:默認的負載因子

  8. static final int TREEIFY_THRESHOLD = 8: 鏈表長度大於等於該參數轉紅黑樹

  9. static final int UNTREEIFY_THRESHOLD = 6: 當樹的節點數小於等於該參數轉成鏈表

Node節點:

Node是HashMap的一個內部類,實現了Map.Entry接口,本質上是一個映射關係

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//定位數組索引位置
        final K key;
        V value;
        Node<K,V> next;//鏈表下一個Node
  			
  			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; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        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;
        }
  
    }
哈希衝突:

HashMap採用鏈地址法處理hash衝突,在每個數組元素上都加一個鏈表結構,當數據被Hash後得到數組下標,把數據放在對應下標元素的鏈表上。

hash值:
static final int hash(Object key) {
        int h;
        //第一步 h = key.hashCode() 求hashCode
        //第二步 h ^ (h >>> 16) 高位運算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在這裏插入圖片描述

HashMap中的put方法:

在這裏插入圖片描述

HashMap中的get方法:
  1. 對key的hashCode()做hash運算,計算index。
  2. 如果在bucket⾥的第⼀個節點⾥直接命中,則直接返回。
  3. 如果有衝突,則通過key.equals(k)去查找對應的Entry
  4. 若爲樹,則在樹中通過key.equals(k)查找,O(logn)
  5. 若爲鏈表,則在鏈表中通過key.equals(k)查找,O(n)
		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;
    }

2、數組和鏈表的區別

ArrayList:

  1. ArrayList非線程安全,底層是一個Object[],添加到ArrayList中的數據保存在了elementData屬性中。
  2. 當調用new ArrayList<>()時,將一個空數組{}賦值給了elementData,這個時候集合的長度size爲默認長度0;
  3. 當調用new ArrayList<>(100)時,根據傳入的長度,new一個Object[100]賦值給elementData,當然如果玩兒的話,傳了一個0,那麼將一個空數組{}賦值給了elementData;
  4. 當調用new ArrayList<>(new HashSet())時,根據源碼,我們可知,可以傳遞任何實現了Collection接口的類,將傳遞的集合調用toArray()方法轉爲數組內賦值給elementData;
向ArrayList添加元素:
		public boolean add(E e) {//直接添加數據
        ensureCapacityInternal(size + 1);  //判斷Object[]數組是否有足夠空間
        elementData[size++] = e;//在對應位置添加元素
        return true;
    }
    
    public void add(int index, E element) {
        rangeCheckForAdd(index);// 判斷index 是否有效

        ensureCapacityInternal(size + 1);  //判斷Object[]數組是否有足夠空間
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);//將index 後面的數據都往後移一位
        elementData[index] = element;
        size++;
    }

		private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private void ensureExplicitCapacity(int minCapacity) {
      modCount++;
      // overflow-conscious code
      if (minCapacity - elementData.length > 0)
        grow(minCapacity);
    }

    private void grow(int minCapacity) {//進行數組擴容
        // overflow-conscious code
        int oldCapacity = elementData.length;
        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);
    }

擴容規則爲“數組當前足夠的最小容量 + (數組當前足夠的最小容量 / 2)”,即數組當前足夠的最小容量 * 1.5,當然有最大值的限制。

在ArrayList中查找元素:
		public E get(int index) {
        rangeCheck(index);//需要判斷傳入的數組下標是否越界
        return elementData(index);
    }
    
    E elementData(int index) {//通過下標查找,同時進行類型轉換
        return (E) elementData[index];
    }

		private void rangeCheck(int index) {//判斷傳入的數組下標是否越界
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
移除ArrayList中元素:
public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;//計算數組中需要移動的位數
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {//遍歷底層數組elementData
                fastRemove(index);//獲取下標,調用remove(int index)
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {//遍歷底層數組elementData
                fastRemove(index);//獲取下標,調用remove(int index)
                return true;
            }
    }
    return false;
}

LinkedList:

  1. 繼承於 AbstractSequentialList ,本質上面與繼承 AbstractList 沒有什麼區別,AbstractSequentialList 完善了 AbstractList 中沒有實現的方法。

  2. Serializable:成員變量 Node 使用 transient 修飾,通過重寫read/writeObject 方法實現序列化。

  3. Cloneable:重寫clone()方法,通過創建新的LinkedList 對象,遍歷拷貝數據進行對象拷貝。

  4. Deque:實現了Collection 大家庭中的隊列接口,說明他擁有作爲雙端隊列的功能。

  5. LinkedList與ArrayList最大的區別就是LinkedList中實現了Collection中的 Queue(Deque)接口 擁有作爲雙端隊列的功能

  6. ListedList採用的是鏈式存儲。鏈式存儲就會定一個節點Node。包括三部分前驅節點、後繼節點以及data值。所以存儲存儲的時候他的物理地址不一定是連續的。

LinkedList集合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實現了Deque(間接實現了Qeque接口),Deque是一個雙向對列,爲LinedList提供了從對列兩端訪問元素的方法

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
初始化:
//初始化長度爲0
transient int size = 0;
//有前後節點
transient Node<E> first;
transient Node<E> last;
向LinkedList添加元素:
public boolean add(E e) {
  linkLast(e);//調用linkLast方法,添加位置是集合最後
  return true;
}

void linkLast(E e) {
 		// 將最後一個元素賦值(引用傳遞)給節點l final修飾符  修飾的屬性賦值之後不能被改變
  	final Node<E> l = last;
 		// 調用節點的有參構造方法創建新節點 保存添加的元素 
  	final Node<E> newNode = new Node<>(l, e, null);
    //此時新節點是最後一位元素 將新節點賦值給last
    last = newNode;
    //如果l是null 意味着這是第一次添加元素 那麼將first賦值爲新節點,這個list只有一個元素存儲元素 
  	//開始元素和最後元素均是同一個元素
    if (l == null)
      first = newNode;
    else
      //如果不是第一次添加,將新節點賦值給l(添加前的最後一個元素)的next
      l.next = newNode;
    //長度+1
    size++;
    //修改次數+1
    modCount++;
}
添加到指定位置:
public void add(int index, E element) {
    //下標越界檢查
    checkPositionIndex(index);
  	//如果是向最後添加 直接調用linkLast
    if (index == size)
      	linkLast(element);
    //反之 調用linkBefore
    else
      	linkBefore(element, node(index));
}

//在指定元素之前插入元素
void linkBefore(E e, Node<E> succ) {
    // assert succ != null; 假設斷言 succ不爲null
    //定義一個節點元素保存succ的prev引用 也就是它的前一節點信息
    final Node<E> pred = succ.prev;
    //創建新節點 節點元素爲要插入的元素e prev引用就是pred 也就是插入之前succ的前一個元素 next是succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    //此時succ的上一個節點是插入的新節點 因此修改節點指向
    succ.prev = newNode;
   // 如果pred是null 表明這是第一個元素
    if (pred == null)
      	//成員屬性first指向新節點
      	first = newNode;
    //反之
    else
      	//節點前元素的next屬性指向新節點
      	pred.next = newNode;
    //長度+1
    size++;
    modCount++;
}
LinkedList列表中,查找元素:
public E get(int index) {
    //檢查下標元素是否存在 實際上就是檢查下標是否越界
    checkElementIndex(index);
    //如果沒有越界就返回對應下標節點的item 也就是對應的元素
    return node(index).item;
}

//下標越界檢查 如果越界就拋異常
private void checkElementIndex(int index) {
  	if (!isElementIndex(index))
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
  	return index >= 0 && index < size;
}

//指定下標的非空節點
Node<E> node(int index) {
    //如果index小於size的二分之一  從前開始查找(向後查找)  反之向前查找  
    if (index < (size >> 1)) {
      	Node<E> x = first;
      	//遍歷
      	for (int i = 0; i < index; i++)
        //每一個節點的next都是他的後一個節點引用 遍歷的同時x會不斷的被賦值爲節點的下一個元素  
        //遍歷到index是拿到的就是index對應節點的元素
        	x = x.next;
      	return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
          	x = x.prev;
        return x;
     }
}

體現了雙向鏈表的優越性,可以從前也可以從後開始遍歷

3、用面向對象的方法求出數組中重複 value 的個數

				//原始數組
        int arr[] = {1,4,1,4,2,5,4,5,8,7,8,77,88,5,4,9,6,2,4,1,5};

        //利用hashmap記錄每個數字出現的次數
        Map<Integer, Integer> map = new HashMap<>();

        //循環數組
        for (int i : arr) {
            //判斷當前數字是否已經統計過,如果統計過,取出出現的次數,加1 ,
            Integer temp = map.get(i);
            int ov = 0;
            if(temp != null){
                ov = temp.intValue();
            }
            Integer v = new Integer(ov + 1 );
            map.put(i, v );
        }

        //循環輸出
        Set<Integer> entry = map.keySet();
        for (Integer key : entry) {
            System.out.println(key + " 出現了 : " + map.get(key) + "  次");
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章