java数据结构12_HashMap类底层实现

哈希表也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如:Redis)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出现在各类的面试题中,重要性可见一斑。

在讨论哈希表之前,我们先回顾一下数组和链表来实现对数据的存储的优缺点:

**数组:**占用空间连续。 寻址容易,查询速度快。但是,增加和删除效率非常低。

**链表:**占用空间不连续。 寻址困难,查询速度慢。但是,增加和删除效率非常高。

从上分析我们知道,数组优势是查询效率高,链表的优势是增删效率高。那么有没有一种数据结构能结合“数组+链表”的双方优点呢?答案就是“哈希表”。

哈希表的本质就是“数组+链表”,这是一种非常重要的数据结构。在哈希表中进行添加、删除和查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成。

我们知道,数据结构的物理存储结构只有两种:顺序存储结构链式存储结构。而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。

我们打开HashMap源码,发现有如下两个核心内容:
在这里插入图片描述
其中的,Node[] table 就是HashMap的核心数组结构,我们也称之为“位桶数组”。我们再继续看Node是什么,源码如下:
在这里插入图片描述
一个Node对象存储了:

key:键对象

value:值对象

next:下一个节点

hash:键对象的hash值

显然就是一个单向链表结构,我们使用图形表示一个Entry的典型示意:
在这里插入图片描述
然后,我们画出Node[]数组的结构(这也是HashMap的结构):
在这里插入图片描述
由图可知,哈希表就是数组链表,底层还是数组但是这个数组每一项就是一个链表。

接下来我们来基于JDK1.7来模拟HashMap的实现,本章节重点模拟HashMap的put()方法和get()方法,在进行模拟put()方法和get()方法的实现之前,我们先做好相关的准备工作。

首先创建一个Node节点类,Node节点类是HashMap的内部类,它有几个重要的属性:键对象(key) 、值对象(value)、键对象的hash值(hash)和下一个节点(next)。

代码实现如下:

class MyHashMap<K, V> {
// Node节点,是一个单链表
	static class Node<K, V> {
		int hash; // 键对象的hash值
		K key; // 键对象
		V value; // 值对象
		Node<K, V> next; // 下一个节点
		// 构造方法
		public Node(int hash, K key, V value, Node<K, V> next) {
			this.hash = hash;
			this.key = key;
			this.value = value;
			this.next = next;
		}
	}
}

Node节点类实现完毕,我们继续添加HashMap中的两个重要属性:哈希表的Node[]数组(table)和存放元素的个数(size)属性。

代码实现如下:

class MyHashMap<K, V> {
	// HashMap的核心数组结构,数组中的每一个元素都是一个链表
	private Node<K, V>[] table = new Node[16]; // 默认长度为16
	// 实际存储元素的个数
	private int size;
	// 此处省略Node节点的实现
}

注意:本次模拟HashMap属于简化实现,此处并没有去考虑table数组的“扩容问题”,所以我们在声明table数组的同时并完成了数组的初始化操作,默认初始化长度16个空间大小。

  • 存储数据过程put(key,value)

准备工作完成之后,我们继续深入学习HashMap如何存储数据。此处的核心是如何产生hash值,该值用来对应数组的存储位置。
在这里插入图片描述
我们的目的是将“key-value”两个对象成对存放到HashMap的Node[]数组中。

实现步骤:

【第一步】:判断key****是否为null

先判断一下要存储内容的key值是否为null,如果为null,则执行putForNullKey()方法,这个方法的作用就是将内容存储到table数组的第一个位置。

【第二步】:获得key****对象的hashcode

如果key不为null,则再去调用key对象的hashcode()方法,获得key对象的哈希值。

【第三步】:获得存储位置的下标

hashcode是一个整数,我们需要将它转化成[0,数组长度-1]范围的整数。我们要求转化后的hash值尽量均匀地分布在[0,数组长度-1]这个区间,减少“hash冲突”。

公式:下标 = key的哈希值%数组长度

【第四步】: Node对象添加到table****数组中

当table[index]返回的结果为null时,则直接创建一个新的Entry对象添加到table[index]处。

当table[index]返回的结果不为null时,则判断链表中是否在相同key。如果存在同的key,就用新的value代替老的value,也就是执行覆盖操作。如果不存在相同的key,那么新创建的Node对象将会储存在链表的表头,通过next指向原有的Node对象,形成链表结构(hash碰撞解决方案)。

代码实现如下:

class MyHashMap<K, V> {
	// 此处省略HashMap的属性
	/**
	 * 添加键值对
	 * 	如果存在相同的key,则返回被覆盖的value值
	 * 	如果不存在相同的key,则返回null
	 */
	public V put(K key, V value) {
		// 第一步:如果 key 为 null,调用 putForNullKey 方法写入null键的值
		if(null == key) {
			return putForNullKey(value); // null总是放在数组的第一个链表中
		}
		// 第二步:获得key对象的hashcode,确保散列均匀
		int hashCode = key.hashCode();
		// 第三步:获取在table中的实际位置,也就是在数组中的下标
		int index = hashCode % table.length;
		// 第四步:将Node对象添加到table数组中
	// 如果table[index]不为 null,通过循环不断遍历链表查找是否在链表中有相同key
        for(Node<K, V> e = table[index]; null != e; e = e.next) {
			// 找到与插入的值的key相同的Node
			if(e.hash == hashCode && (e.key == key || key.equals(e.key))) {
				// 保存覆盖之前的value值
				V oldValue = e.value;
				// key值相同时直接替换value值
				e.value = value;
				// 结束方法,完成hashMap添加的操作
				return oldValue;
			}
		}
		// 如果table[index]为null或者key的hash值相同而key不同,则需要新增Node
		addNode(hashCode, key, value, index);
		return null;
	}
	/**
	 * 添加元素节点
	 */
	private void addNode(int hashCode, K key, V value, int index) { 
		// 获取索引值为hash的Node对象
		Node<K, V> e = table[index];
		// 在table数组中新增Node对象
		table[index] = new Node<K, V>(hashCode, key, value, e);
		// 实际存放元素个数累加
		size++;
	}
	/**
	 * 当key为null时,存放key所对应的value值。
	 */
	public V putForNullKey(V value) {
		// 当数组的第一个元素,存在key为null时,直接覆盖以前的旧值即可
		for(Node<K, V> e = table[0]; null != e; e = e.next) {
			// 找到与插入的值的key相同的Node
			if(null == e.key) {
				// 保存覆盖之前的value值
				V oldValue = e.value;
				// key值相同时直接替换value值
				e.value = value;
				// 结束方法,完成hashMap添加的操作
				return oldValue;
			}
		}
		// 当数组的第一个元素,不存在key为null时,直追加元素即可
		addNode(0, null, value, 0);
		return null;
	}
	// 此处省略Node节点的实现
}

总结:简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。

  • 取数据过程get(key)

实现步骤:

我们需要通过key对象获得“键值对”对象,进而返回value对象。明白了存储数据过程,取数据就比较简单了。

第一步】:判断key是否为null

先判断一下要获取内容的key值是否为null,如果为null,则执行getForNullKey()方法,这个方法的作用就是将内容存储到table数组的第一个位置。

第二步:获得key对象的hashcode

如果key不为null,则再去调用key对象的hashcode()方法,获得key对象的哈希值。

【第三步】:获得存储位置的下标

获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置找到对应的链表。

第四步:在链表上挨个比较key对象

调用equals()方法,将key对象和链表上所有节点的key对象进行比较,直到碰到返回true的节点对象为止。如果key对象和链表上的某个节点的key对象相同,则直接返回该节点对象的value对象值。如果链表遍历比较完毕,都没有遇到key对象和链表节点的key对象相同的情况,那么证明key对象对应的value对象不存在,直接但会null即可!

代码实现如下:

class MyHashMap<K, V> {
	// 此处省略HashMap的属性
	/**
	 * 根据key,获取key所对应的value值
	 */
	public V get(Object key) {
		// 如果key是null,调用getForNullKey取出null的value 
		if(null == key) {
			return getForNullKey();
		}
		// 1.根据该 key的hashCode值计算它的 hash码 
		int index = key.hashCode() % table.length;
		// 2.直接取出table数组中指定索引处的值,
		for(Node<K, V> e = table[index]; null != e; e = e.next) {
			// 如果该 Entry 的 key和hash 与被搜索 key 相同 
			if ((e.hash == index && e.key == key) || key.equals(e.key)) {
				return e.value; 
			} 
		}
		return null;
	}
	/**
	 * 当key为null时,获取key所对应的value值
	 */
	public V getForNullKey() {
		for (Node<K,V> e = table[0]; e != null; e = e.next) {
            if (null == e.key)
                return e.value;
        }
        return null;
	}
	// 此处省略Node节点的实现
}

到此处,基于JDK1.7关于HashMap底层put()方法和get()方法的实现就讲解完毕,那么接下来我们来补充两个关于HashMap的知识点。

  • 扩容问题

HashMap的位桶数组,初始大小为16。实际使用时,显然大小是可变的。如果位桶数组中的元素达到(0.75*数组 length), 就重新调整数组大小变为原来2倍大小。

  • JDK1.8使用红黑树的改进

在JDK8中对HashMap的源码进行了优化,在JDK7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询效率较低。

在JDK8中,HashMap处理“碰撞”增加了红黑树这种数据结构。当碰撞结点较少时,采用链表存储;当较大时(大于8个),采用红黑树来存储,这样大大的提高了查找的效率。
在这里插入图片描述

ps:如需最新的免费文档资料和教学视频,请添加QQ群(627407545)领取。

发布了35 篇原创文章 · 获赞 0 · 访问量 356

猜你喜欢

转载自blog.csdn.net/zhoujunfeng121/article/details/104555544