JAVA经典集合框架学习笔记——HashMap的底层实现原理

  最近做的几个项目都是用Map来存储的数据 ,虽然用得挺顺手,但是对HashMap的底层原理却只知甚少,今天便来简单学习和整理一下。

  数据结构中有数组和链表这两个结构来存储数据。

  数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;

  链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。

  综合这两者的优点,摒弃缺点,哈希表就诞生了,既满足了数据查找方面的特点,占用的空间也不大。

  哈希表可以说就是数组链表,底层还是数组但是这个数组每一项就是一个链表。

  在这个数组中,每个元素存储的其实是一个链表的头,元素的存储位置一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。

  HashMap的构造函数

  HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现。

  HashMap提供了三个构造函数:

  HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。

  HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。

       HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();

  每次初始化HashMap都会构造一个table数组,而table数组的元素为Entry节点。

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
}

  HashMap也可以说是一个数组链表,HashMap里面有一个非常重要的内部静态类——Entry,这个Entry非常重要,它里面包含了键key,值value,下一个节点next,以及hash值,Entry是HashMap非常重要的一个基础Bean,因为所有的内容都存在Entry里面,HashMap的本质可以理解为 Entry[ ] 数组。

  HashMap.put(key,value)

public V put(K key, V value) {
        //当key为null,调用putForNullKey方法,保存null与table第一个`        位置中,这是HashMap允许为null的原因
        if (key == null)
            return putForNullKey(value);
        //计算key的hash值
        int hash = hash(key.hashCode());                  ------(1)
        //计算key hash 值在 table 数组中的位置
        int i = indexFor(hash, table.length);             ------(2)
        //从i出开始迭代 e,找到 key 保存的位置
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判断该条链上是否有hash值相同的(key相同)
            //若存在相同,则直接覆盖value,返回旧value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;    //旧值 = 新值
                e.value = value;
                e.recordAccess(this);
                return oldValue;     //返回旧值
            }
        }
        //修改次数增加1
        modCount++;
        //将key、value添加至i位置处
        addEntry(hash, key, value, i);
        return null;
    }
 

  我简单的理解一下,当执行put操作的时候,HashMap会先判断一下要存储内容的key值是否为null,如果为null,如果为null,则执行putForNullKey方法,这个方法的作用就是将内容存储到Entry[]数组的第一个位置,如果key不为null,则去计算key的hash值,然后对数组长度取模,得到要存储位置的下标,再迭代该数组元素上的链表,看该链表上是否有hash值相同的,如果有hash值相同的,就直接覆盖value的值,如果没有hash值相同的情况,就将该内容存储到链表的表头,最先储存的内容会放在链表的表尾,其实这带代码也顺道解释了HashMap没有Key值相同的情况。这里还有一个情况也要说明一下,会不会出现链表过长的情况?随着要存储的内容越来越多,HashMap里面的东西也越来越多,相同下标的情况也增多,那么迭代链表的也无疑增加了,这会影响数据的查询效率,HashMap对此也做了优化,当HashMap中存储的内容超过数组长度 *loadFactor时,数组就会进行扩容,默认的数组长度是16,loadFactor为加载因子,默认的值为0.75。对于扩容需要说明的一点就是,扩容是一个非常“消耗”的过程,需要重新计算数据在新数组中的位置,并且将内容复制到新数组中,如果我们预先知道HashMap中的元素个数,预设元素的个数,能有效的提高HashMap的存储效率。

  HashMap.get(key)

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

final Entry<K,V> getEntry(Object key) {

        if (size == 0) {

            return null;

        }

 

        int hash = (key == null) ? 0 : hash(key);

        for (Entry<K,V> e = table[indexFor(hash, table.length)];

             e != null;

             e = e.next) {

            Object k;

            if (e.hash == hash &&

                ((k = e.key) == key || (key != null && key.equals(k))))

                return e;

        }

        return null;

    }

  get(key)方法的代码比较好理解,根据key的hash值找到对应的Entry即链表,然后在返回该key值对应的value。

  HashMap的遍历

public static void main(String[] args) {  
  
  
  Map<String, String> map = new HashMap<String, String>();  
  map.put("1", "value1");  
  map.put("2", "value2");  
  map.put("3", "value3");  
    
  //第一种:普遍使用,二次取值  
  System.out.println("通过Map.keySet遍历key和value:");  
  for (String key : map.keySet()) {  
   System.out.println("key= "+ key + " and value= " + map.get(key));  
  }  
    
  //第二种  
  System.out.println("通过Map.entrySet使用iterator遍历key和value:");  
  Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();  
  while (it.hasNext()) {  
   Map.Entry<String, String> entry = it.next();  
   System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());  
  }  
    
  //第三种:推荐,尤其是容量大时  
  System.out.println("通过Map.entrySet遍历key和value");  
  for (Map.Entry<String, String> entry : map.entrySet()) {  
   System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());  
  }  
  
  //第四种  
  System.out.println("通过Map.values()遍历所有的value,但不能遍历key");  
  for (String v : map.values()) {  
   System.out.println("value= " + v);  
  }  
 }   

  使用HashMap的匿名内部类Entry遍历比使用keySet()效率要高很多,使用forEach循环时要注意不要在循环的过程中改变键值对的任何一方的值,否则出现哈希表的值没有随着键值的改变而改变,到时候在删除的时候会出现问题。 此外,entrySet比keySet快些。对于keySet其实是遍历了2次,一次是转为iterator,一次就从hashmap中取出key所对于的value。而entrySet只是遍历了第一次,他把key和value都放到了entry中,所以就快了。

     

 

posted @ 2017-08-28 23:43  钢铁家撸撸  阅读(17725)  评论(0编辑  收藏  举报