我正在尝试编写演示代码,以显示当地图大小超过负载因子阈值时,Hashmap中正在发生重新哈希。 我如何证明内部进行了哈希处理。我还想证明即使在重新哈希期间将旧条目移到新存储桶中,我也可以使用旧键来获取旧元素(让我知道我的假设是正确的)。 下面的示例代码。
import java.util.*;
class RehashDemo{
public static void main(String[] args){
Map<Integer,String> numbers = new HashMap<>(10);
for(int i = 0; i<10;i++){
numbers.put(i,i+"");
}
System.out.println(numbers);
for(int j = 15; j<=20;j++){
numbers.put(j,j+"");
}
System.out.println(numbers);
}
}
答案 0 :(得分:4)
编写一个程序来演示重新哈希并不难,但是您必须了解很多有关HashMap的内部组织,对象的哈希码如何生成,哈希码如何与HashMap的内部结构相关以及这如何影响迭代顺序的信息。< / p>
简而言之,HashMap由一系列存储桶(“表”)组成。每个存储桶都是键值对的链接列表。将一对其键哈希值添加到已被占用的存储桶中,将其添加到该存储桶的链接列表的末尾。可通过调用键的hashCode()
方法,将其与高阶16位(无符号右移16位)(请参见source)进行异或,然后采用表大小的模数来确定存储区。由于表的大小始终是2的幂,因此本质上是与(tablesize-1)的掩码进行“与”运算。 Integer
对象的哈希码只是其整数值。 (source)。最后,HashMap的迭代顺序依次遍历每个存储桶,还依次遍历每个存储桶内的对的链表。
毕竟,您可以看到小整数值将最终出现在相应的存储桶中。例如,Integer.valueOf(0).hashCode()
为0。在进行异和运算后,它将保持为0,并且任何表大小的模量都将保持为0。因此,整数0结束于存储区0,整数1结束于存储区1,并且等等。但是不要忘记,存储桶是表大小的模数。因此,如果表大小为8,则整数8最终会出现在存储区0中。
利用此信息,我们可以用整数键填充HashMap,这些键将最终存储在可预测的存储桶中。让我们创建一个表大小为8且默认加载因子为0.75的HashMap,这意味着我们可以在重新哈希发生之前添加六个映射。
Map<Integer, Integer> map = new HashMap<>(8);
map.put(0, 0);
map.put(8, 8);
map.put(1, 1);
map.put(9, 9);
map.put(2, 2);
map.put(10, 10);
{0=0, 8=8, 1=1, 9=9, 2=2, 10=10}
打印出地图(实质上是使用其toString()
方法),如上所述,依次迭代地图。我们可以看到0和8出现在第一个存储桶中,1和9出现在第二个存储桶中,而2和10出现在第三个存储桶中。现在让我们添加另一个条目:
map.put(3, 3);
{0=0, 1=1, 2=2, 3=3, 8=8, 9=9, 10=10}
迭代顺序已更改!添加新映射超出了重新哈希的阈值,因此表大小增加了一倍,达到16。重新哈希已完成,这次模数为16而不是8。而0和8之前都在存储区0中,现在它们位于独立的存储桶,因为有可用存储桶的两倍。与1/9和2/10相同。当表大小为16时,旧表大小为8的每个存储桶中的第二个条目现在散列到其自己的存储桶中。您可以看到这一点,因为迭代顺序地通过存储桶进行,并且每个存储桶中现在都有一个条目。
当然,我仔细选择了整数值,以使表大小为8时发生冲突,而表大小为16时不发生冲突。这使我们可以非常清楚地看到重新哈希。对于更典型的对象,很难预测哈希码(因此也包括存储桶),因此,很难看到冲突以及发生哈希时发生的变化。