1、HashMap 和 HashTable 有什麼區別?
HashMap:
繼承AbstractMap<K,V>
類,實現了Map<K,V>, Cloneable, Serializable
接口
採用數組+鏈表+紅黑樹實現(jdk1.8後,採用紅黑樹)
- 非線程安全
- Key可以爲null,但只允許有一個,value可以爲null,不限個數
- 默認初始容量爲16,每次擴充,容量變爲原來的2倍
- hash計算方式:
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
- 數組下標計算方式:
int index = (n - 1) & hash
- HashMap使用Iterator進行遍歷
Hashtable
繼承了Dictionary類,實現了Map<K,V>, Cloneable, java.io.Serializable接口
底層結構是Entry數組
-
幾乎所有方法都採用synchronized修飾,線程安全
-
不允許key或者value爲null,value爲空會直接拋出NPE,Hashtable中key的hash計算方法是直接調用Object中的hashCode()方法,所以key也不能爲空
-
默認的初始大小爲11,之後每次擴充,容量變爲原來的2n+1:
int newCapacity = (oldCapacity << 1) + 1;
-
hash計算方法:
key.hashCode()
-
數組下標計算方法:
(hash & 0x7FFFFFFF) % tab.length;
-
HashTable使用Enumeration遍歷
HashMap:
HashMap主要成員變量:
-
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; //哈希桶
-
transient int size:表示當前HashMap包含的鍵值對數量
-
transient int modCount:表示當前HashMap修改次數
-
int threshold:表示當前HashMap能夠承受的最多的鍵值對數量,一旦超過這個數量HashMap就會進行擴容
-
final float loadFactor:負載因子,用於擴容
-
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4:默認的table初始容量
-
static final float DEFAULT_LOAD_FACTOR = 0.75f:默認的負載因子
-
static final int TREEIFY_THRESHOLD = 8: 鏈表長度大於等於該參數轉紅黑樹
-
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方法:
- 對key的hashCode()做hash運算,計算index。
- 如果在bucket⾥的第⼀個節點⾥直接命中,則直接返回。
- 如果有衝突,則通過key.equals(k)去查找對應的Entry
- 若爲樹,則在樹中通過key.equals(k)查找,O(logn)
- 若爲鏈表,則在鏈表中通過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:
- ArrayList非線程安全,底層是一個Object[],添加到ArrayList中的數據保存在了elementData屬性中。
- 當調用
new ArrayList<>()
時,將一個空數組{}賦值給了elementData,這個時候集合的長度size爲默認長度0; - 當調用
new ArrayList<>(100)
時,根據傳入的長度,new一個Object[100]賦值給elementData,當然如果玩兒的話,傳了一個0,那麼將一個空數組{}賦值給了elementData; - 當調用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:
-
繼承於 AbstractSequentialList ,本質上面與繼承 AbstractList 沒有什麼區別,AbstractSequentialList 完善了 AbstractList 中沒有實現的方法。
-
Serializable:成員變量 Node 使用 transient 修飾,通過重寫read/writeObject 方法實現序列化。
-
Cloneable:重寫clone()方法,通過創建新的LinkedList 對象,遍歷拷貝數據進行對象拷貝。
-
Deque:實現了Collection 大家庭中的隊列接口,說明他擁有作爲雙端隊列的功能。
-
LinkedList與ArrayList最大的區別就是LinkedList中實現了Collection中的 Queue(Deque)接口 擁有作爲雙端隊列的功能
-
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) + " 次");
}