欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Java】HashMap底层原理,自己实现HashMap

程序员文章站 2022-06-04 19:56:14
...

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"));
		
	}

}