根据我的理解,我认为:
- 两个对象具有相同的哈希码是完全合法的。
- 如果两个对象相等(使用equals()方法),则它们具有相同的哈希码。
- 如果两个对象不相等,则它们不能具有相同的哈希码
我对么?
现在,如果正确,我将遇到以下问题:HashMap
内部使用对象的哈希码。因此,如果两个对象可以具有相同的哈希码,那么如何HashMap
跟踪它使用的键?
有人可以解释HashMap
内部如何使用对象的哈希码吗?
根据我的理解,我认为:
我对么?
现在,如果正确,我将遇到以下问题:HashMap
内部使用对象的哈希码。因此,如果两个对象可以具有相同的哈希码,那么如何HashMap
跟踪它使用的键?
有人可以解释HashMap
内部如何使用对象的哈希码吗?
Answers:
哈希图的工作方式如下(这有点简化,但它说明了基本机制):
它具有多个“存储桶”,用于存储键值对。每个存储桶都有一个唯一的数字-标识存储桶。将键值对放入地图时,哈希图将查看键的哈希码,并将该对存储在标识符为键的哈希码的存储桶中。例如:密钥的哈希码为235->该对存储在存储区编号235中。(请注意,一个存储区可以存储多于一个键值对)。
当您在哈希图中查找值时,通过为其提供键,它将首先查看您提供的键的哈希码。哈希图随后将查看相应的存储桶,然后将您提供的密钥与存储桶中所有对的密钥进行比较,方法是将它们与equals()
。
现在,您将看到如何在映射中查找键值对非常有效:通过键的哈希码,哈希图可以立即知道要在哪个存储桶中查找,因此只需要针对该存储桶中的内容进行测试。
通过上述机制,您还可以看到对键hashCode()
和equals()
方法有什么要求:
如果两个键相同(比较时equals()
返回true
),则它们的hashCode()
方法必须返回相同的数字。如果键违反了此规则,则相等的键可能会存储在不同的存储桶中,并且哈希图将无法找到键值对(因为它将在同一存储桶中查找)。
如果两个键不同,那么它们的哈希码是否相同也没关系。如果它们的哈希码相同,它们将存储在同一存储桶中,在这种情况下,哈希图将用于equals()
区分它们。
hashCode()
方法返回不同的哈希码,则键类的equals()
和hashCode()
方法违反协定,并且在中使用这些键时,您会得到奇怪的结果HashMap
。
HashMap
,您可以src.zip
在JDK安装目录中的文件中找到该源代码。
HashMap
是一组Entry
对象。
HashMap
仅考虑对象数组。
看看这Object
是什么:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
…
}
每个Entry
对象代表一个键值对。如果存储桶有多个对象,则该字段next
引用另一个Entry
对象Entry
。
有时可能会发生两个不同对象的哈希码相同的情况。在这种情况下,两个对象将保存在一个存储桶中,并显示为链接列表。入口点是最近添加的对象。该对象引用具有该next
字段的另一个对象,依此类推。最后一个条目指的是null
。
HashMap
使用默认构造函数创建时
HashMap hashMap = new HashMap();
创建的阵列的大小为16,默认负载平衡为0.75。
hash % (arrayLength-1)
元素放置位置(桶号)HashMap
,则该值将被覆盖。如果存储桶中已经有至少一个元素,则会添加一个新元素并将其放置在存储桶的第一个位置。它的next
字段是指旧元素。
hash % (arrayLength-1)
Entry
。如果找不到所需的元素,则返回null
int
当然可以是负数,对负数取模将得到负数
您可以在http://javarevisited.blogspot.com/2011/02/how-hashmap-works-in-java.html中找到出色的信息。
总结一下:
HashMap遵循哈希原理
put(key,value): HashMap将key和value对象都存储为Map.Entry。Hashmap应用hashcode(key)来获取存储桶。如果发生冲突,则HashMap使用LinkedList来存储对象。
get(key): HashMap使用Key Object的哈希码找出存储桶的位置,然后调用keys.equals()方法在LinkedList中标识正确的节点,并在Java HashMap中返回该键的关联值对象。
HashMap
对于Java 8
版本,这是的机制的粗略描述(可能与Java 6略有不同)。
hash()
on键计算的,它决定给定键使用哈希表的哪个存储区。Map.Entry
HashMap.Node
节点的链接列表版本。
它可以代表:
HashMap.TreeNode
Node[] table
Set<Map.Entry> entrySet
实体集。int size
float loadFactor
int threshold
threshold = capacity * loadFactor
int hash(key)
如何将哈希映射到存储桶?
使用以下逻辑:
static int hashToBucket(int tableSize, int hash) { return (tableSize - 1) & hash; }
在哈希表中,容量表示存储桶数,可以从获取table.length
。
也可以通过threshold
和进行计算loadFactor
,因此无需将其定义为类字段。
可以通过以下方式获得有效容量: capacity()
threshold
达到,将增加一倍,哈希表的容量(table.length
),然后对所有的元素进行重新散列重建表。O(1)
,因为:
O(1)
。O(1)
。O(1)
,而不是O(log N)
。哈希码确定要检查哈希图的存储桶。如果存储桶中有多个对象,则进行线性搜索以找到存储桶中的哪个项目等于所需的项目(使用equals()
)方法。
换句话说,如果您拥有完美的哈希码,那么哈希图访问是恒定的,那么您就不必遍历存储桶(从技术上讲,您还必须拥有MAX_INT存储桶,Java实现可能会在同一存储桶中共享一些哈希码来实现)减少空间需求)。如果您拥有最差的哈希码(总是返回相同的数字),则您的哈希图访问将变为线性,因为您必须搜索地图中的每个项目(它们都在同一个存储桶中)才能获得所需的内容。
在大多数情况下,编写良好的哈希码并不完美,但其唯一性足以使您或多或少地获得恒定的访问权限。
您在第三点上弄错了。两个条目可以具有相同的哈希码,但不能相等。看一下OpenJdk中HashMap.get的实现。您会看到它检查散列是否相等且密钥是否相等。如果第三个点为真,那么就不必检查密钥是否相等。在键之前比较哈希码,因为前者是更有效的比较。
如果您有兴趣了解更多有关此方面的知识,请查看Wikipedia上有关“ 开放地址冲突解决”的文章,我相信这是OpenJdk实现所使用的机制。该机制与其他答案之一提到的“存储桶”方法略有不同。
import java.util.HashMap;
public class Students {
String name;
int age;
Students(String name, int age ){
this.name = name;
this.age=age;
}
@Override
public int hashCode() {
System.out.println("__hash__");
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
System.out.println("__eq__");
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Students other = (Students) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
public static void main(String[] args) {
Students S1 = new Students("taj",22);
Students S2 = new Students("taj",21);
System.out.println(S1.hashCode());
System.out.println(S2.hashCode());
HashMap<Students,String > HM = new HashMap<Students,String > ();
HM.put(S1, "tajinder");
HM.put(S2, "tajinder");
System.out.println(HM.size());
}
}
Output:
__ hash __
116232
__ hash __
116201
__ hash __
__ hash __
2
因此,在这里我们看到,如果对象S1和S2都具有不同的内容,则可以肯定我们的重写Hashcode方法将为两个对象生成不同的Hashcode(116232,11601)。现在,由于存在不同的哈希码,因此甚至不必费心调用EQUALS方法。因为不同的Hashcode保证对象中的内容不同。
public static void main(String[] args) {
Students S1 = new Students("taj",21);
Students S2 = new Students("taj",21);
System.out.println(S1.hashCode());
System.out.println(S2.hashCode());
HashMap<Students,String > HM = new HashMap<Students,String > ();
HM.put(S1, "tajinder");
HM.put(S2, "tajinder");
System.out.println(HM.size());
}
}
Now lets change out main method a little bit. Output after this change is
__ hash __
116201
__ hash __
116201
__ hash __
__ hash __
__ eq __
1
We can clearly see that equal method is called. Here is print statement __eq__, since we have same hashcode, then content of objects MAY or MAY not be similar. So program internally calls Equal method to verify this.
Conclusion
If hashcode is different , equal method will not get called.
if hashcode is same, equal method will get called.
Thanks , hope it helps.
两个对象相等,表示它们具有相同的哈希码,反之亦然。
2个相等的对象------>它们具有相同的哈希码
2个对象具有相同的哈希码---- xxxxx->它们不相等
HashMap中的Java 8更新-
您可以在代码中执行此操作-
myHashmap.put("old","old-value");
myHashMap.put("very-old","very-old-value");
所以,假设返回两个按键的哈希码"old"
和"very-old"
是一样的。然后会发生什么。
myHashMap
是HashMap,并假设最初您没有指定其容量。因此,根据Java的默认容量为16。因此,当您使用new关键字初始化hashmap时,它立即创建了16个存储桶。现在,当您执行第一条语句时-
myHashmap.put("old","old-value");
然后"old"
计算for 的哈希码,并且因为该哈希码也可能是非常大的整数,所以java在内部进行了此操作-(哈希在这里是哈希码,而>>>是右移)
hash XOR hash >>> 16
这样一来就给一个更大的图片,它会返回一些指标,这将是你现在的键值对0到15之间,以"old"
和"old-value"
将被转换为输入对象的key和value实例变量。然后该条目对象将存储在存储桶中,或者可以说在特定索引处将存储该条目对象。
FYI- Entry是Map界面Map.Entry中的类,具有这些签名/定义
class Entry{
final Key k;
value v;
final int hash;
Entry next;
}
现在,当您执行下一条语句时-
myHashmap.put("very-old","very-old-value");
和 "very-old"
提供与相同的哈希码"old"
,因此此新键值对再次发送到相同的索引或相同的存储桶。但是,由于此存储桶不为空,next
因此Entry对象的变量用于存储此新键值对。
并将其存储为具有相同哈希码的每个对象的链表,但是用值6指定了TRIEFY_THRESHOLD。因此,到达此值后,链表将转换为以第一个元素为元素的平衡树(红黑树)。根。
哈希图基于哈希原理
HashMap的get(Key k)方法调用键对象上的hashCode方法,并将返回的hashValue应用于其自己的静态哈希函数,以找到存储区位置(支持数组),在该存储区中,键和值以称为Entry(Map)的嵌套类的形式存储。条目)。因此,您已经得出结论,从上一行开始,键和值都作为Entry对象的形式存储在存储桶中。因此,认为仅价值存储在存储桶中是不正确的,并且不会给面试官以良好的印象。
如果key为null,则Null总是映射到哈希0,因此索引为0。
如果key不为null,它将在键对象上调用hashfunction,请参见上述方法的第4行,即key.hashCode(),因此在key.hashCode()返回hashValue之后,第4行如下
int hash = hash(hashValue)
现在,它将返回的hashValue应用于其自己的哈希函数。
我们可能想知道为什么我们再次使用hash(hashValue)计算哈希值。答案是:它可以防御质量差的哈希函数。
现在,使用最终哈希值来查找存储Entry对象的存储桶位置。入口对象像这样存储在存储桶中(哈希,键,值,存储桶索引)
我不会详细介绍HashMap的工作原理,但会举一个例子,以便我们可以通过将HashMap与现实联系起来来记住HashMap的工作原理。
我们有键,值,哈希码和存储桶。
在一段时间内,我们将它们与以下各项相关:
使用Map.get(key):
Stevie想去他朋友的(Josse)住的房子,该房子住在VIP社区的别墅里,那就叫JavaLovers Society。Josse的地址是他的SSN(每个人的地址都不同)。维护了一个索引,在该索引中我们根据SSN找出协会的名称。可以将该索引视为找出HashCode的算法。
使用Map.put(key,Value)
通过找到HashCode,然后为该值找到合适的社会,然后存储该值。
希望对您有所帮助,并且可以进行修改。
这将是一个很长的答案,喝一杯然后继续阅读……
散列是关于将键值对存储在内存中的,可以更快地进行读写。它将键存储在数组中,将值存储在LinkedList中。
假设我要存储4个键值对-
{
“girl” => “ahhan” ,
“misused” => “Manmohan Singh” ,
“horsemints” => “guess what”,
“no” => “way”
}
因此,要存储密钥,我们需要一个由4个元素组成的数组。现在如何将这4个键之一映射到4个数组索引(0,1,2,3)?
因此,java查找单个键的hashCode并将它们映射到特定的数组索引。哈希码公式为-
1) reverse the string.
2) keep on multiplying ascii of each character with increasing power of 31 . then add the components .
3) So hashCode() of girl would be –(ascii values of l,r,i,g are 108, 114, 105 and 103) .
e.g. girl = 108 * 31^0 + 114 * 31^1 + 105 * 31^2 + 103 * 31^3 = 3173020
哈希和女孩!我知道你在想什么 您对那疯狂的二重奏的迷恋可能使您错过一件重要的事情。
为什么Java用31乘以它?
这是因为31是2 ^ 5 – 1形式的奇质数。而且奇数素数减少了哈希冲突的机会
现在,如何将此哈希码映射到数组索引?
答案是,Hash Code % (Array length -1)
。所以“girl”
映射到(3173020 % 3) = 1
在我们的情况下。这是数组的第二个元素。
值“ ahhan”存储在与数组索引1关联的LinkedList中。
HashCollision-如果您尝试查找hasHCode
键“misused”
并 “horsemints”
使用上述公式,您会发现两者都给我们相同1069518484
。哇!学习到教训了 -
2个相等的对象必须具有相同的hashCode,但不能保证hashCode匹配则对象相等。因此,它应将与“滥用”和“ horsemints”相对应的两个值都存储到存储桶1(1069518484%3)。
现在哈希图看起来像–
Array Index 0 –
Array Index 1 - LinkedIst (“ahhan” , “Manmohan Singh” , “guess what”)
Array Index 2 – LinkedList (“way”)
Array Index 3 –
现在,如果某个主体试图找到该键的值,则“horsemints”
Java会迅速找到它的hashCode并将其进行模块化,然后开始在对应的LinkedList中搜索它的值index 1
。因此,这种方式我们不必搜索所有4个数组索引,从而使数据访问更快。
但是,等等,一秒钟。在该linkedList对应的数组索引1中有3个值,它如何找出哪个是键“ horsemints”的值?
实际上,当我说HashMap只是将值存储在LinkedList中时,我撒了谎。
它将两个键值对都存储为映射条目。因此,实际上Map看起来像这样。
Array Index 0 –
Array Index 1 - LinkedIst (<”girl” => “ahhan”> , <” misused” => “Manmohan Singh”> , <”horsemints” => “guess what”>)
Array Index 2 – LinkedList (<”no” => “way”>)
Array Index 3 –
现在您可以看到,遍历与ArrayIndex1对应的linkedList时,实际上将LinkedList的每个条目的键与“ horsemints”进行比较,当找到一个键时,它只是返回它的值。
希望你在阅读的过程中玩得开心:)
据说一张图片值1000字。我说:有些代码胜过1000个单词。这是HashMap的源代码。获取方法:
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
因此,很明显哈希是用于查找“存储桶”的,并且第一个元素始终在该存储桶中进行检查。如果没有,那么equals
使用键的键在链接列表中查找实际元素。
让我们看看put()
方法:
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = 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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
稍微复杂一点,但是很明显,新元素被放置在选项卡中基于哈希计算的位置:
i = (n - 1) & hash
这i
是将放置新元素的索引(或它是“存储桶”)。n
是tab
数组的大小(“存储桶”的数组)。
首先,尝试将其作为该“存储桶”中的第一个元素。如果已经有一个元素,则将一个新节点附加到列表中。