一.简介
HashMap实际上是一个“链表散列”的数据结构。JDK1.8以前:HashMap底层是用数组+双向链表实现的。JDK1.8及以后:HashMap底层是用数组+双向链表+红黑树实现的。
数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;
链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
HashMap综合应用了这两种数据结构,实现了寻址容易,插入删除也容易。
二.HashMap常用方法
1.put(K key, V value):以键值对的形式向HashMap中添加元素
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("1", "张三");
2.putAll(Map<? extends K, ? extends V> m):将一个Map全部添加到另一个Map
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("1", "张三");
HashMap<String, String> allHashMap = new HashMap<String, String>();
allHashMap.putAll(hashMap);
3.size():获取集合长度
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("1", "张三");
int num = hashMap.size();
4.isEmpty():集合是否为空
HashMap<String, String> hashMap = new HashMap<String, String>();
boolean b = hashMap.isEmpty();
5.get(Object key):根据键获取对应的值
HashMap<String, String> hashMap = new HashMap<String, String>();
String value = hashMap.get("1");
6.containsKey(Object key):集合中是否包含key
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("1", "张三");
hashMap.put("2", "李四");
hashMap.put("3", "王五");
hashMap.put("4", "赵六");
boolean b = hashMap.containsKey("3");
boolean c = hashMap.containsKey("6");
Log.d("TAG", "b----:" + b);
Log.d("TAG", "c----:" + c);
b----:true
c----:false
7.containsValue(Object value):集合中是否包含value
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("1", "张三");
hashMap.put("2", "李四");
hashMap.put("3", "王五");
hashMap.put("4", "赵六");
boolean b = hashMap.containsValue("张三");
boolean c = hashMap.containsValue("6");
Log.d("TAG", "b----:" + b);
Log.d("TAG", "c----:" + c);
b----:true
c----:false
8.remove(Object key):删除集合中key对应的value
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("1", "张三");
hashMap.put("2", "李四");
hashMap.put("3", "王五");
hashMap.put("4", "赵六");
int num1 = hashMap.size();
Log.d("TAG", "num1----:" + num1);
hashMap.remove("2");
int num2 = hashMap.size();
Log.d("TAG", "num2----:" + num2);
num1----:4
num2----:3
9.clear():清空集合元素
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("1", "张三");
hashMap.put("2", "李四");
hashMap.put("3", "王五");
hashMap.put("4", "赵六");
int num1 = hashMap.size();
Log.d("TAG", "num1----:" + num1);
hashMap.clear();
int num2 = hashMap.size();
Log.d("TAG", "num2----:" + num2);
num1----:4
num2----:0
10.keySet():遍历集合 获取key 然后通过获取的key获得相应的value
public class CollectionActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_collection);
initArrayList();
}
public void initArrayList() {
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("1", "张三");
hashMap.put("2", "李四");
hashMap.put("3", "王五");
hashMap.put("4", "赵六");
Set<String> set = hashMap.keySet();
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
String value = hashMap.get(key);
Log.i("TAG", "key----:" + key + "----value----:" + value);
}
}
}
I/TAG: key----:1----value----:张三
I/TAG: key----:2----value----:李四
I/TAG: key----:3----value----:王五
I/TAG: key----:4----value----:赵六
11.values():遍历集合 获取value
public class CollectionActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_collection);
initArrayList();
}
public void initArrayList() {
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("1", "张三");
hashMap.put("2", "李四");
hashMap.put("3", "王五");
hashMap.put("4", "赵六");
hashMap.put("5", "980");
Collection<String> collection = hashMap.values();
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String value = iterator.next();
Log.i("TAG", "value----:" + value);
}
}
}
I/TAG: value----:张三
I/TAG: value----:李四
I/TAG: value----:王五
I/TAG: value----:赵六
I/TAG: value----:980
12.entrySet():遍历集合 直接获取key value
public class CollectionActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_collection);
initArrayList();
}
public void initArrayList() {
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("1", "张三");
hashMap.put("2", "李四");
hashMap.put("3", "王五");
hashMap.put("4", "赵六");
hashMap.put("5", "980");
Set<Map.Entry<String, String>> entrySet = hashMap.entrySet();
Iterator<Map.Entry<String, String>> iterator = entrySet.iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> map = iterator.next();
String key = map.getKey();
String value = map.getValue();
Log.i("TAG", "key----" + key + "----value----:" + value);
}
}
}
I/TAG: key----1----value----:张三
I/TAG: key----2----value----:李四
I/TAG: key----3----value----:王五
I/TAG: key----4----value----:赵六
I/TAG: key----5----value----:980
三.源码分析
我们从HashMap的构造方法开始我们的源码之旅。
源码分析之构造方法
讲解前,先列举一下HashMap类中常用的变量和类,因为下面的源码分析都会用到。
【1】int threshold;
含义:The next size value at which to resize 。
含义:容器所能容纳的Key-Value对。也称为阈值。
计算:capacity * load factor
************************************************
【2】transient int modCount;
含义:The number of times this HashMap has been structurally modified。
含义:HashMap结构修改的次数。主要用于在迭代器遍历集合时 判断fast-fail 来确保多线程同步问题。具体下面会讲解
************************************************
【3】final float loadFactor;
含义:The load factor for the hash table。
含义:负载因子。表示可最大容纳数据数量的比例。
*************************************************
【4】transient int size;
含义:The number of key-value mappings contained in this map。
含义:HashMap中实际存入的键值对数量。
*************************************************
【5】transient Node<K,V>[] table;
含义:存放数据的数组
1.无参构造方法
使用
HashMap<String, String> map = new HashMap<>();
map.put("2", "张三");
源码
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//默认加载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
也就是说,使用默认无参构造方法时,只是指定默认的加载因子为0.75f。
2.指定大小的构造方法
使用
HashMap<String, String> map = new HashMap<>(5);
map.put("2", "张三");
源码
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
内部调用两个参数的构造方法
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
static final int MAXIMUM_CAPACITY = 1 << 30;
也就是说,使用指定大小的构造方法时,内部调用两个参数的构造方法。
构造方法中 需要校验传参的大小。
<1> 传参的大小 小于0 抛出异常。
<2> 传参的大小 大于MAXIMUM_CAPACITY即(1 << 30==1073741824) 则设置传参大小为1073741824 也就是说HashMap的默认可以容纳的Key-Value对是1073741824。
<3> 其他值,就使用传参的大小。
然后,负载因子是默认的0.75f。
然后使用tableSizeFor()方法计算threshold的值
tableSizeFor()方法源码
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
此方法的含义就是,将传参的大小变成最接近2的n次幂的数。比如传入7计算后为8 传入15计算后16 传入16计算后16。也就是说threshold(容器中所能容纳的Key-Value对)的值一定是2的n次幂。
源码分析之put()方法
使用
HashMap<String, String> map = new HashMap<>(5);
map.put("2", "张三");
源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
内部调用putVal方法。传五个参数。
第一个参数需要根据Key计算哈希值。调用hash()方法。这个hash()方法就是常常说到的哈希函数。
源码分析之哈希函数
hash()方法源码
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
说明:key.hashCode():就是拿到Key对应的hashCode。
如果Key为空 则 返回0。
如果Key不为空 则返回Key的hashCode和hashCode>>>16异或值。
大概的原理:
就是低16位是和高16位进行异或,高16位保持不变。一般的数组长度都会比较短,取模运算中只有低位参与散列;高位与低位进行异或,让高位也得以参与散列运算,使得散列更加均匀。
哈希函数的目标:
计算key在数组中的下标。
图解:
当有冲突时
第一个键值对A进来。通过计算其key的hash得到的index=0。记做:Entry[0] = A。
第二个键值对B,通过计算其index也等于0, HashMap会将B.next =A,Entry[0] =B。
第三个键值对C,通过计算其index也等于0,那么C.next = B,Entry[0] = C。
这样我们发现index=0的地方事实上存取了A,B,C三个键值对(Entry),它们通过next这个属性链接在一起。我们可以将这个地方称为桶。 对于不同的元素,可能计算出了相同的函数值,这样就产生了“冲突”,这就需要解决冲突,“直接定址”与“解决冲突”是哈希表的两大特点。
下面我们继续putVal()方法
putVal()方法源码
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab是HashMap内部数组,n是数组的长度,i是要插入的下标,p数组中该下标对应的节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果数组是null或者是空。则调用resize()方法进行扩容 首次put存值时这个条件会成立的
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//扩容后的数组
//使用位与运算代替取模得到下标对应的节点p 然后判断p是否为空 为空的话 也就是当前数组下标下没有节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//创建Node对象
//数组下标对应的节点不为空 也就是有值
else {
Node<K,V> e; K k;
//判断当前节点与要插入的key是否相同
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) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//长度大于等于8时转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//modCount数值+1表示结构改变
++modCount;
//判断长度是否达到最大限度,如果是则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
参数说明:
参数 int hash:Key值通过hash()哈希函数计算得到的数组下标中的位置。
参数 K key:Key。
参数 V value:Value。
参数 boolean onlyIfAbsent:是否覆盖旧值,true表示不覆盖,false表示覆盖,默认为false。
参数 boolean evict:创建模式。一般不涉及。
putVal()方法几个要点
<1> 如果数组还没有初始化(数组长度是0),则先初始化。
<2> 通过hash方法计算key的hash值,进而计算得到应该放置到数组的位置。
<3> 如果该位置为空,则直接放置此处(包装成Node)。
<4> 如果该位置不为空,而且元素是红黑树,则插入到其中。
<5> 如果是链表,则遍历链表,如果找到相等的元素则替换,否则插入到链表尾部。
<6> 如果链表的长度大于或等于8,则将链表转成红黑树。
源码分析之扩容
resize()方法源码
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
//oldTab 原数组
Node<K,V>[] oldTab = table;
//原数组大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//原数组最大容量
int oldThr = threshold;
//新数组大小 新数组容量
int newCap, newThr = 0;
//如果原数组长度大于0
if (oldCap > 0) {
//如果原数组长度超过了设置的最大长度(1<<30,也就是最大整型正数)
if (oldCap >= MAXIMUM_CAPACITY) {
// 直接把Integer阈值设置为原数组容量 然后返回原数组
threshold = Integer.MAX_VALUE;
return oldTab;
}
//否则 新数组的大小和容量变成原数组最大容量的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//如果原数组的最大容量大于0 也就是说 新建HashMap的时候指定了数组长度
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//这种情况就是创建HashMap时候使用的是无参构造方法 新数组最大长度是默认16 最大容量是默认16*0.75=12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 循环遍历原数组,并给每个节点计算新的位置
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果是红黑树,调用红黑树的拆解方法
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
<2> 扩容时,如果创建HashMap时候,指定了大小。则扩容时新的数组使用传参的大小。
如果没有传参,则扩容时新的数组默认最大长度 16 默认最大容量12
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
<3> 扩容源码可知
当链表的长度>=8且数组长度>=64时,会把链表转化成红黑树
当链表长度>=8,但数组长度<64时,会优先进行扩容,而不是转化成红黑树。
当红黑树节点数<=6,自动转化成链表。
红黑树在jdk1.8之后出现的,jdk1.7采用的是数组+链表模式。
小结1
<1> HashMap采用链地址法,当发生冲突时会转化为链表,当链表过长会转化为红黑树提高效率。
<2> HashMap默认最大长度是16,默认加载因子是0.75,默认最大容量是16*0.75=12。
源码分析之get()方法
使用
HashMap<String, String> map = new HashMap<>(5);
map.get("2");
源码
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode方法源码
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
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;
}
四.线程安全
由源码可知,HashMap的get()方法和put()方法等等的方法。并没有同步。所以肯定不是线程安全的。
在多线程的情况下无法保证数据的一致性。举个例子:HashMap下标1的位置为null,线程A需要将节点X插入下标1的位置,在判断是否为null之后,线程被挂起;此时线程B把新的节点Y插入到下标1的位置;恢复线程A,节点X会直接插入到下标1,覆盖节点Y,导致数据丢失。
解决方法
<1> 采用Hashtable。
<2> 调用Collections.synchronizeMap()方法来让HashMap具有多线程能力。
<3> 采用ConcurrentHashMap。
前两个方案的思路是相似的,均是在每个方法中,对整个对象进行上锁。这样其实是会造成资源浪费的。以为比如线程A访问的是数组下标1的节点。线程B访问的是数组下标2的节点。两个线程是可以同时访问的。这种情况是没必要同步的。这也就是HashTable和Collections.synchronizeMap()方法两个方法的弊端。
而ConcurrentHashMap是在关键的地方加了同步。
https://blog.csdn.net/weixin_37730482/article/details/69281691
关于HashMap线程安全,还有一个fast-fail问题,即快速失败。当使用HashMap的迭代器遍历HashMap时,如果此时HashMap发生了结构性改变,如插入新数据、移除数据、扩容等,那么Iteractor会抛出fast-fail异常,防止出现并发异常,在一定限度上保证了线程安全。
源码
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
也就是说。HashIterator抽象类中nextNode()方法中modCount != expectedModCount是否相等。如果不相等抛出异常。而expectedModCount变量是在HashIterator抽象类的构造方法中初始化的 expectedModCount = modCount;
而每次HashMap有修改,比如put一个元素modCount会加一的。
下面三个类就是我们Iterator方式遍历HashMap时,用到的三个类。
比如
Set<String> set = hashMap.keySet();
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
String value = hashMap.get(key);
Log.i("TAG", "key----:" + key + "----value----:" + value);
}
fast-fail异常只能当做遍历时的一种安全保证,而不能当做多线程并发访问HashMap的手段。