public class T_HashMap {//这是main方法,程序的人口public static void main(String[] args) {//创建一个HashMap的对象 :存储的是双列数据,键值对key-value .HashMap<Integer, String> hm = new HashMap<>();//存储数据:|System.out.println(hm.put(12, "one"));System.out.println(hm.put(7, "two"));System.out.println(hm.put(19, "three"));System.out.println(hm.put(12, "four"));System.out.println(hm.put(6, "five"));System.out.println("集合中的元素: " + hm/* . toString()*/);System.out.println("集合中元素的数量: " + hm.size());}
}


头插法

重要属性

//1. HashMap的K, v的值,在创建对象的时候确定: K: Integer V:String
//HashMap的父类AbstractMap已经实现类Map接口,但是源码中又单独实现了Map接口
//这个操作就是-一个多余的操作-->集合的创作者 承认了
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {//默认初始化化容量,即16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量,即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;  //默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;  //HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态
static final Entry<?,?>[] EMPTY_TABLE = {};  //空的存储实体
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  //HashMap的元素数目
transient int size;  //HashMap下次扩容是的阀值,threshold = 初始容量 * 加载因子
int threshold;  //HashMap的加载因子
final float loadFactor;  //修改次数
transient int modCount;  //最大的扩容阈值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;//hash总值,默认为0,目的使hash更复杂,结果更散列
transient int hashSeed = 0;

构造方法(一般不建议使用带参构造方法)

//容量取16,加载因子取0.75,构造HashMap
public HashMap() {  this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
} //通过加载因子构造HashMap,容量取默认值,即16
public HashMap(int initialCapacity) {  this(initialCapacity, DEFAULT_LOAD_FACTOR);
} //通过初始容量和加载因子构造HashMap
public HashMap(int initialCapacity, float loadFactor) {  if (initialCapacity < 0)//初始容量不能小于0throw new IllegalArgumentException("Illegal initial capacity: " +  initialCapacity);  if (initialCapacity > MAXIMUM_CAPACITY)//最大容量不能大于2的30次方 initialCapacity = MAXIMUM_CAPACITY;  if (loadFactor <= 0 || Float.isNaN(loadFactor))//加载因子必须为数字,并且不能小于0throw new IllegalArgumentException("Illegal load factor: " +  loadFactor);  this.loadFactor = loadFactor;  threshold = initialCapacity;  init();//这个实现为空,LinkedHashMap会使用
} //通过其他Map来初始化HashMap,容量通过其他Map的size来计算,装载因子取0.75
public HashMap(Map<? extends K, ? extends V> m) {  this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);  inflateTable(threshold);//初始化HashMap底层的数组结构  putAllForCreate(m);//添加m中的元素
}


inflateTable(初始化HashMap)
选取初始加载容量为:大于等于传入容量的2的幂次方。比如传入容量为10,那么实际初始化容量为16。

//初始化HashMap的底层数据结构
private void inflateTable(int toSize) {int capacity = roundUpToPowerOf2(toSize);//选取合适的容量值threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//选取合适的threshold(扩容阀值)table = new Entry[capacity];//初始化底层数据结构initHashSeedAsNeeded(capacity);//选择合适的Hash总值,这里和虚拟机的配置有关
}  //选择合适的容量值,容量值取大于等于最接近number的2的冪数
private static int roundUpToPowerOf2(int number) {// 如果传入容量超出了最大值,则采用最大值。如果小于1,则容量使用1// 其余情况:容量 <= 最接近容量的2的幂数,比如 容量 10=>16,  16=>16/** 假设传入值为10:*   首先,计算(number - 1) << 1,也就是(10-1) << 1 = 18,number - 1是为了防止特殊情况,比如number = 16*   然后调用Integer.highestOneBit(18),最终返回为16.*        二进制                十进制*       0001 0010               18*       0001 1111               31          首先,将最高位之后所有位转换为1*       0000 1111               15          右移一位之后,相减也就是 (11111)2 - (1111)2 => 31 - 15 = 16*       ----------------------------*       0000 1111               16* */return number >= MAXIMUM_CAPACITY  ?MAXIMUM_CAPACITY  : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

Hash总值
使用hash总值的目的是:使原本的hash算法更加散列,需要加入虚拟机的配置。在jdk1.8的hashMap中,并没有该属性。

//选择合适的Hash总值,这里和虚拟机的配置有关
final boolean initHashSeedAsNeeded(int capacity) {  boolean currentAltHashing = hashSeed != 0;  // hashSend默认值为0,所以该值默认为falseboolean useAltHashing = sun.misc.VM.isBooted() &&  (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  // 数组的实际容量 >= ALTERNATIVE_HASHING_THRESHOLD(默认为Integer.MAX,需要使用jvm参数修改)boolean switching = currentAltHashing ^ useAltHashing;  // currentAltHashing默认为false,所以只有useAltHashing为true才能触发下面的修改hash总值if (switching) {  hashSeed = useAltHashing  ? sun.misc.Hashing.randomHashSeed(this)  : 0;  }  return switching;
}

Hash的计算方式

//计算key的Hash值,这里针对String类的Key优化了Hash函数
final int hash(Object k) {  int h = hashSeed;  //hash总值,初始值为0if (0 != h && k instanceof String) {//这里针对String优化了Hash函数,是否使用新的Hash函数和Hash因子有关  return sun.misc.Hashing.stringHash32((String) k);  }  /** 这里将获取到的hashCode值>>>和^,是为了更好的平均分配定位hash桶位置索引,让高位的二进制数参与到定位hash桶位置的运算中。* */h ^= k.hashCode();  h ^= (h >>> 20) ^ (h >>> 12);  return h ^ (h >>> 7) ^ (h >>> 4);
}

定位Hash桶的计算方式(定位Hash桶计算公式:h & (length-1)。)

//根据Hash值和Hash表的大小选择合适的Hash桶
static int indexFor(int h, int length) {/** 返回需要保证两个条件:*       1.返回值小于length*       2.分配平均* 假设hash为17,传入容量为10,经过计算之后的容量为16,那么返回值就为 17 & (16 - 1)*       二进制             十进制*       0001 0001           17*       0000 1111           15*   &* -----------------------------*       0000 0001           1* */return h & (length-1);
}

put方法
如果底层数组为空,则会先初始化底层数组,默认容量为16。定位Hash桶位置,并且遍历该链表下的所有节点,如果有节点和插入节点的key相同,就覆盖该节点,并且返回旧的value,如果没有key相同,那么就会采用头插法,将该节点插入。
如果key为空,则执行空的逻辑,说明HashMap可以存放key为null的元素,该元素默认存放在数组下标为0的链表中。

//添加元素
public V put(K key, V value) {  if (table == EMPTY_TABLE) {//具体看:容量计算方法inflateTable(threshold);}  if (key == null)//如果key为空,则执行空的逻辑,说明HashMap可以存放key为null的元素return putForNullKey(value);  int hash = hash(key);//获取key的Hash值  int i = indexFor(hash, table.length);//定位Hash桶  //如果放入的数组的位置上没有元素的话,那么直接添加就行了,不用走这个for循环for (Entry<K,V> e = table[i]; e != null; e = e.next) {//定位到hash桶的链表上,是否有key相同的元素,如果有返回并修改旧值Object k;//发生哈西碰撞的时候,会先比较哈希值//比较key是否是一一个对象,如果key是一-个对象的话,equals就不比较了//如果不是同一个对象,会比较equals方法//如果hash值- -样,equals方法比较的结果也一-样,那么才会走这个if方法:if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//hash相同,并且key相同V oldValue = e.value;  //获取老的valuee.value = value;//新值覆盖旧值-->只替换value不替换keye.recordAccess(this);//调用value的回调函数,其实这个函数也为空实现  return oldValue;//返回旧值}  }  modCount++;//更新修改次数  addEntry(hash, key, value, i);//添加到对应Hash桶的链接表中,头插法return null; //如果该key以前没有被添加过,则返回null
}

Hash桶的扩容

//添加Entry
void addEntry(int hash, K key, V value, int bucketIndex) {if ((size >= threshold) && (null != table[bucketIndex])) {//判断是否需要扩容,存放元素(包含链表)大于等于阈值,并且定位hash桶首个元素不为nullresize(2 * table.length);//按2倍扩容,执行扩容hash = (null != key) ? hash(key) : 0;//计算新插入元素的hash桶位置。  bucketIndex = indexFor(hash, table.length);}  createEntry(hash, key, value, bucketIndex);//创建元素
}
//执行扩容
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, initHashSeedAsNeeded(newCapacity));//将老的数据拷贝到新的结构中。initHashSeedAsNeeded为是否使用hash总值,与虚拟机配置有关,默认为falsetable = newTable;//修改HashMap的底层数组  threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改阀值
}
//将老的数据拷贝到新的结构中
void transfer(Entry[] newTable, boolean rehash) {  int newCapacity = newTable.length;//新的容量  for (Entry<K,V> e : table) {  while(null != e) {  Entry<K,V> next = e.next;  if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);  }  int i = indexFor(e.hash, newCapacity);//定位Hash桶,计算的结果可能为:初始位置 或者 初始位置 + 扩容量e.next = newTable[i];//扩容插入也会采用头插法,与旧的hash桶相比链表的顺序可能会被颠倒,并且分散到 初始位置 和 初始位置 + 扩容量两个位置中newTable[i] = e;  e = next;  }  }
}

9.2 扩容的条件
扩容的条件为:((size >= threshold) && (null != table[bucketIndex])) ,即存放元素(包含链表)大于扩容阈值,并且定位hash桶位置首个元素不为null。
9.3 扩容的运行机制
hash桶的扩容默认为原来hash桶的2倍进行扩容,扩容之后旧的元素会被拷贝到新的hash桶中,复制到的位置会为两种,分别为:初始位置(旧的位置) 或者 初始位置 + 扩容量(也就是原来的hash桶的长度),复制也会采用头插法。所以,在复制之后,新的hash桶与旧的hash桶链表的顺序会相反。在多线程的情况下,可能会出现环形链表的情况,导致数据不安全。
9.4 扩容的优势
扩容不仅是提升数组长度,通过扩容可以将原来的链表变短,因为链表的hash会重新计算(hash & length -1),计算结果会出现两种情况,所以原本链表可能会被分配到两个位置,从而提升查找效率。
9.5 如何避免HashMap扩容
假设需要固定的30个容量(需要的容量为30)。
那么只需要让扩容阈值永远达不到30即可(扩容阈值 = 实际容量 * 加载因子)。
那么构造传入容量32,通过计算的实际容量也就为32,加载因子为1,那么hashmap只有达到32(扩容阈值)才会扩容,需要30个容量的话,就不会扩容。



面试题

hashmap源码1.7相关推荐

  1. HashMap源码实现分析

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 一.前言 HashMap 顾名思义,就是用hash表的原理 ...

  2. Java源码详解二:HashMap源码分析--openjdk java 11源码

    文章目录 HashMap.java介绍 1.HashMap的get和put操作平均时间复杂度和最坏时间复杂度 2.为什么链表长度超过8才转换为红黑树 3.红黑树中的节点如何排序 本系列是Java详解, ...

  3. JDK 1.6 HashMap 源码分析

    前言 ​ 前段时间研究了一下JDK 1.6 的 HashMap 源码,把部份重要的方法分析一下,当然HashMap中还有一些值得研究得就交给读者了,如有不正确之处还望留言指正. 准备 ​ 需要熟悉数组 ...

  4. HashMap 源码详细分析(JDK1.8)

    1. 概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值 ...

  5. Java类集框架 —— HashMap源码分析

    HashMap是基于Map的键值对映射表,底层是通过数组.链表.红黑树(JDK1.8加入)来实现的. HashMap结构 HashMap中存储元素,是将key和value封装成了一个Node,先以一个 ...

  6. Java集合:HashMap源码剖析

    一.HashMap概述 二.HashMap的数据结构 三.HashMap源码分析      1.关键属性      2.构造方法      3.存储数据      4.调整大小 5.数据读取     ...

  7. 查询已有链表的hashmap_源码分析系列1:HashMap源码分析(基于JDK1.8)

    1.HashMap的底层实现图示 如上图所示: HashMap底层是由  数组+(链表)=(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...

  8. HashMap源码浅析

    HashMap源码主要一些属性 //默认的初始化容量(2的n次方) static final int default_inital_capacity = 16; //最大指定容量为2的30次方 sta ...

  9. 【Java深入研究】9、HashMap源码解析(jdk 1.8)

    一.HashMap概述 HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现.与HashTable主要区别为不支持同步和允许null作为key和value.由于HashMap不是线程 ...

  10. HashMap源码解析(JDK1.8)

    HashMap源码解析(JDK1.8) 目录 定义 构造函数 数据结构 存储实现源码分析 删除操作源码分析 hashMap遍历和异常解析 1. 定义 HashMap实现了Map接口,继承Abstrac ...

最新文章

  1. TensorFlow编程结构
  2. ElasticSearch 面试 4 连问,你顶得住么?
  3. 商汤涨涨涨涨:上市4日股价累涨130%,市值一度3000亿港元
  4. matlab 神经网络预测时间序列示例(水痘模型)
  5. IDEA运行第一个Spring Boot应用程序
  6. Linq中的Where与SkipWhile
  7. SimpleDateFormat类的安全问题,这6个方案总有一个适合你
  8. Java自动装箱和拆箱
  9. Struts2返回Json数据(使用Struts2插件)
  10. 腾讯用微信、QQ 把微视送上了 App Store 第一 | 畅言
  11. asp.net 将ppt,word转化为pdf实现在线浏览详解
  12. 关于ibatis中sqlMap配置文件中使用到,的处理
  13. 十六、Oracle学习笔记:索引和约束(表字段快速查询和约束)
  14. Codeforces Round #375 (Div. 2) D. Lakes in Berland 贪心
  15. 在Pyramid中使用Mako模板以及默认和.html后缀关联
  16. 分布式期末复习总结(林子雨老师)
  17. 联想计算机系统重装,联想笔记本电脑一键重装系统的方法
  18. 什么是机器学习(漫画版)
  19. yun之梦 酒仙wang 实战 带源码
  20. sqlzoo 答案全集

热门文章

  1. 增益dB与放大倍数K的转换关系,以及-3dB带宽定义
  2. PowerDVD v18.0.1815.62 极致蓝光版破解版
  3. 路上铺个“补丁”,智能汽车高速途中瞬间失控!应用最广自动驾驶技术被曝漏洞 | 字节跳动参与的新研究...
  4. 数字时代的新一代数据安全
  5. 找工作时牢牢记住这些
  6. 台达触摸屏-实现按钮单选功能(宏程序)
  7. 点积应用-求两个向量夹角
  8. ST-GCN复现的全过程(详细)
  9. 锁定的计算机怎么休眠,计算机待机、休眠、锁定的用法和区别
  10. 6-2 分数计算 (10分)