Java集合Map之HashMap原理(jdk1.7版)

       Map是java开发中最常见的一种数据结构,最常用的Map类型有HashMap、TreeMap、SortedMap等,今天就和大家分享下HashMap的底层原理,以及注意事项。HashMap 底层是基于 数组 + 链表 组成的,但是在具体实现方面jdk1.7和jdk1.8稍微有些不同,今天我们先来学习下jdk1.7的HashMap,请看下图

根据上图我们知道HashMap其实是由一个数组组成,每个数组里面包含一个链表,每个链表是由多个K,V的数据结构组成,OK我们先对其结构有个大概的了解,接下来我们先看下jdk1.7中的HashMap的主要源码实现;

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 既然HashMap底层是数组,那么数组总有个初始化大小吧,这个大小默认就是16,那为什么要写成1 << 4,其实就是为了强调数组的大小要是2的幂。

static final int MAXIMUM_CAPACITY = 1 << 30;该项是指数组最大为2的30次方,为什么是2^30呢,是因为1左移31位的为-2147483648,所以最多只能移30。

static final float DEFAULT_LOAD_FACTOR = 0.75f;该项指的是默认的负载因子为0.75,后面的f是代表float类型。因为Map在使用过程中不断的往里面存放数据,当数组的大小超过一定的容量时,就需要扩容,那到底多大就会触发扩容呢?默认容量16*0.75=12时,就需要将当前 16 的容量进行扩容,看到这里DEFAULT_LOAD_FACTOR 明白是干嘛用了吧!

static final Entry<?,?>[] EMPTY_TABLE = {}; transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; 这边需要结合起来看,首先声明个空的数组,然后将空的数组赋给table。那么这个数组是如何定义的呢,请看源码

static class Entry<K,V> implements Map.Entry<K,V> {
      final K key;
      V value;
      //存储指向下一个Entry的引用,单链表结构
      Entry<K,V> next;
      //对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
      int hash;

      /**
       * Entry构造函数.
       */
      Entry(int h, K k, V v, Entry<K,V> n) {
          value = v;
          next = n;
          key = k;
          hash = h;
}

transient int size;Map存放数量的大小。

int threshold;该项表示下次扩容阀值(容量*加载因子)

final float loadFactor;该项表示负载因子,那和DEFAULT_LOAD_FACTOR有什么不一样呢,其实DEFAULT_LOAD_FACTOR只是初始化了个常量,loadFactor才是真正负载因子的变量,在HashMap无参构造函数中就执行了一行代码

public HashMap() {
     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

 transient int modCount;modCount用于记录HashMap的修改次数,由于HashMap是非线程安全的,不可避免的造成了线程1在遍历,线程2在修改的时候发生冲突,此时就会抛出ConcurrentModificationException异常。

想要了解一个类,就必须先要了解其创建过程,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,容量去默认值,即16  
public HashMap(int initialCapacity) {  
    this(initialCapacity, DEFAULT_LOAD_FACTOR);  
}  

//装载因子取0.75,容量取16,构造HashMap  
public HashMap() {  
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);  
}  

//通过其他Map来初始化HashMap,容量通过其他Map的size来计算,装载因子取0.75,并加入其他map内容 
public HashMap(Map<? extends K, ? extends V> m) {  
     this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,     
     DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);  
     inflateTable(threshold);//初始化HashMap底层的数组结构  
     putAllForCreate(m);//添加m中的元素  
}  

从上面源码可以看出HashMap的构造函数就干了一件事,指定默认容量和装载因子。了解了构造函数接下来我们看下最重要的put和get操作。

public V put(K key, V value) {
      //如果table数组为空数组{},即创建后的第一次put
      if (table == EMPTY_TABLE) {
          inflateTable(threshold);//分配数组空间
      }
      if (key == null)
          return putForNullKey(value);
      int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
      int i = indexFor(hash, table.length);//获取在table中的实际位置
      for (Entry<K,V> e = table[i]; e != null; e = e.next) {
          //如果该对应数据已存在,执行覆盖操作。返回旧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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
      addEntry(hash, key, value, i);//新增一个entry
      return null;
 }

这里面关键几步操作:1.如果数组为空,那么给数据分配空间。2.对key进行hash计算。3.根据hash值找到数组中位置。4.如果数据已存在则覆盖。5.如果在数组中没有找到对应的hashcode,则新建Entry。整个流程看下图:

接下来我们看下是如何给数组分配空间的,请看源码:

private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);
        //记录下次扩容的阀值
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];//分配空间
        initHashSeedAsNeeded(capacity);//更新一下rehash的判断条件,便于以后判断是否rehash,先不用管这个方法
}

/**
* 2的次幂
*/
private static int roundUpToPowerOf2(int number) {
        return number >= MAXIMUM_CAPACITY
               ? MAXIMUM_CAPACITY
               : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

这是初始化数组空间的主要逻辑,请看下图:

添加一个K,V时候如果在数组中找到相应的Entry时候,则需要覆盖老的value,如果没有就需要添加新的Entry,源码如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
      if ((size >= threshold) && (null != table[bucketIndex])) {
          resize(2 * table.length);//进行扩容,新容量为旧容量的2倍
          hash = (null != key) ? hash(key) : 0;
          bucketIndex = indexFor(hash, table.length);//扩容后重新计算插入的位置下标
      }

      //把元素放入HashMap的桶的对应位置
      createEntry(hash, key, value, bucketIndex);
 }
 //创建元素  
 void createEntry(int hash, K key, V value, int bucketIndex) {  
      Entry<K,V> e = table[bucketIndex];  //获取待插入位置元素
      table[bucketIndex] = new Entry<>(hash, key, value, e);//这里执行链接操作,使得新插入的元素指向原有元素。 
      size++;//元素个数+1  
 }  

添加新的Entry之前先要判断下是否需要扩容,如果需要则扩容到之前的两倍,扩容之后需要重新计算位置的下标,重新迁移数据,最后插入数据。请看下图:

ok,这就是put操作的主要源码,接下来我们来看下get源码:

//获取key值为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) {//元素个数为0  
          return null;//直接返回null  
      }  

      int hash = (key == null) ? 0 : hash(key);//获取key的Hash值  
      for (Entry<K,V> e = table[indexFor(hash, table.length)];//根据key和表的长度,定位到Hash桶  
           e != null;  
           e = e.next) {//进行遍历  
          Object k;  
          if (e.hash == hash &&  
              ((k = e.key) == key || (key != null && key.equals(k))))//判断Hash值和对应的key,合适则返回值  
              return e;  
      }  
      return null;  
}  

看完源码详细大家对get的大体流程有了初步了解,首先计算出key的hashcode,根据indexFor找到数组的下标,然后遍历链表,通过key的equals方法比对查找对应的记录。看到这里大家有没有发现个问题呢?就是当 Hash 冲突严重时,在数组里形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O ( N ) 。其实啊这个问题在jdk1.8中得到了优化了,欲知详情敬请关注下一遍《Java集合Map之HashMap原理(jdk1.8版)》

如果你想更加系统的学习java的各种知识,请关注公众号:"辉哥讲技术"

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值