HashMap源码分析与实现





面试的时候经常会遇见诸如:“java中的HashMap是怎么工作的”,“HashMap的get和put内部的工作原理”这样的问题。本文将用一个简单的例子来解释下HashMap内部的工作原理。每当hashmap扩容的时候需要重新去add Entry对象,需要重新hash,然后放入我们新的entry table数组里面。如果在工作中,已经知道hashmap需要存多少值,几千或者几万的时候,最好新指定题目的扩容大小,防止在put的时候进行再次扩容很多次。


一、源码分析

1、初始化参数

在hashmap中我们必须要知道的参数有:初始化容量大小,默认是1 << 4,也就是16,

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

加载因子0.75f,

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

hash在什么时候扩容?

put的时候会去做扩容,当大于3/4的时候就会。偶数扩容  ,  2*16    2*32   2*64 
2、put方法

    
    
  1. public V put(K key, V value) {
  2. if (table == EMPTY_TABLE) {
  3. inflateTable(threshold);
  4. }
  5. if (key == null)
  6. return putForNullKey(value);
  7. int hash = hash(key);
  8. int i = indexFor(hash, table.length);
  9. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  10. Object k;
  11. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  12. V oldValue = e.value;
  13. e.value = value;
  14. e.recordAccess( this);
  15. return oldValue;
  16. }
  17. }
  18. modCount++;
  19. addEntry(hash, key, value, i);
  20. return null;
  21. }


对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。

key的hashcode()方法会被调用,然后计算hash值。hash值用来找到存储Entry对象的数组的索引。有时候hash函数可能写的很不好,所以JDK的设计者添加了另一个叫做hash()的方法,它接收刚才计算的hash值作为参数。如果你想了解更多关于hash()函数的东西,可以参考:hashmap中的hash和indexFor方法

indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引。

如果两个key有相同的hash值(也叫冲突),他们会以链表的形式来存储。所以,这里我们就迭代链表。


如果在刚才计算出来的索引位置没有元素,直接把Entry对象放在那个索引上。
如果索引上有元素,然后会进行迭代,一直到Entry->next是null。当前的Entry对象变成链表的下一个节点。
如果我们再次放入同样的key会怎样呢?逻辑上,它应该替换老的value。事实上,它确实是这么做的。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)),如果这个方法返回true,它就会用当前Entry的value来替换之前的value。
对于put的值,我们可以来看一个例子:


public class demo {
    
     public static void main(String[] args ) {
          HashMap hashMap = new HashMap<String, String>();
         Object put1 = hashMap .put( “aa” , “30” ) ;
         Object put2 = hashMap .put( “aa” , “31” ) ;
         System. out .println( put1 + “:” + put2 );
    }
}

当put两个相同的key的时候,这个put返回的值是oldValue,也就是说我这里打印出来的 结果就是  null:30


put返回的值是oldValue。key一样value会覆盖
如果hash key重复,value是不会覆盖的( 经过hash算法返回的值如果重复了)。

对key进行hash处理的时候其实也是进行的位移处理,我们来看到hash的源码。


    
    
  1. final int hash(Object k) {
  2. int h = hashSeed;
  3. if ( 0 != h && k instanceof String) {
  4. return sun.misc.Hashing.stringHash32((String) k);
  5. }
  6. h ^= k.hashCode();
  7. // This function ensures that hashCodes that differ only by
  8. // constant multiples at each bit position have a bounded
  9. // number of collisions (approximately 8 at default load factor).
  10. h ^= (h >>> 20) ^ (h >>> 12);
  11. return h ^ (h >>> 7) ^ (h >>> 4);
  12. }
3、get方法

当你传递一个key从hashmap总获取value的时候:对key进行null检查。如果key是null,table[0]这个位置的元素将被返回。
key的hashcode()方法被调用,然后计算hash值。
indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。
在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。


    
    
  1. public V get(Object key) {
  2. if (key == null)
  3. return getForNullKey();
  4. Entry<K,V> entry = getEntry(key);
  5. return null == entry ? null : entry.getValue();
  6. }


    
    
  1. final Entry<K,V> getEntry(Object key) {
  2. if (size == 0) {
  3. return null;
  4. }
  5. int hash = (key == null) ? 0 : hash(key);
  6. for (Entry<K,V> e = table[indexFor(hash, table.length)];
  7. e != null;
  8. e = e.next) {
  9. Object k;
  10. if (e.hash == hash &&
  11. ((k = e.key) == key || (key != null && key.equals(k))))
  12. return e;
  13. }
  14. return null;
  15. }

4、Entry


hashmap table  :数组+链接   的数据结构 




    
    
  1. void resize(int newCapacity) {
  2. Entry[] oldTable = table;
  3. int oldCapacity = oldTable.length;
  4. if (oldCapacity == MAXIMUM_CAPACITY) {
  5. threshold = Integer.MAX_VALUE;
  6. return;
  7. }
  8. //创建Entry对象的数组
  9. Entry[] newTable = new Entry[newCapacity];
  10. //赋值
  11. transfer(newTable, initHashSeedAsNeeded(newCapacity));
  12. table = newTable;
  13. threshold = ( int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  14. }

二、手写hashmap

通过前面的内容,我们已经知道了hashmap的原理了,我们现在来自己写一个hashmap。

1、map接口


    
    
  1. public interface Map<K,V> {
  2. public V put(K k,V v);
  3. public V get(K k);
  4. public int size();
  5. public interface Entry<K,V>{
  6. public K getKey();
  7. public V getValue();
  8. }
  9. }

2、实现类


    
    
  1. public class HashMap<K,V> implements Map<K, V> {
  2. //初始容量
  3. private static int defaultLength= 16;
  4. //加载因子
  5. private static double defaultLoader= 0.75;
  6. private Entry[] table= null;
  7. private int size= 0;
  8. public HashMap() {
  9. this(defaultLength, defaultLoader);
  10. }
  11. public HashMap(int length,double loader){
  12. defaultLength=length;
  13. defaultLoader=loader;
  14. table= new Entry[defaultLength];
  15. }
  16. public HashMap(Entry[] table, int size) {
  17. super();
  18. this.table = table;
  19. this.size = size;
  20. }
  21. public V put(K k, V v) {
  22. size++;
  23. int index=hash(k);
  24. Entry<K, V> entry=table[index];
  25. if(entry== null){
  26. table[index]=newEntry(k,v, null);
  27. } else{
  28. table[index]=newEntry(k,v,entry);
  29. }
  30. return (V)table[index].getValue();
  31. }
  32. private Entry<K,V> newEntry(K k, V v, Entry<K, V> next) {
  33. return new Entry(k, v, next);
  34. }
  35. public int hash(K k){
  36. int m=defaultLength;
  37. int i=k.hashCode()%m;
  38. return i>= 0?i:-i;
  39. }
  40. public V get(K k) {
  41. int index=hash(k);
  42. if(table[index]== null){
  43. return null;
  44. }
  45. //在数组中查找
  46. return find(k,table[index]) ;
  47. }
  48. public V find(K k, Entry entry) {
  49. if(k==entry.getKey() || k.equals(entry.getKey())){
  50. if(entry.next!= null){
  51. //System.out.println("1oldValue:"+entry.next.getValue());
  52. }
  53. return (V) entry.getValue();
  54. } else{
  55. if(entry.next!= null){
  56. //System.out.println("2oldValue:"+entry.next.getValue());
  57. return find(k, entry.next);
  58. }
  59. }
  60. return null;
  61. }
  62. public int size() {
  63. return size;
  64. }
  65. class Entry<K,V> implements Map.Entry<K, V>{
  66. K k;
  67. V v;
  68. Entry<K,V> next;
  69. public Entry(K k, V v, Entry<K, V> next) {
  70. super();
  71. this.k = k;
  72. this.v = v;
  73. this.next = next;
  74. }
  75. public K getKey() {
  76. return k;
  77. }
  78. public V getValue() {
  79. return v;
  80. }
  81. public K getK() {
  82. return k;
  83. }
  84. public void setK(K k) {
  85. this.k = k;
  86. }
  87. public V getV() {
  88. return v;
  89. }
  90. public void setV(V v) {
  91. this.v = v;
  92. }
  93. public Entry<K, V> getNext() {
  94. return next;
  95. }
  96. public void setNext(Entry<K, V> next) {
  97. this.next = next;
  98. }
  99. }
  100. }

3、通过一个main函数,来测试一下,我们写的这个hashmap是否正确。


    
    
  1. public static void main(String[] args) {
  2. Map<String, Integer> map= new HashMap<String,Integer>();
  3. map.put( "sss", 30);
  4. map.put( "sss", 31);
  5. System.out.println(map.get( "sss"));
  6. }
注意,引入包的时候要注意,不要引入jdk框架的map和hash包,要引入我们自己写的这个包。


HashMap有一个叫做Entry的内部类,它用来存储key-value对。
上面的Entry对象是存储在一个叫做table的Entry数组中。
table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。
key的hashcode()方法用来找到Entry对象所在的桶。
如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。


—————————————————————————————————

文章出处: http://blog.csdn.net/sdksdk0/article/details/79299286
作者:朱培 ID:sdksdk0
—————————————————————————————————



        </article>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值