一 知识准备

HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

二  HashMap的数据结构:

  JDK 7.0及以前

  在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

  JDK 8.0(本文主要介绍JDK 8.0的实现)

JDK7.0及以前,HashMap的结构都是基于一个数组以及多个链表的实现,处理Hash冲突的方法就是将对应节点以链表的形式存储。

简单的实现是以HashMap性能牺牲为代价的,如果说有成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那将不可避免的花费0(N)的查找时间,严重影响性能。JDK 8.0开始使用数组+链表+红黑树的组合来实现HashMap。

(http://www.cnblogs.com/leesf456/p/5242233.html)

三 字段

//HashMap的散列表
transient Node<K,V>[] table; //存放entry的set
transient Set<Map.Entry<K,V>> entrySet; //记录HashMap中存储了多少个键值对<KEY-VALUE>
transient int size; //mod是modify的缩写,hashMap的结构发生结构变化时会记录一次。
transient int modCount; //默认初始化table的大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //table的最大大小
static final int MAXIMUM_CAPACITY = 1 << 30;
//这是一个比例参数,当table中已经被占用的元素数与table总长度的比例不小于这个参//数的时候,就会发生table的扩容,每次扩容都以2倍大小进行扩容,注意resize()函数
static final float DEFAULT_LOAD_FACTOR = 0.75f; //当size大于这个数时,就进行一次扩容,即调用resize()函数
int threshold; //当节点冲突数达到8时,就会对hash表进行调整,如果table的长度小于64,那么会进//行table扩容,如果不小于64,那么会将因冲突形成的单链表调整为红黑树。
static final int TREEIFY_THRESHOLD = 8; //在删除冲突节点之后,同hash的节点数低于这个值时,将红黑树重新恢复为单链表。
static final int UNTREEIFY_THRESHOLD = 6; //注意到TREEIFY_THRESHOLD解释,不小于64时仅对table进行扩容,这个64就是//指这个值。
static final int MIN_TREEIFY_CAPACITY = 64;

四 构造函数

  /**
* @param initialCapacity 初始容量
* @param loadFactor 负载因子*/
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);
} /**
* @param initialCapacity 初始容量,默认负载因子0.75*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
} /**
* 默认初始容量16,默认负载因子 0.75
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
} /**
* @param 使用一个map来初始化新的HashMap*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}

五 Get 和 Put 方法

put方法

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

putVal方法

 /**
* Implements Map.put and related methods
* @param hash hash for key
* @param key 键
* @param value 值
* @param onlyIfAbsent 如果是true,不改变已存在的值,字面意思,只当map中该对象没有才存入,默认false
* @param evict 如果 false, 散列表处于创建模式,默认true
* @return 返回旧值或者null或者 none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
    //先判断,table大小,如果table为null,或者没分配空间,就resize一次
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果首节点为null,就创建一个首节点。注意到tab[i = (n - 1) & hash],(n-1)&hash才是真正的hash值,也就是存储在table的位置(index)。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//创建一个新的节点
else {//冲突处理
Node<K,V> e; K k;
       //p这时候是指向table[i]的那个Node,这时候先判断下table[i]这个节点是不是和我们待插入节点有相同的hash、key值。如果是就e = p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
      //这里说明第一个节点的hash、key值与我们带插入Node的hash、key值不吻合,那么要从这个节点之后的链表节点或者树节点中查找。由于之前提到过,1.8的HashMap存储碰撞节点时
      ,有可能是用红黑树存储,那么先判断首节点p的类型,如果是TreeNode类型(Node的子类),那么就说明碰撞节点已经用红黑树存储,那么使用树的插入方法,如果新插入了树节点,
      那么e会等于null,用于后面的判断与处理
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) {//e引用下一个节点,如果是null,表示没有找到同hash、key的节点
p.next = newNode(hash, key, value, null);//创建一个新的节点,放到冲突链表的最后
              // 注意到如果这时候冲突节点个数达到8个,那么就会treeifyBin(tab, hash)函数,看是否需要改变冲突节点的存储结构,
               这个treeifyBin首先回去判断当前hash表的长度,如果不足64的话,实际上就只进行resize,扩容table,如果已经达到64,那么才会将冲突项存储结构改为红黑树。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
            //如果找到了同hash、key的节点,那么直接退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//调整下p节点
}
}
if (e != null) { // existing mapping for key
         //注意到这时候要判断是不是要修改已插入节点的value值,两个条件任意满足即修改
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);//这个是空函数,可以由用户根据需要覆盖
return oldValue;
}
}
++modCount;//当插入了新节点,才会运行到这儿,由于插入了新节点,整个HashMap的结构调整次数+1
if (++size > threshold)//HashMap中节点数+1,如果大于threshold,那么要进行一次扩容
resize();
afterNodeInsertion(evict);//这个是空函数,可以由用户根据需要覆盖
return null;
}

get方法,比较简单,就是在在table上根据key.hash查找,如果hash值相同有多个,则根据key.equals()在链表或者红黑树上遍历比较,得到最终值

 public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 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;
}

六 总结

JDK 8.0的HashMap中没当冲突节点个数大于8时,就先尝试table扩容,当table数达到64后,冲突节点数为8时,则进行链表向树结构转换,这样对于冲突节点的访问复杂度就会大幅度降低,当然这是建立在插入时冲突处理算法复杂度提升为代价的。

集合源码分析之 HashMap的更多相关文章

  1. 【集合框架】JDK1.8源码分析之HashMap(一) 转载

    [集合框架]JDK1.8源码分析之HashMap(一)   一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...

  2. Java 集合源码分析(一)HashMap

    目录 Java 集合源码分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5. JDK 1.7 的 Con ...

  3. java集合源码分析(六):HashMap

    概述 HashMap 是 Map 接口下一个线程不安全的,基于哈希表的实现类.由于他解决哈希冲突的方式是分离链表法,也就是拉链法,因此他的数据结构是数组+链表,在 JDK8 以后,当哈希冲突严重时,H ...

  4. java集合源码分析(三):ArrayList

    概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...

  5. Java集合源码分析(七)HashMap<K, V>

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap  ...

  6. Java集合源码分析(四)HashMap

    一.HashMap简介 1.1.HashMap概述 HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射.此类不保证映射的顺序,假定哈希函数将元素 ...

  7. Java集合源码分析(五)——HashMap

    简介 HashMap 是一个散列表,存储的内容是键值对映射. HashMap 继承于AbstractMap,实现了Map.Cloneable.java.io.Serializable接口. HashM ...

  8. 【集合框架】JDK1.8源码分析之HashMap(一)

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

  9. 【集合框架】JDK1.8源码分析之HashMap & LinkedHashMap迭代器(三)

    一.前言 在遍历HashMap与LinkedHashMap时,我们通常都会使用到迭代器,而HashMap的迭代器与LinkedHashMap迭代器是如何工作的呢?下面我们来一起分析分析. 二.迭代器继 ...

随机推荐

  1. xfs管理2T以上大分区

    设置gpt硬盘 parted /dev/sdb (parted) mklabel gpt (parted) mkpart primary xfs 0 -1 (Ignore) (parted) quit ...

  2. EasyUI手风琴 Tab卡使用

    --案例项目代码,初始化手风琴,定义打开Tab的方法. $(result).each(function () { //m_pi_jscode,pi_jscode if (m_pi_id != this ...

  3. jQuery-prepend、append、before、after的区别

    举例说明,原始html代码如下: <ol> <li>List item 1</li> <li>List item 2</li> <li ...

  4. Linux 命令后台运行

    写这个随笔主要是每次Deepin用shadowsocks的时候总需要命令行启动,然后一个终端就一直开着总是点错了就给关了. (不知道为什么我的Deepin的shadowsocks-qt5总是连接不上的 ...

  5. mysql数据库 thinkphp连贯操作where条件的判断不正确的问题

    前两天一直写一个基于thinkphp的东西,遇到从mysql数据库里select数据,where条件一直出现问题的情况.直接上代码: $history = M('history'); $suerId ...

  6. QT5中两个窗体之间传递信息(值)

    一个窗体A调用另一个窗体B: 1)包含窗体B的头文件#include"B.h" 2)在窗体A中增加slots函数: public slots: void infoRecv(QStr ...

  7. centos 7 iptables基本配置

    安装iptable iptable-service #先检查是否安装了iptables service iptables status #安装iptables yum install -y iptab ...

  8. react里面 react-router4 跳转

    在react里面跳转的时候,一般可以用 <Link to='/tradeList' /> 但是我们在运用组件组合的时候经常会通过传参去判断,如果props传过来是参数,如果有link进行跳 ...

  9. 7 MSSQL数据库备份与恢复

    0 MSSQL数据库备份 1 SQLAgent配置 2 设置连接属性 3 输入SA账号密码 4 SQL备份脚本配置 5 生成SQL全量备份脚本 6 生成SQL差异备份脚本 7 修改SQL差异备份脚本 ...

  10. mac home brew install go

    mac利器home brew安装Go 首先你得需要安装home brew和ruby环境(因为home brew依赖ruby) 如果没有请自行到链接安装 准备好之后就开始安装go了 brew updat ...