HashMap是用来存放键值对对的,其底层实现是数组+链表(JDK1.6),而且在多线程的情况下它是不安全的,下面是基于JDK1.6的常用功能的源码分析。

一、构造方法和一些重要的属性

1、几个属性

// 默认的初始容量
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;

2、指定初始容量和加载因子的构造方法

public HashMap(int initialCapacity, float loadFactor) {// 初始容量不能小于0if (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,实际初始容量也为16while (capacity < initialCapacity)capacity <<= 1;this.loadFactor = loadFactor;// 计算扩容的临界值threshold = (int)(capacity * loadFactor);// 初始化数组table = new Entry[capacity];// init是个空方法init();
}

3、指定初始容量的构造方法

调用上面的那个构造方法,并指定加载因子为默认加载因子0.75

public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

4、无参构造方法

加载因子默认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();
}

5、依据给定的集合构造HashMap

加载因子采用默认的加载因子,而初始容量就取(默认初始容量(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对应的valueif (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的长度值
}

二、增

1、单个键值对的添加

public V put(K key, V value) {// 如果key为null,则有单独的处理if (key == null)return putForNullKey(value);// 计算hashint 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;
}

2、把一个集合添加到HashMap中

整体的做法是遍历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());}
}

三、删

1、指定键的值来删除

通过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) {// 计算hashint 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;elseprev.next = next;e.recordRemoval(this);return e;}prev = e;e = next;}return e;
}

2、清空集合

通过循环置空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;
}

2、是否为空集合

长度为0,就是空集合

public boolean isEmpty() {return size == 0;
}

3、是否包含指定的key

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

4、是否包含指定的value

主要就是遍历匹配,因为不能根据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");// 遍历keyfor(String key : map.keySet()){System.out.println(key);}// 遍历valuefor(String val : map.values()){System.out.println(val);}// 遍历key和valuefor(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());}
}

关于以上代码,下面结合源码稍作分析

1、遍历key

先来看一下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 returnint expectedModCount;    // For fast-failint index;      // current slotEntry<K,V> current;    // current entryHashIterator() {expectedModCount = modCount;if (size > 0) { // advance to first entryEntry[] 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 {@Overridepublic String toString() {System.out.println("toString");return super.toString();}
}

这段代码在调试的时候会输出toString,但在运行的时候不会输出,这个基本上就验证了问题所在。后来我在eclipse中调试了一下keySet方法,发现进去的时候keySet确实是null.

2、values和entrySet

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)的死循环。

阅读HashMap(1.6)源码所做的一些记录相关推荐

  1. HashMap、ConcurrentHashMap源码解读(JDK7/8)

    下载地址(已将图片传到云端,md文件方便浏览更改):https://download.csdn.net/download/hancoder/12318377 推荐视频地址: https://www.b ...

  2. Deep Compression阅读理解及Caffe源码修改

    Deep Compression阅读理解及Caffe源码修改 作者:may0324 更新:  没想到这篇文章写出后有这么多人关注和索要源码,有点受宠若惊.说来惭愧,这个工作当时做的很粗糙,源码修改的比 ...

  3. [vue-element]有阅读过ElementUI的源码吗?

    [vue-element]有阅读过ElementUI的源码吗? 读过, 公司封装自己的UI库的时候, 我都过很多UI库的源码, 比如element, iview, vuetifyjs, 等 个人简介 ...

  4. 深入理解HashMap(三): 关键源码逐行分析之构造函数

    前言 系列文章目录 上一篇我们说明了HashMap的hash算法, 说到HashMap在构造时会自动将table设为2的整数次幂. 本篇我们就来聊聊HashMap的构造函数. 本文的源码基于 jdk8 ...

  5. 如何快速阅读并分析Android源码

    很多时候为了解决一个问题必须先理解Android系统的运行原理,这时候就得阅读应用层框架的源码.学会阅读系统源码或者他人的代码,这是研发必备的技能之一,只有了解别人写的东西才能在他之上更好的行事,同时 ...

  6. 文档说明类型PHP网站源码 适合做使用手册网站PHP源码

    下载地址在最下面 一.系统简介 1. 产品简介 系统集电脑站.手机站.微信.APP.小程序于一体,共用空间,数据同步,是国内五站合一优秀企业建站解决方案.系统采用PHP开发,具有操作简单.功能强大.稳 ...

  7. 付费阅读微信小程序源码V1.8.2,小程序和公众号双版本

    介绍: 付费阅读微信小程序源码V1.8.2版本,源码包括付费阅读小程序和公众号双版本,前后端开源源码. 支持免费看一部分文字.视频.音频,然后其他部分需要付费才能观看的小程序,此版本为独立版小程序,和 ...

  8. 宝塔面板ab模板建站_宝塔一键部署源码怎么做才能好用。[第7篇]

    QQ20170728-155809@2x.png (10 KB, 下载次数: 265) 2017-7-28 15:58 上传 宝塔早就有做一键部署源码的计划了,只是源于研发精力有限,对这些目标任务的优 ...

  9. HashMap jdk1.7源码阅读与解析

    转载自  HashMap源码阅读与解析 一.导入语 HashMap是我们最常见也是最长使用的数据结构之一,它的功能强大.用处广泛.而且也是面试常见的考查知识点.常见问题可能有HashMap存储结构是什 ...

  10. hashmap与concurrenthashmap源码解析

    hashmap源码解析转载:http://www.cnblogs.com/ITtangtang/p/3948406.html 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此 ...

最新文章

  1. php array的实现原理,PHP数组遍历与实现原理
  2. FisherVector编码的来龙去脉
  3. SqlHelper简单实现(通过Expression和反射)4.对象反射Helper类
  4. mysql备份文件0kb_Oracle 数据文件大小为0kb或者文件丢失恢复
  5. 信安精品课:第1章网络信息安全概述精讲笔记
  6. A站有一个页面需要PV统计 A站读写该数据 B站读该数据 需要数据同步
  7. VC++多线程工作笔记0006---线程间同步机制1
  8. IREC-GAN:在线推荐中基于模型的对抗训练强化学习
  9. 放之四海皆适用的设计原则(一)
  10. 华为认证考试在哪里考比较靠谱?
  11. 百度文库,道客巴巴等文库免积分下载
  12. 解决应用程序错误,内存不能为“read”或“written”
  13. Eclipse 菜单栏翻译
  14. 最土团购系统常见问题的汇总
  15. 很有意境的语句[转]
  16. 微信小程序 | 一文总结全部营销抽奖功能
  17. 为什么 200M 宽带,打王者荣耀还是会有 460 的延迟?
  18. 2022广东省安全员A证第三批(主要负责人)考试题库模拟考试平台操作
  19. setsockopt与getsockopt的参数解析与使用
  20. 05_Java筑基之Java开发初体验

热门文章

  1. SpringBoot+MybatisPlus实现关联表查询
  2. 搜索引擎的原理以及倒排索引技术
  3. sketchup草图家具拆单软件 有屋 衣柜橱柜 全屋定制 设计渲染生产一体化SU
  4. java基础代码-实现键盘输入
  5. java基础知识点总结(一)
  6. 读写卡测试程序VFP源代码
  7. 大数运算经典:棋盘上的米粒。
  8. 线性动力学问题(二)
  9. DDcGAN:用于多分辨率图像融合的双判别器生成对抗网络
  10. RapidMiner教程