根据我的理解,我认为:
我说错了吗?
现在,如果我是对的,我有以下问题:
HashMap
在内部使用对象的哈希码。因此,如果两个对象可以具有相同的哈希码,那么HashMap
如何跟踪它使用哪个密钥?
有人可以解释HashMap
内部如何使用对象的哈希码吗?
答案 0 :(得分:318)
散列图的工作原理如下(这有点简化,但它说明了基本机制):
它有许多用于存储键值对的“桶”。每个桶都有一个唯一的编号 - 这就是识别存储桶的内容。将键值对放入映射时,哈希映射将查看键的哈希码,并将该对存储在桶中,其中标识符是键的哈希码。例如:密钥的哈希码是235 - >该对存储在桶号235中。(注意,一个桶可以存储多个键值对)。
当你在hashmap中查找一个值时,通过给它一个键,它将首先查看你给出的键的哈希码。然后,hashmap将查看相应的存储桶,然后通过将它们与equals()
进行比较,将它与您提供的密钥与存储桶中所有对的密钥进行比较。
现在你可以看到这对于在地图中查找键值对非常有效:通过键盘的哈希码,哈希表可以立即知道要查看哪个桶,这样它只需要测试中的内容。那桶。
查看上述机制,您还可以了解密钥的hashCode()
和equals()
方法需要哪些要求:
如果两个键相同(equals()
在比较它们时返回true
),则其hashCode()
方法必须返回相同的数字。如果密钥违反了这个,那么相同的密钥可能会存储在不同的存储桶中,并且hashmap将无法找到键值对(因为它将在同一个存储桶中查找)。
如果两个密钥不同,那么它们的哈希码是否相同并不重要。如果它们的哈希码相同,它们将存储在同一个桶中,在这种情况下,哈希映射将使用equals()
来区分它们。
答案 1 :(得分:84)
你的第三个断言不正确。
两个不等对象具有相同的哈希码是完全合法的。它由HashMap
用作“首过滤镜”,以便地图可以使用指定的键快速找到可能的条目。然后测试具有相同哈希码的密钥与指定密钥的相等性。
您不希望要求两个不相等的对象不能具有相同的哈希码,否则将限制为2 32 可能的对象。 (这也意味着不同的类型甚至不能使用对象的字段来生成哈希码,因为其他类可以生成相同的哈希值。)
答案 2 :(得分:63)
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
对象。
有时可能会发生2个不同对象的哈希码相同。在这种情况下,两个对象将保存在一个存储桶中,并将显示为链接列表。
入口点是最近添加的对象。此对象引用具有next
字段的另一个对象,依此类推。最后一个条目是null
。
使用默认构造函数
创建HashMap
时
HashMap hashMap = new HashMap();
创建的数组大小为16,默认为0.75负载平衡。
hash % (arrayLength-1)
(桶号)HashMap
中保存的密钥添加值,则会覆盖值。如果存储桶已经至少有一个元素,则会添加一个新元素并将其放置在存储桶的第一个位置。它的next
字段指的是旧元素。
hash % (arrayLength-1)
Entry
。
如果找不到所需的元素,请返回null
答案 3 :(得分:34)
您可以在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的hashcode查找存储桶位置,然后调用keys.equals()方法识别LinkedList中的正确节点,并在Java中返回该键的关联值对象HashMap中。
答案 4 :(得分:21)
以下是HashMap
机制的粗略描述,Java 8
版本,(可能与Java 6略有不同)。
hash()
计算的,它决定用于给定密钥的哈希表的哪个桶。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)
。答案 5 :(得分:14)
哈希码确定要检查的hashmap的哪个存储桶。如果存储桶中有多个对象,则执行线性搜索以查找存储桶中的哪个项目等于所需项目(使用equals()
)方法。
换句话说,如果你有一个完美的哈希码,那么hashmap访问是不变的,你将永远不必遍历一个桶(技术上你也必须有一个MAX_INT桶,Java实现可能共享一些哈希码)减少空间要求的同一桶。如果您有最差的哈希码(总是返回相同的数字),那么您的哈希映射访问将变为线性,因为您必须搜索地图中的每个项目(它们都在同一个桶中)才能获得您想要的内容。
大多数情况下,编写良好的哈希码并不完美,但它足够独特,可以为您提供或多或少的持续访问。
答案 6 :(得分:11)
你在第三点上错了。两个条目可以具有相同的哈希码但不相等。看一下HashMap.get from the OpenJdk的实现。您可以看到它检查哈希值是否相等且键是否相等。如果点三是真的,那么就没有必要检查密钥是否相等。在密钥之前比较哈希码,因为前者是更有效的比较。
如果您有兴趣进一步了解这一点,请查看关于Open Addressing collision resolution的维基百科文章,我认为这是OpenJdk实现使用的机制。这种机制与“桶”方法略有不同,其中一个答案提到了。
答案 7 :(得分:5)
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.
答案 8 :(得分:2)
每个Entry对象代表键值对。如果存储桶具有多于1个条目,则字段next将引用其他Entry对象。
有时可能会发生2个不同对象的hashCodes相同。在这种情况下,2个对象将保存在一个存储桶中,并将显示为LinkedList。入口点是最近添加的对象。此对象引用具有下一个字段的其他对象,因此一个。最后一个条目是指null。 使用默认构造函数
创建HashMap时使用大小16和默认0.75负载平衡创建数组。
答案 9 :(得分:1)
哈希映射的工作原理是哈希
HashMap get(Key k)方法在密钥对象上调用hashCode方法,并将返回的hashValue应用于其自己的静态哈希函数,以查找存储桶位置(支持数组),其中键和值以名为Entry的嵌套类的形式存储(Map.Entry)。因此,您已得出结论,从上一行开始,Key和value都作为Entry对象的形式存储在存储桶中。所以认为只有价值存储在桶中是不正确的,不会给面试官留下好印象。
如果key为null,则Null键始终映射到hash 0,因此索引为0。
如果key不为null,则它将调用key对象的hashfunction,参见上面方法中的第4行,即key.hashCode(),所以在key.hashCode()返回hashValue之后,第4行看起来像
int hash = hash(hashValue)
现在,它将返回的hashValue应用到自己的散列函数中。
我们可能想知道为什么我们使用hash(hashValue)再次计算hashvalue。答案是它可以抵御质量差的哈希函数。
现在,最终的哈希值用于查找存储Entry对象的存储桶位置。入口对象像这样存储在桶中(散列,键,值,桶指数)
答案 10 :(得分:1)
我不会详细介绍HashMap的工作原理,但会给出一个例子,这样我们就可以通过将HashMap与现实联系起来来记住HashMap的工作原理。
我们有Key,Value,HashCode和bucket。
有时,我们会将每一项与以下内容联系起来:
使用Map.get(key):
斯蒂维想要找到住在贵宾社会别墅的朋友家(Josse),让它成为JavaLovers Society。 Josse的地址是他的SSN(每个人都不一样)。 我们保留了一个索引,我们在其中找到了基于SSN的社团名称。 该索引可以被认为是找出HashCode的算法。使用Map.put(键,值)
通过查找HashCode然后存储该值,可以找到适合此值的社会。
我希望这会有所帮助,并且可以进行修改。
答案 11 :(得分:1)
两个对象相等,意味着它们具有相同的哈希码,但反之亦然
HashMap中的Java 8更新 -
您在代码中执行此操作 -
myHashmap.put("old","key-value-pair");
myHashMap.put("very-old","old-key-value-pair");
因此,假设为"old"
和"very-old"
键返回的哈希码是相同的。然后会发生什么。
myHashMap
是一个HashMap,假设您最初没有指定其容量。所以java的默认容量是16.所以现在只要使用new关键字初始化hashmap,就会创建16个桶。现在当你执行第一个声明 -
myHashmap.put("old","key-value-pair");
然后计算"old"
的哈希码,并且因为哈希码也可能是非常大的整数,所以,java内部做了这个 - (哈希是哈希码在这里,&gt;&gt;&gt;是右移)
hash XOR hash >>> 16
所以给出一个更大的pictureit将返回一些索引,它将在0到15之间。现在你的键值对"old"
和"key-value-pair"
将被转换为Entry对象的键和值实例变量。然后此条目对象将存储在存储桶中,或者您可以说在特定索引处将存储此条目对象。
FYI- Entry是Map interface-Map.Entry中的一个类,具有这些签名/定义
class Entry{
final Key k;
value v;
final int hash;
Entry next;
}
现在执行下一个语句 -
myHashmap.put("very-old","old-key-value-pair");
和"very-old"
提供与"old"
相同的哈希码,因此这个新的键值对再次发送到同一个索引或同一个桶。但由于此存储桶不为空,因此Entry对象的next
变量用于存储此新的键值对。
并且这将被存储为具有相同哈希码的每个对象的链表,但是TRIEFY_THRESHOLD被指定为值6.因此在此到达之后,链表被转换为平衡树(红黑树)元素作为根。
答案 12 :(得分:0)
据说,一张图片价值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
数组(“桶”数组)的大小。
首先,尝试将其作为“桶”中的第一个元素。如果已有元素,则将新节点附加到列表中。
答案 13 :(得分:-1)
这将是一个很长的答案,喝一杯,继续阅读...
Hashing就是将键值对存储在内存中,可以更快地读写。 它将键存储在数组中,并将值存储在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”
,您会看到两个都给我们相同{ {1}}。 Whooaa !!吸取教训 -
2个相等的对象必须具有相同的hashCode,但不能保证是否 hashCode匹配然后对象是相等的。所以它应该存储 两个值对应于桶1的“误用”和“马心” (1069518484%3)。
现在哈希映射看起来像 -
1069518484
现在,如果某个主体试图找到键Array Index 0 –
Array Index 1 - LinkedIst (“ahhan” , “Manmohan Singh” , “guess what”)
Array Index 2 – LinkedList (“way”)
Array Index 3 –
的值,java会快速找到它的hashCode,对其进行模块化并开始在LinkedList对应的“horsemints”
中搜索它的值。因此,我们不需要搜索所有4个数组索引,从而使数据访问速度更快。
但是,等一下。在LinkedList对应的数组索引1中有3个值,它如何找出哪个是关键字“horsemints”的值?
实际上我撒谎,当我说HashMap只是将值存储在LinkedList中时。
它将两个键值对都存储为映射条目。所以实际上Map看起来像这样。
index 1
现在你可以看到,当遍历对应于ArrayIndex1的linkedList时,它实际上将该LinkedList的每个条目的键与“horsemints”进行比较,当它找到一个时,它只返回它的值。
希望你在阅读时玩得开心:)