HashMap是用来存放键值对对的,其底层实现是数组+链表(JDK1.6),而且在多线程的情况下它是不安全的,下面是基于JDK1.6的常用功能的源码分析。
// 默认的初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 存储数据的数组
transient Entry[] table;
// HashMap的长度
transient int size;
// 扩容的临界值,就是说当HashMap的长度达到这个值得时候,就要扩容了,其大小是容量*加载因子
int threshold;
// 实际的加载因子
final float loadFactor;
public HashMap(int initialCapacity, float loadFactor) {
// 初始容量不能小于0
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);
int capacity = 1;
// 通过位移运算让容量为2的指数倍(大于等于指定容量)。比如如果你指定初始容量为9,实际的初始容量为16,如果指定初始容量为16,实际初始容量也为16
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
// 计算扩容的临界值
threshold = (int)(capacity * loadFactor);
// 初始化数组
table = new Entry[capacity];
// init是个空方法
init();
}
调用上面的那个构造方法,并指定加载因子为默认加载因子0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
加载因子默认0.75,默认初始容量16
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
加载因子采用默认的加载因子,而初始容量就取(默认初始容量(16))和(传入的集合长度除以默认加载因子后再加1)中比较大的那个值。然后通过迭代器循环取出m(传入的集合)中的键值然后放到HashMap中
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}
private void putAllForCreate(Map<? extends K, ? extends V> m) {
// 通过迭代器循环
for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<? extends K, ? extends V> e = i.next();
putForCreate(e.getKey(), e.getValue());
}
}
private void putForCreate(K key, V value) {
// 如果key为null,则直接让其hash为0,之后会把其散列到table[0]上
int hash = (key == null) ? 0 : hash(key.hashCode());
// 通过key的hash和table的长度进行按位与运算,得到这个键值应该放到数组table哪个位置的下标
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 检测key是否已经存在,如果已经存在,则替换此key对应的value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
createEntry(hash, key, value, i);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
// 此处是头插(在头部插入,JDK1.8的时候变成了尾插)
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
size++;// 更新HashMap的长度值
}
public V put(K key, V value) {
// 如果key为null,则有单独的处理
if (key == null)
return putForNullKey(value);
// 计算hash
int hash = hash(key.hashCode());
// 计算散列到table的那个位置,得到的i即为要散列到数组table的下标
int i = indexFor(hash, table.length);
// 查找此key是否已经存在,如果存在就修改value,并把旧的value返回
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);// HashMap中的recordAccess方法是空的
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
// 此处是头插
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);// 扩容
}
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];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);// 计算新的扩容点
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
// 循环取出原数组中的数据,并重新计算每条数据散列位置,注意,每个hash桶中的数据,如果扩容以后两条数据还落在同一个桶中,则其在链表中的前后顺序会倒置(因为是头插,但取出的时候也是从头部开始取得),因此在多线程下可能造成死循环
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
// key为null的插入
private V putForNullKey(V value) {
// key为null的数据固定散列到table[0]桶中,同样,如果有key为null的数据,则把其value替换,并把旧的value返回,如果不存在key为null的数据,则正常插入
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
整体的做法是遍历m,获取到每一条数据,然后调用HashMap的put方法,把数据加入到HashMap中
public void putAll(Map<? extends K, ? extends V> m) {
int numKeysToBeAdded = m.size();
// 如果m为空集合,则直接返回
if (numKeysToBeAdded == 0)
return;
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
if (newCapacity > table.length)
resize(newCapacity);
}
// 遍历添加
for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<? extends K, ? extends V> e = i.next();
put(e.getKey(), e.getValue());
}
}
通过key的值来计算该key所散列的位置,然后循环取匹配,如果存在指定的key,则删除,并返回key所对应的原值
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
// 计算hash
int hash = (key == null) ? 0 : hash(key.hashCode());
// 找到所在桶的下标
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
// 循环匹配
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
// 如果要删除的是链表的第一个元素就直接把table[i]指向后一个元素就行了,如果删除的是中间的某个元素,直接要删除的元素的上个元素的next直接指向要删除的下个元素
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
通过循环置空table数组
public void clear() {
modCount++;
Entry[] tab = table;
for (int i = 0; i < tab.length; i++)
tab[i] = null;
size = 0;
}
通过给定的key计算出数组的下标,然后通过循环对应下标处的链表对key进行匹配,如果匹配到就返回其对应的值,否则返回null
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
1、长度
size属性就是其长度
public int size() {
return size;
}
长度为0,就是空集合
public boolean isEmpty() {
return size == 0;
}
和get方法的逻辑基本上是一样
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
主要就是遍历匹配,因为不能根据value值来确定所在的桶,这个操作比较费时。
public boolean containsValue(Object value) {
if (value == null)
return containsNullValue();
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
private boolean containsNullValue() {
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (e.value == null)
return true;
return false;
}
一些测试代码
public void test(){
Map<String, String> map = new HashMap<String, String>();
map.put("a", "aa");
map.put("b", "bb");
map.put("c", "cc");
map.put("d", "dd");
// 遍历key
for(String key : map.keySet()){
System.out.println(key);
}
// 遍历value
for(String val : map.values()){
System.out.println(val);
}
// 遍历key和value
for(Map.Entry<String, String> entry : map.entrySet()){
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}
// 迭代器遍历 (在遍历的时候删除特定的值可以用迭代器)
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<String, String> e = iterator.next();
System.out.println(e.getKey());
System.out.println(e.getValue());
}
}
关于以上代码,下面结合源码稍作分析
先来看一下keySet方法
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
// keySet属性的定义
transient volatile Set<K> keySet = null;
首先第一次调用keySet方法的时候,keySet的值为null,然后就会new一个KeySet,并赋给keySet变量。下面是KeySet类的结构
private final class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
}
我们知道,foreach的实现是基于迭代器来实现的,所以会调用KeySet的iterator方法,看到其中是直接调用的newKeyIterator放,这个方法在哪儿呢?在HashMap中有这么个方法
Iterator<K> newKeyIterator() {
return new KeyIterator();
}
在这个方法中直接new了一个KeyIterator,我们继续看KeyIterator这个类,这个类是HashMap的内部类
private final class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
这个类又继承了HashIterator类
private abstract class HashIterator<E> implements Iterator<E> {
Entry<K,V> next; // next entry to return
int expectedModCount; // For fast-fail
int index; // current slot
Entry<K,V> current; // current entry
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
public final boolean hasNext() {
return next != null;
}
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
}
HashIterator类中有hasNext方法,KeyIterator类中有next方法,这就构成了迭代器循环的基础
在这儿说一个遇到过的问题。就是在调试的时候,第一次调用keySet方法的时候,发现keySet的居然有值,我就想可能是在其他地方对keySet有初始化,但是找了一圈都没有找到初始化的地方,然后查了一下资料说是idea在调试的时候会调用toString方法,然后我就写了一段测试代码。
public void testDebugger(){
System.out.println("before");
DebuggerTest debuggerTest = new DebuggerTest();
System.out.println("after");
}
public class DebuggerTest {
@Override
public String toString() {
System.out.println("toString");
return super.toString();
}
}
这段代码在调试的时候会输出toString,但在运行的时候不会输出,这个基本上就验证了问题所在。后来我在eclipse中调试了一下keySet方法,发现进去的时候keySet确实是null.
values和entrySet基本上和keySet是一样的套路,自己看一下代码就明白了
在多线程的情况下,JDK1.6中HahsMap在扩容的时候可能会形成死循环(jJDK1.8不存在这种情况,因其使用的是尾插法,但其仍然不是线程安全的),我们分析死循环形成的原因,死循环是发生在扩容的时候,把数据从旧的数组移到新的数组的时候。看下代码
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;// ①
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
比说说有个链表是a->b->null(并且假设这两个Entry在扩容后还落在同一个桶中),然后有个线程(A)在执行完①这一行的时候,此时next指向了b,被剥夺了cpu的执行权限,然后另个一个线程(B)也执行了扩容,扩容后链表变成了b->a->null,当A线程再获得执行权限后,会形成a->b再次指向b这种情况。这样就会形成a<->b(a的next指向b,b的next指向a)的死循环。