1.1HashMap
为什么80%的码农都做不了架构师?>>>
1、Hash链表结构
在看HashMap源码之前,先复习下Hash表结构:
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
若关键字为 k,则其值存放在f(k)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数,按这个思想建立的表为散列表。
对不同的关键字可能得到同一散列地址,即 k1 != k2,而f(k1)=f(k2),这种现象称为冲突(英语:Collision)。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数f(k)和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为散列表,这一映射过程称为散列造表或散列,所得的存储位置称散列地址。
若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。
维基百科中关于Hash Table的描述清晰的定义了哈希结构的关键因素key、散列函数、冲突。其图示结构如下
2、HashMap结构简述
HashMap继承自Map,实现了Map的主结构Entry<K,V>。
static class Entry<K,V> implements Map.Entry<K,V> {final K key;V value;Entry<K,V> next;int hash;//对key进行hashcode运算后得到的hash值,存储在Entry,避免重复计算Entry(int h, K k, V v, Entry<K,V> n) {value = v;next = n;key = k;hash = h;}}
HashMap.Entry中包含key、value已经指向下一个Entry的引用
定义好主要结构后,HashMap中还定义了
public class HashMap<K, V> {transient HashMap.Entry<K, V>[] table;//HashMap的主干数组transient int size;//实际存储的key-value键值对的个数int threshold; //阈值final float loadFactor;//负载因子,代表了table的填充度有多少,默认是0.75transient int modCount;//用于快速失败}
3、HashMap详解
public class HashMap<K, V> {transient HashMap.Entry<K, V>[] table;//HashMap的主干数组transient int size;//实际存储的key-value键值对的个数int threshold; //阈值final float loadFactor;//负载因子,代表了table的填充度有多少,默认是0.75transient int modCount;//用于快速失败//构造函数public HashMap(){this(16, 0.75F);}public HashMap(int initialCapacity){this(initialCapacity, 0.75F);}public HashMap(int initialCapacity, float loadFactor){//一堆校验后调用this.init();}public HashMap(Map<? extends K, ? extends V> map){this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);inflateTable(threshold);putAllForCreate(m);}}
3.1)关于初始化
jdk1.7中为HashMap提供了4个构造函数,其中三个方法,一堆参数初始化后殊途同归的走向init()方法,当我满心欢喜以为马上就要揭开核心时发现,我操什么鬼,init里面什么都没有。注释都没有。只能心里默默的念叨,好吧看来初始化就是纯粹意义上的初始化。
void init() {}
不过三个构造函数一通看完并不是没有收获,比如
- HashMap 默认容量是16
- HashMap (loadFactor)默认负载因子是0.75
- HashMap (threshold)初始阈值 是容量
至于第四个构造函数,这里暂且按下不说。
3.2)关于写操作
我们知道jdk1.7的Map规范中定义了,Map的写操作就两个,put(K key,V value)和putAll(Map<? extends K, ? extends V> map)
这里先看下put操作
public V put(K key, V value) {if (table == EMPTY_TABLE) {//如果当前的table没有被分配block则inflate tableinflateTable(threshold);}if (key == null)return putForNullKey(value);int hash = hash(key);int i = indexFor(hash, table.length);//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧valuefor (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);return oldValue;}}modCount++;addEntry(hash, key, value, i);return null;} //核心是初始化物理存储位置private void inflateTable(int toSize) {// Find a power of 2 >= toSize 意思是说找到一个大于等于toSize的2的幂次int capacity = roundUpToPowerOf2(toSize);//此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);table = new Entry[capacity];initHashSeedAsNeeded(capacity);}private static int roundUpToPowerOf2(int number) {// assert number >= 0 : "number must be non-negative";return number >= MAXIMUM_CAPACITY? MAXIMUM_CAPACITY: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;}
1、先聊聊方法inflateTable:该方法先是通过Integer的highestOneBit方法对toSize进行取2的幂次处理,然后重新计算阈值、初始化一个大小为capacity的数组。
2、然后我们在看看如果key是null的情况:
private V putForNullKey(V value) {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;}
如果key=null 则将value放入table[0]的位置或者table[0]的冲突链上;
3、关于hash
final int hash(Object k) {int h = hashSeed;if (0 != h && k instanceof String) {return sun.misc.Hashing.stringHash32((String) k);}h ^= k.hashCode();// This function ensures that hashCodes that differ only by// constant multiples at each bit position have a bounded// number of collisions (approximately 8 at default load factor).h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);}
简单说来就是如果没有设置hashSeed,则对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀
4、关于indexFor,进一步处理来获取实际的存储位置
static int indexFor(int h, int length) {// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";return h & (length-1);}
inflateTable中我们说到table的capacity是2的幂次,所以h于上length-1就是如下所示了
比如h=18,length=16
1 0 0 1 0& 0 1 1 1 1__________________0 0 0 1 0 = 2
所以存储位置是table[2]或者其对于的冲突处理链上;
5、addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {//size>=当前阀值,并且对于的table[bucketIndex]位置不为空,则扩容为当前table.length的两倍//关于扩容暂且跳过,等会细聊if ((size >= threshold) && (null != table[bucketIndex])) {resize(2 * table.length);//扩容后重新计算hashhash = (null != key) ? hash(key) : 0;//扩容后重新计算bucketIndexbucketIndex = indexFor(hash, table.length);}//创建一个新的条目createEntry(hash, key, value, bucketIndex);}//创建一个新的Entry,并且Entry中的next指向当前table[bucketIndex]上的值,将新创建的条目作为当前table[bucketIndex]void createEntry(int hash, K key, V value, int bucketIndex) {Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<>(hash, key, value, e);size++;}
至此put方法完了。
最后说下总体put的总体流程
然后创建链表
3.3)关于读操作
public V get(Object key) {if (key == null)return getForNullKey();Entry<K,V> entry = getEntry(key);return null == entry ? null : entry.getValue();}private V getForNullKey() {if (size == 0) {return null;}for (Entry<K,V> e = table[0]; e != null; e = e.next) {if (e.key == null)return e.value;}return null;}final Entry<K,V> getEntry(Object key) {if (size == 0) {return null;}int hash = (key == null) ? 0 : hash(key);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;}
get方法比较简单,可以看出
如果key是null则取table[0]对于的链条上的key == null的条目
如果key不是null,则对key进行hash,然后indexFor,获取到bucketIndex后,循环比较table[bucketIndex]对应链条上的key,获取对应条目对应的值。
所以get的总体流程是
然后循环链表
3.4)关于扩容
在进行put操作是有个判定,如果size>=当前阀值,并且对于的table[bucketIndex]位置不为空就调用resize方法进行扩容。
//扩容容量是当前table.length的两倍void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}//新建一个容量为newCapacity的bucketEntry[] newTable = new Entry[newCapacity];//重新hashtransfer(newTable, initHashSeedAsNeeded(newCapacity));table = newTable;//计算新的阀值threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}/*** Transfers all entries from current table to newTable.*/void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;//循环table桶上所有链条上的条目,重新hash、indexForfor (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);e.next = newTable[i];newTable[i] = e;e = next;}}}
4、jdk1.8新特性
读完了1.7的HashMap源码我以为终于掌握了一套葵花宝典,发现我想多了。1.8中HashMap对于Hash碰撞的解决方案做了比较大的改动,引入了红黑树来解决碰撞。
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent; // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev; // needed to unlink next upon deletionboolean red;
}
引入红黑树后的处理方式变成了链表+红黑树的方式
具体是
/*** Implements Map.put and related methods** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if 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如果没有被初始化,则进行初始化if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//如果没有发生碰撞,则直接赋值if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//如果冲突位置上的key和传入的key一样则新的节点if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//如果冲突位置上的节点是红黑树节点,到红黑树种执行节点插入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) {p.next = newNode(hash, key, value, null);//如果链表的长度大于8是则将链表转换成红黑树if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//如果节点的key在链表中已存在时,e不为空。需要进行value替换if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
5、常见问题
1、极端情况下HashMap的复杂度?
答:jdk1.7是n ,jdk1.8是lgn
2、HashMap什么时候进行bucket分配的?
答:添加第一元素时。
欢迎大家补充
转载于:https://my.oschina.net/shyann/blog/1922377
1.1HashMap相关推荐
- HashMap类Compute方法详解及样例
hashMap简介 hashMap是基于Map的实现,并且允许null value和null key,它不保证存储的数据的顺序.特别要注意的是hashMap不是同步的,要实现同步除了在外部实现同步外, ...
- Java基础day16
Java基础day16 Java基础day16-集合3 1.Map集合 1.1Map集合概述和特点 1.2Map集合的基本功能 1.3Map集合的获取功能 1.4Map集合的遍历(方式1) 1.5Ma ...
- hashMap 底层原理+LinkedHashMap 底层原理+常见面试题
1.源码 java1.7 hashMap 底层实现是数组+链表 java1.8 对上面进行优化 数组+链表+红黑树 2.hashmap 是怎么保存数据的. 在hashmap 中有这样一个结构 Node ...
- Java高并发程序设计学习笔记(五):JDK并发包(各种同步控制工具的使用、并发容器及典型源码分析(Hashmap等))...
转自:https://blog.csdn.net/dataiyangu/article/details/86491786#2__696 1. 各种同步控制工具的使用 1.1. ReentrantLoc ...
- Java Se相关测试题(偏线程、集合)含答案及详解
Java Se相关测试题(偏线程.集合)(简答.编程)含答案及详解 一.选择.简答题 二.编程题 (编程答案有很多思路,实现方式不同,如果有不同见解可打在评论区或私信) 一.选择.简答题 1.publ ...
- 从零开始刷Leetcode——数组(896.905.914.922)
文章目录 896. 单调数列 905. 按奇偶排序数组 914. 卡牌分组 922. 按奇偶排序数组 II 896. 单调数列 如果数组是单调递增或单调递减的,那么它是单调的. 如果对于所有 i &l ...
- Map和Set,简单模拟实现哈希表以及哈希表部分底层源码的分析
目录 Map和Set的简单介绍 降低哈希冲突发生的概率以及当冲突发生时如何解决哈希冲突 简单模拟实现哈希表--1.key为整形:2.key为引用类型 哈希表部分底层源码的分析 1.Map和Set的简单 ...
- Java SE基础知识详解第[12]期—集合(Set、Collections、Map、集合嵌套)
写在前面: 每一个不曾起舞的日子,都是对生命的辜负. 希望看到这里的每一个人都能努力学习,不负韶华,成就更好的自己. 以下仅是个人学习过程中的一些想法与感悟,Java知识博大精深,作为初学者,个人能力 ...
- 【Java】Java基础
1:计算机概述(了解) (1)计算机 (2)计算机硬件 (3)计算机软件 系统软件:window,linux,mac 应用软件:qq,yy (4)软件开发(理解) 软件:是由数据和指令组成的.(计算器 ...
- java-集合框架库
目录 集合框架库 0要点 0.1重点 0.2接口简要介绍 1Collection接口 1.1List接口 1.2Set接口 1.3Queue接口 2Map接口 2.0哈希表 2.1HashMap 2. ...
最新文章
- SQL SERVER的锁机制(三)——概述(锁与事务隔离级别)
- SQL Server 2008 Analysis Services 多维数据库一步一步从入门到精通
- 互联网绑上美国外交政策战车
- python简单代码演示效果-演示python如何创建和使用一个简单的元类的代码
- boost::container实现从内存资源派生的测试程序
- nbiot开发需要掌握什么_学习软件开发需要准备什么?
- C++返回char*第n个位置开始的子字符串
- 设计软件哪里找?图片素材哪里找?
- 学习Python编程培训 有哪些爬虫技术课程需要掌握
- 用递归将嵌套的JSON对象遍历出来,转为二维数组 或一维数组
- 查看显卡显存_选购显卡必须知道的五大参数及分类推荐购买显卡
- bash脚本运行报错问题原因及解决方法
- canvas学习之-七色板
- Misc-Xp0int(数据包分析)
- Linux的进程优先级NI和PR到底有什么区别
- android 传感器应用
- 简单的命令改善你的Linux安全
- MGR 8.0 + ProxySQL 2.0 部署实录
- 【Keil编译警告】warning C316:unterminated conditionals
- linux aio 线程,linux AIO (异步IO) 那点事儿