hashmap源码1.7
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相关推荐
- HashMap源码实现分析
点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 一.前言 HashMap 顾名思义,就是用hash表的原理 ...
- Java源码详解二:HashMap源码分析--openjdk java 11源码
文章目录 HashMap.java介绍 1.HashMap的get和put操作平均时间复杂度和最坏时间复杂度 2.为什么链表长度超过8才转换为红黑树 3.红黑树中的节点如何排序 本系列是Java详解, ...
- JDK 1.6 HashMap 源码分析
前言 前段时间研究了一下JDK 1.6 的 HashMap 源码,把部份重要的方法分析一下,当然HashMap中还有一些值得研究得就交给读者了,如有不正确之处还望留言指正. 准备 需要熟悉数组 ...
- HashMap 源码详细分析(JDK1.8)
1. 概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值 ...
- Java类集框架 —— HashMap源码分析
HashMap是基于Map的键值对映射表,底层是通过数组.链表.红黑树(JDK1.8加入)来实现的. HashMap结构 HashMap中存储元素,是将key和value封装成了一个Node,先以一个 ...
- Java集合:HashMap源码剖析
一.HashMap概述 二.HashMap的数据结构 三.HashMap源码分析 1.关键属性 2.构造方法 3.存储数据 4.调整大小 5.数据读取 ...
- 查询已有链表的hashmap_源码分析系列1:HashMap源码分析(基于JDK1.8)
1.HashMap的底层实现图示 如上图所示: HashMap底层是由 数组+(链表)=(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...
- HashMap源码浅析
HashMap源码主要一些属性 //默认的初始化容量(2的n次方) static final int default_inital_capacity = 16; //最大指定容量为2的30次方 sta ...
- 【Java深入研究】9、HashMap源码解析(jdk 1.8)
一.HashMap概述 HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现.与HashTable主要区别为不支持同步和允许null作为key和value.由于HashMap不是线程 ...
- HashMap源码解析(JDK1.8)
HashMap源码解析(JDK1.8) 目录 定义 构造函数 数据结构 存储实现源码分析 删除操作源码分析 hashMap遍历和异常解析 1. 定义 HashMap实现了Map接口,继承Abstrac ...
最新文章
- TensorFlow编程结构
- ElasticSearch 面试 4 连问,你顶得住么?
- 商汤涨涨涨涨:上市4日股价累涨130%,市值一度3000亿港元
- matlab 神经网络预测时间序列示例(水痘模型)
- IDEA运行第一个Spring Boot应用程序
- Linq中的Where与SkipWhile
- SimpleDateFormat类的安全问题,这6个方案总有一个适合你
- Java自动装箱和拆箱
- Struts2返回Json数据(使用Struts2插件)
- 腾讯用微信、QQ 把微视送上了 App Store 第一 | 畅言
- asp.net 将ppt,word转化为pdf实现在线浏览详解
- 关于ibatis中sqlMap配置文件中使用到,的处理
- 十六、Oracle学习笔记:索引和约束(表字段快速查询和约束)
- Codeforces Round #375 (Div. 2) D. Lakes in Berland 贪心
- 在Pyramid中使用Mako模板以及默认和.html后缀关联
- 分布式期末复习总结(林子雨老师)
- 联想计算机系统重装,联想笔记本电脑一键重装系统的方法
- 什么是机器学习(漫画版)
- yun之梦 酒仙wang 实战 带源码
- sqlzoo 答案全集
热门文章
- 增益dB与放大倍数K的转换关系,以及-3dB带宽定义
- PowerDVD v18.0.1815.62 极致蓝光版破解版
- 路上铺个“补丁”,智能汽车高速途中瞬间失控!应用最广自动驾驶技术被曝漏洞 | 字节跳动参与的新研究...
- 数字时代的新一代数据安全
- 找工作时牢牢记住这些
- 台达触摸屏-实现按钮单选功能(宏程序)
- 点积应用-求两个向量夹角
- ST-GCN复现的全过程(详细)
- 锁定的计算机怎么休眠,计算机待机、休眠、锁定的用法和区别
- 6-2 分数计算 (10分)