一 知识准备

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. Eclipse常用设置和快捷键

    1.提示键配置一般默认情况下,Eclipse ,MyEclipse 的代码提示功能是比Microsoft Visual Studio的差很多的,主要是Eclipse ,MyEclipse本身有很多选项 ...

  2. 解决 MVC4 Code First 数据迁移 数据库发生更改导致调试失败解决方法(二)

    文章转载自:http://www.cnblogs.com/amoniyibeizi/p/4486617.html 前几天学MVC过程中,遇到更改Model类以后,运行程序就会出现数据已更改的问题导致调 ...

  3. 2013应届毕业生各大IT公司待遇整理汇总篇(转)

    不管是应届毕业生还是职场中人,在找工作时都必然会对待遇十分关注,而通常都是面试到最后几轮才知道公司给出的待遇.如果我们事先就了解大概行情,那么就会在面试之前进行比较,筛选出几个心仪的公司,这样才能集中 ...

  4. PHP : 数据库中int类型保存时间并通过年月份时分秒进行显示

    1.表设计: 2.数据库操作页面:将时间戳插入到数据库中 我们到数据库中可以看到: 3.我们将数据进行显示: 页面结果:(二维数组) 4.以为用mysqli_fetch_all得到的是二维数组,那么我 ...

  5. 修改FileZilla(FTP客户端)同时传输的文件数

    在菜单中点击“编辑”.“设置”,按照以下步骤操作:

  6. 解决Wamp各版本中 Apache 文件列表图标无法显示

    Edit the following file manually and change the path to the icons folder (it appears times in the fi ...

  7. tomcat7 的The Apache Tomcat Native library which allows optimal performance 的解决

    1.        用Myeclipse启动tomcat7启动时可能会收到下面的信息: 七月 24, 2014 10:13:30 上午 org.apache.catalina.core.AprLife ...

  8. mysql索引长度

    http://blog.csdn.net/qsc0624/article/details/51335632 大家应该知道InnoDB单列索引长度不能超过767bytes,联合索引还有一个限制是长度不能 ...

  9. 转:SSM框架——详细整合教程(Spring+SpringMVC+MyBatis)

    转:https://www.cnblogs.com/zyw-205520/p/4771253.html 1.基本概念   1.1.Spring Spring是一个开源框架,Spring是于2003 年 ...

  10. 在CentOS 6.5上安装NodeJS

    CentOS的软件源未包含有最新的nodejs, 需要手动编译安装. 首先安装依赖的库与工具 yum install libtool automake autoconf gcc-c++ openssl ...