【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来加强理解

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

}

猜你喜欢

转载自blog.csdn.net/weixin_38516944/article/details/81324096