Java集合类之HashMap源码分析

jopen 11年前

    hash表是一种常见的数据结构,主要是通过hash算法将数据尽可能的散列开来存放,当要查找某一数据时,可以通过hash算法直接定位,节省了对比查找的时间。map是一种key、value形式的键值对,将hash表和map结合即形成了HashMap。

    在Java中HashMap的数据是以Entry数组的形式存放的,HashMap通过对key进行hash运算得到一个数组下标,然后将数据存放到Entry数组对应的位置,又因为不同的key进行hash运算可能会得到一个相同的数组下标,为了解决碰撞覆盖冲突,所以Entry本身又是一个链表的结构,即以后不同的key相同数组下标的数据的next会被赋值为已存在Entry链表,新的Entry会替换数组值。

HashMap的存储数据的示例图如下:

Java集合类之HashMap源码分析

HashMap 的put方法的源码解析

public V put(K key, V value) {          if (key == null)              return putForNullKey(value); // HashMap接收key为null的数据          int hash = hash(key.hashCode()); //对key的hashCode再进行hash运算          int i = indexFor(hash, table.length);//根据hash值和entry数组的大小计算出新增数据应该存放的数组位置          for (Entry e = table[i]; e != null; e = e.next) {               // for循环遍历找到的数组下标的entry,如果hash值和key都相等,则覆盖原来的value值                      Object k;              if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                  V oldValue = e.value;                  e.value = value;                  e.recordAccess(this);                  return oldValue;              }          }            modCount++;          //如果上面for循环没有找到相同的hash和key,则增加一个entry          addEntry(hash, key, value, i);          return null;      }

void addEntry(int hash, K key, V value, int bucketIndex) {   Entry e = table[bucketIndex]; //找到下标的entry          //new 一个新的entry,赋值给当前下标数组          table[bucketIndex] = new Entry(hash, key, value, e);          if (size++ >= threshold)              resize(2 * table.length);      }
Entry(int h, K k, V v, Entry n) {              value = v;              next = n; //即将原来数组下标对应的entry赋值给新的entry的next              key = k;              hash = h;          }

    (1)hash值相同且key相等数据将被覆盖。

    (2)添加新的entry时,将已存在的数据下标的entry(可能是null)赋值给新entry的next,新entry将替换原数组下标的值。

HashMap的get方法源码解析 

public V get(Object key) {          //key为null时特别处理          if (key == null)              return getForNullKey();          int hash = hash(key.hashCode());          //indexFor(hash, table.length) 根据hash值和数组长度计算出下标,然后遍历Entry链表          for (Entry e = table[indexFor(hash, table.length)];               e != null;               e = e.next) {              Object k;              if (e.hash == hash && ((k = e.key) == key || key.equals(k)))                  return e.value;          }          return null;      }

总结

  • 一个对象当HashMap的key时,必须覆盖hashCode()和equals()方法,hashCode()的返回值尽可能的分散。
  • 当HashMap的entry的数组足够大,key的hash值足够分散时,即是可以实现一个entry数组下标最多只对应了一个entry,此时get方法的时间复杂度可以达到O(1)。
  • 在数组长度和get方法的速度上要达到一个平衡。数组比较长碰撞出现的概率就比较小,所以get方法获取值时就比较快,但浪费了比较多的空间;当数组长度没有冗余时,碰撞出现的概率比较大,虽然节省了空间,但会牺牲get方法的时间。
  • HashMap有默认的装载因子loadFactor=0.75,默认的entry数组的长度为16。装载因子的意义在于使得entry数组有冗余,默认即允许25%的冗余,当HashMap的数据的个数超过12(16*0.75)时即会对entry数组进行第一次扩容,后面的再次扩容依次类推。
  • HashMap每次扩容一倍,resize时会将已存在的值从新进行数组下标的计算,这个是比较浪费时间的。在平时使用中,如果能估计出大概的HashMap的容量,可以合理的设置装载因子loadFactor和entry数组初始长度即可以避免resize操作,提高put的效率。
  • HashMap不是线程安全的,多线程环境下可以使用Hashtable或ConcurrentHashMap。