【Java】HashMap底层原理,自己实现HashMap
HashMap基本原理
HashMap 是 Map 的一个实现类,它代表的是一种键值对的数据存储形式。Key 不允许重复出现。
jdk 8 之前,其内部是由数组+链表来实现的,而 jdk 8 对于链表长度超过 8 的链表将转储为红黑二叉树。
基本成员属性
//默认的容量,即默认的数组长度 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 如果小于等于零或者非数字类型也会抛出异常。
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补齐
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方法
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 所指向的头节点的链表或红黑树中,需要遍历查找。
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
node = p;
第二步:如果头节点是红黑树结点,那么调用红黑树自己的遍历方法去得到这个待删结点。否则就是普通链表,使用 do while 循环去遍历找到待删结点。
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。
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获取元素的位置
//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指定键值对
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"));
}
}
上一篇: CentOS 安装 OpenVZ
推荐阅读
-
hashmap源码扩容(hashmap底层原理面试)
-
hashmap源码扩容(hashmap底层原理面试)
-
List、Set集合系列之剖析HashSet存储原理(HashMap底层)
-
Java CAS底层实现原理实例详解
-
走进Java Map家族 (1) - HashMap实现原理分析
-
HashMap原理(一) 概念和底层架构
-
详解 Java HashMap 实现原理
-
[五]类加载机制双亲委派机制 底层代码实现原理 源码分析 java类加载双亲委派机制是如何实现的
-
面试官再问你 HashMap 底层原理,就把这篇文章甩给他看
-
SpringBoot整合log4j日志与HashMap的底层原理解析