哈希
-
哈希算法,是一类「算法」,用于求哈希值。
-
哈希表(Hash Table),是一种「数据结构」。
-
哈希函数,Java中实现哈希算法的一类「函数」。
-
Map
是Map/Map的意思,在Java中Map
表示一种把K
映射到V
的「数据类型」。 -
HashMap
,是Java中用哈希表实现的一种「Map
」。
一、Hash算法
1.什么是Hash算法?
hash 英 [hæʃ] 美 [hæʃ]
名词 剁碎的食物;混杂,拼凑;重新土豆 vt. 弄糟,把…弄乱;切碎;推敲 n. (Hash)人名;(阿拉伯、保、英)哈什;(西)阿什
hash处理我觉得叫「切碎」比较合适,但正式上会被称为「散列」,大部分时候也叫「哈希」
Hash算法定义:
-
接受「任意长度的二进制输入值」,对输入值做运算(切碎),最终给出「固定长度的二进制输出值」。
用更好理解的方式来说,哈希算法是摘要算法:它从不同的输入中,通过一些计算摘取一段输出数据,且这个值可以代替输入数据。
其实现的核心目标是:
-
确定性:相同的输入会产生相同的哈希值。
-
高效性:计算哈希值的过程应该注意高效性。
-
均匀性:输入的不同值应尽量映射为不同的哈希值,避免哈希冲突。
-
不可逆性:从哈希值无法反生成原始输入。
哈希算法不是某种固定的算法,它代表的是一类算法。
哈希算法
-
MD5、SHA-1、SHA-256:这些是密码学哈希函数,主要用于加密和数据完整性校验。
-
CRC32:用于校验文件的完整性。
-
FNV-1、MurmurHash:常用于哈希表的实现,因为它们的冲突率较低且计算效率较高。
2.Hash算法有什么用?
加密与信息安全
哈希算法广泛影响信息安全领域。许多密码学协议中都依赖哈希算法来确保数据安全性。
-
密码存储:在系统中,用户密码通常不会直接保存,而是保存其哈希值。登录时,用户输入的密码会被哈希,再与存储的哈希值进行比较。
-
数字签名:哈希算法用于生成数字签名,通过哈希值确认信息是否被篡改。
2.哈希表
哈希算法被广泛评估数据结构中,特别是哈希表(Hash Table),用于提高数据查找效率。
-
哈希表是一种基于键值对的数据结构,哈希算法将键映射到一个阵列的索引位置,从而可以快速查找、插入和删除数据。典型的时间复杂度为 O(1)O(1 )O(1)。
-
Java中的
HashMap
和HashSet(基于HashMap)
都是基于哈希表实现的。
3.去重和查重
哈希值常用于数据去重,如在大量数据中检测是否存在重复的文件或内容。通过计算每个数据项的哈希值,可以快速判断两份数据是否相同(即使内容很多)。
-
常见应用:文件去重系统、缓存去重。
二、hashCode()
hashCode()
Method是Java中Object
类的一个方法,它用于返回对象的缓存码。哈希码用于支持基于哈希表的集合类,如
HashMap
、HashSet
和Hashtable
等。Java中有些类会根据自己的实际用途来重写Object中的
hashCode()
方法比如Integer类,String类,在我们自定义类的时候如果需要也可以重写
hashCode()
方法 //常与equals()
方法一起重写
1. Object类中的默认 hashCode() 实现
Java中Object
类的hashCode()
方法是基于对象的内存地址来生成哈希值的。
默认实现的是对象的内存地址的整数表示形式。Java虚拟机(JVM)通过这种方式保证对象在默认情况下具有唯一的哈希值。
其具体实现因 JVM 不同而存在差异,可能使用对象的地址或其他底层优化方法。
2.整数中的hashCode()
Integer中的hashCode方法直接返回自身存储的数值
3. String中的hashCode()
字符串有一个字符串变量来存储哈希值,即当该字符串第一次调用hashCode()方法时,hash默认值为0,继续执行,当字符串长度大于0时计算出一个哈希值赋给散列
之后再调用hashCode()方法时不会重新计算,直接返回hash;
侵犯时先判断字符串的编码方式然后根据对应的编码方式进入对应的类中(StringLatin1或StringUTF16)默认是StringUTF16
从中我们可以看出源码
计算时,使用的是该字符串截取的一个字符数组值,用每个字符的ASCII值进行计算
哈希计算公式是:s[0]31^(n-1) + s[1]31^(n-2) + ... + s[n-1],n是字符数组的长度,s是字符数组;
算法中还有一个乘数31,为什么使用31呢? hash函数必须选用质数,这是被科学家论证过的hash函数减少冲突的理论; 如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为使用偶数相当于位移运算(低位补0);
31 * i 可以用 (i << 5) - i 来计算,而移位操作的效率高于乘法,所以这是基于性能角度的考虑;
31是个不大不小的质数,兼顾了性能和冲突率,太小hash冲突概率大,太大过于分散占用存储空间大,
所以选择一个不大不小的质数很有必要。
Student student = new Student(); System.out.println("初始化前 对象student的hash码:"+student.hashCode()); student.setName("张三"); student.setAge(18); System.out.println("初始化后 对象student的hash码:"+student.hashCode()); Integer a = 123; System.out.println("数字123的hash码:"+a.hashCode()); String str = "123";// Ascii码 49 50 51 // 49*31*31 + 50*31 + 51 =48690 // 计算时,使用的是该字符串截成的一个字符数组,用每个字符的ASCII值进行计算, // 可以看出哈希计算公式是: // s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1], // n是字符数组的长度,s是字符数组; System.out.println("字符串123的hash码:"+str.hashCode());
三、Hash表
在 Java 中,哈希表(HashTable
)是一种基于散列算法的数据结构,它允许以键值对的形式存储数据,并通过键来高效地查找值。Java 提供了多种实现哈希表的方式,最常见的类包括 Hashtable
和 HashMap
。下面是对这两者的详细介绍:
1. Hashtable
-
简介:
Hashtable
是 Java 最早期引入的一种哈希表实现,它存储键值对并通过哈希函数计算键的哈希值,以实现高效的查找和插入操作。 -
不允许 null: 在
Hashtable
中,键和值都不允许为null
。 -
线程安全:
Hashtable
的所有方法都是同步的,因此它是线程安全的。多个线程可以安全地访问同一个Hashtable
实例。
2. HashMap
-
简介:
HashMap
是Map
接口的另一种实现,也是基于哈希表的数据结构,用于存储键值对。与Hashtable
不同的是,HashMap
的方法是非同步的,即它不是线程安全的。 -
允许 null: 在
HashMap
中,键和值都可以为null
。 -
效率高: 由于
HashMap
不同步,它在多线程环境中需要手动同步,但在单线程环境中性能优于Hashtable
。
package hash; import java.util.HashMap; import java.util.Hashtable; public class Hash { public static void main(String[] args) { // Hashtable 示例 Hashtable<Integer, String> table = new Hashtable<>(); table.put(1, "table1"); table.put(2, "table2"); // 获取值 String value1 = table.get(1); // 返回 "table1" System.out.println(value1); // 遍历键值对 for (Integer key : table.keySet()) { System.out.println("Key: " + key + ", Value: " + table.get(key)); } // HashMap 示例 HashMap<Integer, String> map = new HashMap<>(); map.put(1, "map1"); map.put(2, "map2"); // 获取值 String value2 = map.get(1); // 返回 "map1" System.out.println(value2); // 遍历键值对 for (Integer key : map.keySet()) { System.out.println("Key: " + key + ", Value: " + map.get(key)); } } }
四、HashMap详解
HashMap
是 Java 中常用的哈希表数据结构,用于存储键值对。它通过哈希函数将键映射到存储桶(buckets)中的索引位置,并在桶中存储键值对。下面是 HashMap
的一般实现原理:
1. 数据结构
-
数组 + 链表/红黑树:
HashMap
内部基于一个数组实现存储桶,每个存储桶存储具有相同哈希值的键值对。在 JDK 8 中引入了链表与红黑树的结合优化,在链表长度超过阈值(8)时,将链表转换为红黑树,以提高查找效率。
2. 存储和获取操作
-
插入操作: 当调用
put(key, value)
方法时,HashMap
首先计算键key
的哈希值,并根据哈希值确定存储桶的位置。如果该位置为空,直接将键值对存入;如果该位置已经存在元素,会根据键的equals()
方法判断是否为同一键,如果是,则更新对应的值;如果不是,则在该位置形成一个链表或红黑树,将新的键值对添加到链表或树中。 -
获取操作: 当调用
get(key)
方法时,HashMap
首先计算键key
的哈希值,并确定存储桶的位置。然后,遍历该位置的链表或红黑树,根据键的equals()
方法查找对应的值。
3. 哈希冲突的处理
-
哈希冲突: 不同的键可能会映射到同一个存储桶位置,这就是哈希冲突。
HashMap
通过链表或红黑树来处理同一存储桶中的多个键值对。 -
链表处理: JDK 8 之前,哈希冲突时使用链表,新元素会插入到链表的头部。
-
红黑树处理: JDK 8 引入了红黑树来优化
HashMap
,提高处理链表过长的性能。当链表长度达到一定阈值(默认为8),链表将会转换为红黑树,以提高查找效率。当链表转换为红黑树后,查找、插入、删除的时间复杂度能保持在 O(log n) 级别。 (链表的时间复杂度为O(n))
package hash; import org.w3c.dom.Node; import java.util.LinkedList; class MyHashMap<K, V> { // 每个桶是一个链表,链表里存储 Entry private LinkedList<Entry<K, V>>[] buckets; private static final int DEFAULT_CAPACITY = 16; // 默认桶的数量 private int size = 0; // 当前 HashMap 中的键值对数量 // 构造函数 public MyHashMap() { this(DEFAULT_CAPACITY); } // 初始化指定大小的哈希表 public MyHashMap(int capacity) { buckets = new LinkedList[capacity]; // 初始化每个桶(链表) for (int i = 0; i < capacity; i++) { buckets[i] = new LinkedList<>(); } } // 哈希函数,返回键的哈希值对应的桶索引 private int getBucketIndex(K key) { return key.hashCode() % buckets.length; } // 内部静态类,表示每个键值对 private static class Entry<K, V> { K key; V value; Entry(K key, V value) { this.key = key; this.value = value; } } // 插入键值对,如果键已经存在,则更新值 public void put(K key, V value) { int bucketIndex = getBucketIndex(key); LinkedList<Entry<K, V>> bucket = buckets[bucketIndex]; // 遍历当前桶,检查键是否已经存在 for (Entry<K, V> entry : bucket) { if (entry.key.equals(key)) { entry.value = value; // 如果键已经存在,则更新值 return; } } // 如果键不存在,则插入新的键值对 bucket.add(new Entry<>(key, value)); size++; } // 获取值,根据键查找 public V get(K key) { int bucketIndex = getBucketIndex(key); LinkedList<Entry<K, V>> bucket = buckets[bucketIndex]; // 遍历桶中的所有 Entry,查找匹配的键 for (Entry<K, V> entry : bucket) { if (entry.key.equals(key)) { return entry.value; // 找到对应的值 } } // 如果没找到,返回 null return null; } // 返回当前键值对数量 public int size() { return size; } // 判断键是否存在 public boolean containsKey(K key) { int bucketIndex = getBucketIndex(key); LinkedList<Entry<K, V>> bucket = buckets[bucketIndex]; // 遍历桶中的所有 Entry,查找匹配的键 for (Entry<K, V> entry : bucket) { if (entry.key.equals(key)) { return true; } } return false; } // 删除指定的键值对 public void remove(K key) { int bucketIndex = getBucketIndex(key); LinkedList<Entry<K, V>> bucket = buckets[bucketIndex]; // 使用迭代器删除键 var iterator = bucket.iterator(); while (iterator.hasNext()) { Entry<K, V> entry = iterator.next(); if (entry.key.equals(key)) { iterator.remove(); size--; return; } } } }
代码解析
-
基础结构:
-
使用一个数组
buckets
来存储每个桶(链表),每个桶是一个LinkedList
,链表中存储Entry
对象,Entry
保存键和值。 -
DEFAULT_CAPACITY
定义了默认的哈希表大小(桶的数量),默认是16
。
-
-
哈希函数:
-
getBucketIndex(K key)
方法计算出给定键在哈希表中对应的桶索引,使用键的hashCode()
值对桶数量取模(key.hashCode() % buckets.length
)。
-
-
Entry
类别:-
Entry<K, V>
是一个内部静态类,用于存储键值对。每个Entry
对象包含key
和value
。
-
-
put(K key, V value)
方法:-
put
用来插入键值对。首先计算索引桶内键的哈希值对应的,然后检查该桶内是否已经有相同的键,如果有,更新值;如果没有,将新的Entry
插入链表。
-
-
get(K key)
方法:-
get
用于查找键对应的值。根据键的哈希值找到桶,查找链表查找键,找到时返回对应的值,找不到返回null
。
-
-
size()
方法:-
返回当前
HashMap
中存储的按键值对的数量。
-
-
containsKey(K key)
方法:-
检查某个键是否存在于
HashMap
中,查找逻辑和get
类似。
-
-
remove(K key)
方法:-
remove
根据 键删除键值对。首先找到对应的桶,遍历桶中的链表,通过迭代器移除匹配的键。
-