Java中HashMap的实现原理

Java中的hashCode和equals

关于hashCode

  • hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的
  • 如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同
  • 如果对象的equals方法被重写,那么对象的hashCode也要重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点
  • 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里“

再归纳一下就是hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的。
以下对hashCode的解读摘自其他博客:

1.hashcode是用来查找的。
例如内存中有这样的位置
0 1 2 3 4 5 6 7
而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之一,如果不用hashcode而任意存放,那么当查找时就需要到这八个位置里挨个去找,或者用二分法一类的算法。
但如果用hashcode那就会使效率提高很多。
我们这个类中有个字段叫ID,那么我们就定义我们的hashcode为ID%8,然后把我们的类存放在取得得余数那个位置。比如我们的ID为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果ID是13,求得的余数是5,那么我们就把该类放在5这个位置。这样,以后在查找该类时就可以通过ID除 8求余数直接找到存放的位置了。
2.但是如果两个类有相同的hashcode怎么办那(我们假设上面的类的ID不是唯一的),例如9除以8和17除以8的余数都是1,那么这是不是合法的,回答是:可以这样。那么如何判断呢?在这个时候就需要定义 equals了。
也就是说,我们先通过 hashcode来判断两个类是否存放某个桶里,但这个桶里可能有很多类,那么我们就需要再通过 equals 来在这个桶里找到我们要的类。
那么。重写了equals(),为什么还要重写hashCode()呢?
想想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不通过重写hashcode()来找到桶,光重写equals()有什么用啊

关于equals

1.equals和==
== 用于比较引用和比较基本数据类型时具有不同的功能:
比较基本数据类型,如果两个值相同,则结果为true
而在比较引用时,如果引用指向内存中的同一对象,结果为true;
equals()作为方法,实现对象的比较。由于 == 运算符不允许我们进行覆盖,也就是说它限制了我们的表达。因此我们重写equals()方法,达到比较对象内容是否相同的目的。而这些通过 == 运算符是做不到的。
2.object类的equals()方法的比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:
java.io.file,java.util.Date,java.lang.string,包装类(Integer,Double等)
String s1=new String(“abc”);
String s2=new String(“abc”);
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
运行结果为false true
3.为了实现在向HashMap中添加键值对,可以根据对象的内容来判断两个对象是否相等,这就需要重写hashCode()方法和equals()方法,实例如下:

package com.base.hashmap;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class hashmap {
	public static void main(String[] args) {

		HashMap<Person, String> hm = new HashMap<Person, String>();
		Person p1 = new Person("111", "name1");
		Person p2 = new Person("111", "name1");
		hm.put(p1, "address1");
		hm.put(p2, "address2");
		
		Iterator iter = hm.entrySet().iterator();
		while(iter.hasNext()) {
			Map.Entry entry = (Map.Entry) iter.next();
			Person key = (Person) entry.getKey();
			String val = (String) entry.getValue();
			System.out.println("key="+key+"  value:"+val);
		}
	}
}

class Person{
	String id;
	String name;
	
	public Person(String id,String name) {
		this.id = id;
		this.name = name;
	}
	
	//hashCode,equals和toString方法是source-》自动生成的
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		if (id == null) {
			if (other.id != null)
				return false;
		} else if (!id.equals(other.id))
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	
	@Override
	public String toString() {
		return "Person [" + (id != null ? "id=" + id + ", " : "") + (name != null ? "name=" + name : "") + "]";
	}
}

输出结果如下:
key=Person [id=111, name=name1] value:address2

HashMap的节点

HashMap是一个集合,键值对的集合,源码中每个节点用Node<K,V>表示

static class Node<K,V> implements Map.Entry<K,V> {
   final int hash;
   final K key;
   V value;
   Node<K,V> next;
   ...
}

Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。

HashMap的数据结构

HashMap的数据结构为 数组+(链表或红黑树),上图:
在这里插入图片描述为什么采用这种结构来存储元素呢?
数组的特点:查询效率高,插入,删除效率低。
链表的特点:查询效率低,插入删除效率高。
在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。

HashMap存储元素的过程

有这样一段代码:

HashMap<String,String> map = new HashMap<String,String>();
map.put("刘德华","张惠妹");
map.put("张学友","大S");

现在我要把键值对 “刘德华”,”张惠妹”存入map:
**第一步:**计算出键“刘德华”的hashcode,该值用来定位要将这个元素存放到数组中的什么位置。
调用这个方法会生成一个int型的整数,我们叫它哈希码,哈希码和调用它的对象地址和内容有关。
通过hashcode值和数组长度取模我们可以得到元素存储的下标。
刘德华的hashcode为33,数组长度为16,则要存储在数组索引为 33%16=1的地方。
在这里插入图片描述可以分两种情况:

  1. 数组索引为1的地方是空的,这种情况很简单,直接将元素放进去就好了。

  2. 已经有元素占据了索引为1的位置,这种情况下我们需要判断一下该位置的元素和当前元素是否相等,使用equals来比较。

如果使用默认的规则是比较两个对象的地址。也就是两者需要是同一个对象才相等,当然我们也可以重写equals方法来实现我们自己的比较规则最常见的是通过比较属性值来判断是否相等。

如果两者相等则直接覆盖,如果不等则在原元素下面使用链表的结构存储该元素。

在这里插入图片描述每个元素节点都有一个next属性指向下一个节点,这里由数组结构变成了数组+链表结构,红黑树又是怎么回事呢?
因为链表中元素太多的时候会影响查找效率,所以当链表的元素个数达到8的时候使用链表存储就转变成了使用红黑树存储,原因就是红黑树是平衡二叉树,在查找性能方面比链表要高。

HashMap中的两个重要的参数

HashMap中有两个重要的参数:初始容量大小和加载因子。初始容量大小是创建时给数组分配的容量大小,默认值为16,用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用resize方法将数组容量增加到原来的两倍,专业术语叫做扩容。

那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.751000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能。
在这里插入图片描述
创建HashMap时我们可以通过合理的设置初始容量大小来达到尽量少的扩容的目的。加载因子也可以设置,但是除非特殊情况不建议设置。

总结

总结:HashMap的实现原理:

  1. 利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
  3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
  4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

在这里插入图片描述

<think>好的,用户现在想了解JavaHashMap实现原理,特别是数据结构与存储机制。根据用户提供的引用内容,我需要综合这些信息来组织一个清晰的回答。 首先,用户之前的对话是关于Spring WebSocket的网络切换问题,现在突然转到HashMap,需要确认是否是同一用户或是否有上下文关联。但根据当前问题,用户明确引用了四个关于HashMap的引用内容,所以应该专注于这些内容。 用户给出的引用中提到了HashMap数据结构是数组和链表的结合,后来可能转换为红黑树,扩容机制是两倍扩容,负载因子0.75。需要将这些信息整合起来,解释HashMap的内部结构、哈希计算、解决冲突的方法、扩容过程等。 需要确保回答的结构清晰,分点说明,比如数据结构、存储机制、哈希冲突处理、扩容机制等部分。同时,引用中的内容要正确标注来源,例如引用[2]提到数组和链表结合,引用[4]提到扩容机制和负载因子。 可能用户对HashMap的理解有一定基础,但需要系统性的讲解。需要检查是否有遗漏的关键点,比如哈希函数、树化阈值、null键处理等。另外,用户可能对线程安全感兴趣,但引用中没有提到,可以简要提及。 在回答时要注意使用中文,行内公式用$...$,独立公式用$$...$$,但根据问题内容,可能涉及较少的数学公式,主要是数据结构的描述和流程说明。需要确保LaTeX语法正确,例如哈希函数的位置计算。 最后,生成的相关问题需要与HashMap相关,比如线程安全、红黑树转换、初始容量选择等。确保问题能够引导用户进一步了解HashMap的更多细节。</think>### Java HashMap实现原理详解 #### 一、核心数据结构 HashMap的底层实现基于**数组+链表/红黑树**的复合结构,具体可分为以下层次: 1. **数组(哈希桶)** 默认初始容量为$16$,每个数组元素称为一个**桶(bucket)**,存储链表的头节点或红黑树的根节点[^3]。 $$ \text{数组索引计算公式:} \ index = (n-1) \ \& \ hash(key) $$ 其中$n$为数组长度,需保持为$2$的幂(如16/32/64)以优化位运算效率[^3]。 2. **链表与红黑树** - **链表**:哈希冲突时,新元素以尾插法加入链表(Java 8前为头插法) - **红黑树**:当链表长度≥$8$且数组长度≥$64$时,链表自动转换为红黑树,时间复杂度从$O(n)$优化为$O(\log n)$[^4] ```java // Node节点定义(链表结构) static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // 链表指针 } ``` #### 二、存储机制与哈希冲突 1. **哈希计算** 使用键的`hashCode()`二次加工: ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 通过高位异或减少哈希冲突概率[^4] 2. **键值存储流程** ```mermaid graph TD A[计算key的哈希值] --> B{数组索引是否冲突?} B -->|无冲突| C[直接存入数组位置] B -->|冲突| D[遍历链表/红黑树] D --> E{节点key是否已存在?} E -->|存在| F[更新value] E -->|不存在| G[添加新节点] G --> H{链表长度≥8?} H -->|是| I[转换为红黑树] ``` #### 三、动态扩容机制 1. **扩容触发条件** 当元素数量 > 阈值(阈值 = 容量 × 负载因子,默认$16×0.75=12$)时触发扩容[^4] 2. **扩容过程** - 新容量 = 原容量 × 2(保持$2^n$特性) - 数据迁移时重新计算索引,利用高位判断: $$ \text{新索引} = \begin{cases} \text{原索引} & \text{高位为0} \\ \text{原索引 + 原容量} & \text{高位为1} \end{cases} $$ - 红黑树在迁移时可能退化为链表(节点数≤6时) #### 四、特殊特性 1. **允许null键/值**:`hash(null)`返回$0$,存储在数组第$0$个位置 2. **非线程安全**:多线程操作可能导致数据覆盖或死循环(Java 7头插法问题) 3. **快速失败机制**:迭代时检测到结构性修改会抛出`ConcurrentModificationException` --- ### 相关问题 1. HashMap为什么选择红黑树而非其他平衡树? 2. 如何设计一个高性能的哈希函数? 3. ConcurrentHashMap如何实现线程安全?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值