【Java】HashMap底层原理,自己实现HashMap
HashMap基本原理
HashMap 是 Map 的一个实现类,它代表的是一种键值对的数据存储形式。Key 不允许重复出现。
jdk 8 之前,其内部是由数组+链表来实现的,而 jdk 8 对于链表长度超过 8 的链表将转储为红黑二叉树。
基本成员属性
[code]//默认的容量,即默认的数组长度 16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大的容量,即数组可定义的最大长度 static final int MAXIMUM_CAPACITY = 1 << 30; //实际存储的键值对个数 transient int size; //用于迭代防止结构性破坏的标量 transient int modCount; //负载因子,指元素在总素组百分比,超过这个比例要进行数组扩容 final float loadFactor; //HashMap 中默认负载因子为 0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; //阈值 int threshold;
构造函数
最基本的构造函数,需要调用方传入两个参数,initialCapacity 和 loadFactor。程序的大部分代码在判断传入参数的合法性,initialCapacity 小于零将抛出异常,大于 MAXIMUM_CAPACITY 将被限定为 MAXIMUM_CAPACITY。loadFactor 如果小于等于零或者非数字类型也会抛出异常。
[code]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); }
构造函数的核心在于初始化操作threshold: >>> 无符号右移,忽略符号位,空位都以0补齐
[code]static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
通过异或的位运算将两个字节的 n 打造成比 cap 大但最接近 2 的 n 次幂的一个数值。因为 2 的 n 次幂小一的值在二进制角度看全为 1,将有利于 HashMap 中的元素搜索。
put方法
[code]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; //根据键的 hash 值找到该键对应到数组中存储的索引 //如果为 null,那么说明此索引位置并没有被占用 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //不为 null,说明此处已经被占用,只需要将构建一个节点插入到这个链表的尾部即可 else { Node<K,V> e; K k; //当前结点和将要插入的结点的 hash 和 key 相同,说明这是一次修改操作 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果 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) treeifyBin(tab, hash); break; } //遍历的过程中,如果发现与某个结点的 hash和key,这依然是一次修改操作 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //e 不是 null,说明当前的 put 操作是一次修改操作并且e指向的就是需要被修改的结点 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //如果添加后,数组容量达到阈值,进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
resize函数分为两个部分
- 拿到旧数组长度,如果长度达到极限限定就不再扩容,如果未达到极限旧将数组容量扩大两倍,阈值也扩大两倍
- 根据新容量初始化新数组,将数组每个节点元素静止拷贝到新数组,获取头结点,如果节点是红黑树结点,红黑树分裂,转移至新表
remove方法
第一步:需要删除的结点就是这个头节点,让 node 引用指向它。否则说明待删除的结点在当前 p 所指向的头节点的链表或红黑树中,需要遍历查找。
[code]if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) node = p;
第二步:如果头节点是红黑树结点,那么调用红黑树自己的遍历方法去得到这个待删结点。否则就是普通链表,使用 do while 循环去遍历找到待删结点。
[code]else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } }
第三步:如果是红黑树结点的删除,直接调用红黑树的删除方法进行删除即可,如果是待删结点就是一个头节点,那么用它的 next 结点顶替它作为头节点存放在 table[index] 中,如果删除的是普通链表中的一个节点,用该结点的前一个节点直接跳过该待删结点指向它的 next 结点即可。最后,如果 removeNode 方法删除成功将返回被删结点,否则返回 null。
[code]if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; }
equals()和hashcode()
equals()为true的话,hashcode()一定相等
hash code()相等的话,equals()不一定为true
在put键值对的时候,实在比较equals(),如果相等,则覆盖。
为什么HashMap长度为2的幂次
hashmap获取元素的位置
[code]//h位元素的hashcode,length为数组长度,&相当于× static int indexFor(int h, int length) { return h & (length-1); }
如果length为2的次幂 则length-1 转化为二进制必定是11111……的形式,与h的二进制&操作效率高,如果不是11111...的形式,&后必有0位,造成位置浪费,增加碰撞几率
节约空间,使元素平均分布
根据以上原理,用数组+链表实现是一个简单的HashMap来加强理解
- put方法,把键值对入HashMap
- get方法,获取HashMap指定键值对
[code]package test; import java.util.HashMap; import java.util.LinkedList; /** * 键值对 */ class MyEntry{ Object key; Object value; public MyEntry(Object key, Object value) { super(); this.key = key; this.value = value; } } /** * 自己实现hashmap,了解底层结构 * @author 袁盛桐 * */ public class MyHashMap { //Map的底层结构为数组+链表 LinkedList[] arr = new LinkedList[999]; int size; /** * 往数组中添加键值对 * @param key * @param value */ public void put(Object key,Object value) { //new一个键值对储存key和value MyEntry e = new MyEntry(key, value); //a为key的hascode取余数组长度 int hash = key.hashCode(); hash = hash<0?-hash:hash; int a = hash%arr.length; //如果该地址没有链表对象,把对象连在链表对应位置 if(arr[a]==null) { LinkedList list = new LinkedList(); arr[a]=list; list.add(e); }else { //如果键值一样,新的键值对替换旧的 LinkedList list = arr[a]; //遍历这个位置的链表每个对象 for(int i=0;i<list.size();i++) { MyEntry e2 = (MyEntry)list.get(i); if(e2.key.equals(key)) { e2.value=value; return; } } arr[a].add(e); } } /** * 取值 */ public Object get(Object key) { int a = key.hashCode()%arr.length; if(arr[a]!=null) { LinkedList list = arr[a]; for(int i=0;i<list.size();i++) { MyEntry e = (MyEntry)list.get(i); if(e.key.equals(key)) { return e.value; } } } return null; } public static void main(String[] args) { MyHashMap test = new MyHashMap(); test.put("111", "111-111"); test.put("111", "222-222"); System.out.println(test.get("111")); } }
阅读更多
- Java集合 --- HashMap底层实现和原理
- Java中HashMap底层实现原理(JDK1.8)源码分析
- 最小二乘法多项式曲线拟合原理与实现(错误地方已经修改底层补充自己写的java实现)
- 【Java】Iterator底层原理,自己实现Iterator
- Java之HashMap底层实现原理/HashMap、HashTable、HashSet
- Java中HashMap底层实现原理(JDK1.8)源码分析
- (转载)Java中HashMap底层实现原理(JDK1.8)源码分析
- 【Java】HashSet底层原理,自己实现HashSet
- HashMap底层实现原理的Java演示
- java——HashMap的实现原理,自己实现简单的HashMap
- 算法---hash算法原理(java中HashMap底层实现原理和源码解析)
- Java中HashMap底层实现原理(JDK1.8)源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- Java中HashMap底层实现原理(JDK1.8)源码分析
- JAVA HashMap底层实现原理
- Java基础面试题2-HashMap的源码,实现原理,底层结构
- Java中HashMap底层实现原理(JDK1.8)源码分析
- Java面试绕不开的问题: Java中HashMap底层实现原理(JDK1.8)源码分析
- java面试之HashMap的实现原理和底层数据结构