java中HashMap工作原理

1,数据结构

hashmap作为key-value集合,理想效果是能达到O(1)复杂度。

结构图:

java中HashMap工作原理

数据结构HashMap

public class HashMap<K, V> extends AbstractMap<K, V>
  implements Map<K, V>, Cloneable, Serializable
{
  static final int DEFAULT_INITIAL_CAPACITY = 16;
  static final int MAXIMUM_CAPACITY = 1073741824;
  static final float DEFAULT_LOAD_FACTOR = 0.75F;
  transient Entry[] table;//这里就是用来存K-V的数组
  transient int size;
  int threshold;
  final float loadFactor;
  volatile transient int modCount;
  private transient Set<Map.Entry<K, V>> entrySet = null;
  private static final long serialVersionUID = 362498820763181265L;
。
。
。
}

数据结构Entry

static class Entry<K, V> {
    final K key;
    V value;
    Entry<K, V> next;//key.hashcode()值相同的下一个节点
final int hash;
    Entry(int paramInt, K paramK, V paramV, Entry<K, V> paramEntry)
    {
      this.value = paramV;
      this.next = paramEntry;
      this.key = paramK;
      this.hash = paramInt;//hash(key.hashCode())的值,其实是经过了两次hash后的值
}
    public K getKey();
    public V getValue();
    public V setValue(V paramV);
    public boolean equals(Object paramObject);
    public int hashCode();
}

2,hashmap的工作原理

接下来就是put(key, value)get(key)过程,为了通过放入的过程(put)来理解取出(get)的过程,这里先说put(key, value)

put(key, value)过程,源码和注释:

public V put(K paramK, V paramV)
  {
    if (paramK == null)
      return putForNullKey(paramV);
int i = hash(paramK.hashCode());//第二次hash处理
//i&this.table.length-1求出下标
int j = indexFor(i, this.table.length);
//下标j就是K在Entry数组中的位置bucket,遍历Entry链表,若K存在(注意K
//引用相同或者equals()条件满足都视为存在)放入V并返回原值;否则新增
    for (Entry localEntry = this.table[j]; localEntry != null; localEntry = localEntry.next)
    {
      Object localObject1;
      if ((localEntry.hash != i) || (((localObject1 = localEntry.key) != paramK) && (!paramK.equals(localObject1))))
        continue;
      Object localObject2 = localEntry.value;
      localEntry.value = paramV;
      localEntry.recordAccess(this);
      return localObject2;
    }
this.modCount += 1;
//新增Entry节点(这里是在bucket位置从链表头部插入,常数复杂度)
    addEntry(i, paramK, paramV, j);
    return null;
  }

get(key)过程,源码和注释:

public V get(Object paramObject)
  {
    if (paramObject == null)
      return getForNullKey();
//跟put方法一样,先根据K求出下标,找到bucket位置,然后遍历Entry链表
    int i = hash(paramObject.hashCode());
    for (Entry localEntry = this.table[indexFor(i, this.table.length)]; localEntry != null; localEntry = localEntry.next)
    {
      Object localObject;
      //此处就是K要尽量使用持久化对象的原因哦,K的hashcode变了或者equals方法不
      //满足就找不到啦,也是我们要同时复写K的hashCode()和equals方法的原因
      if ((localEntry.hash == i) && (((localObject = localEntry.key) == paramObject) || (paramObject.equals(localObject))))
        return localEntry.value;
    }
    return null;
  }

通过以上源码分析可以看出在bucket不太大的情况下put(key, value)get(key)操作都是O(1),文章开头之所以说是理想效果就是考虑到了最坏的情况,当bucket无限增大,导致所有Entry都挤在一个bucket里面,那么每一次put(key, value)get(key)都是O(n)复杂度,HashMap也就没意义了!一个好的hash算法是兼顾性能和碰撞率的,性能就是hash值的计算过程不能太耗时间,碰撞率在这里说的就是让Entry尽可能的离散,不要挤在一个bucket里面。这里从两个方面来说碰撞率:

hash算法

常用的作为KString.hashCode计算:

 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

使用 int 算法,这里 s[i] 是字符串的第 i 个字符,n 是字符串的长度

第二次hash:

  //paramInt为key.hashCode()的值
  static int hash(int paramInt)
  {
    paramInt ^= paramInt >>> 20 ^ paramInt >>> 12;
    return paramInt ^ paramInt >>> 7 ^ paramInt >>> 4;
  }

计算方法挺简单的,就是几次无符号移位以及异或运算,然而我并不明白其中的奥妙!

table也就是Entry数组的大小以及rehash

这里其实想说的就是加载因子loadFactor(默认0.75),也就是Entry数(this.size)达到this.table.length*0.75就要rehash了,

HashMap构造函数:

 //paramInt初始容量 paramFloat 加载因子
  public HashMap(int paramInt, float paramFloat)
  {
    if (paramInt < 0)
      throw new IllegalArgumentException("Illegal initial capacity: " + paramInt);
    if (paramInt > 1073741824)
      paramInt = 1073741824;
    if ((paramFloat <= 0.0F) || (Float.isNaN(paramFloat)))
      throw new IllegalArgumentException("Illegal load factor: " + paramFloat);
int i = 1;
//可以看到table.length的初始值是大于paramInt的最小的2的对数
    while (i < paramInt)
      i <<= 1;
    this.loadFactor = paramFloat;
    this.threshold = (int)(i * paramFloat);
    this.table = new Entry[i];
    init();
  }

put操作中,新增节点调用addEntry函数:

void addEntry(int paramInt1, K paramK, V paramV, int paramInt2)
  {
    Entry localEntry = this.table[paramInt2];
this.table[paramInt2] = new Entry(paramInt1, paramK, paramV, localEntry);
//条件满足的话,rehash
    if (this.size++ >= this.threshold)
      resize(2 * this.table.length);//扩大为2倍
  }
 //调整table为两倍大小
  void resize(int paramInt)
  {
    Entry[] arrayOfEntry1 = this.table;
    int i = arrayOfEntry1.length;
    if (i == 1073741824)
    {
      this.threshold = 2147483647;
      return;
    }
    Entry[] arrayOfEntry2 = new Entry[paramInt];
    transfer(arrayOfEntry2);
    this.table = arrayOfEntry2;
    this.threshold = (int)(paramInt * this.loadFactor);
  }
  //rehash
  void transfer(Entry[] paramArrayOfEntry)
  {
    Entry[] arrayOfEntry = this.table;
    int i = paramArrayOfEntry.length;
    for (int j = 0; j < arrayOfEntry.length; j++)
    {
      Object localObject = arrayOfEntry[j];
      if (localObject == null)
        continue;
      arrayOfEntry[j] = null;
      //遍历原table每一个bucket,计算Entry在新table的下标,并将Entry仍然从bucket     //头部插入,可以看到rehash后每个bucket会被翻转一次
      do
      {
        Entry localEntry = ((Entry)localObject).next;
        int k = indexFor(((Entry)localObject).hash, i);
        ((Entry)localObject).next = paramArrayOfEntry[k];
        paramArrayOfEntry[k] = localObject;
        localObject = localEntry;
      }
      while (localObject != null);
    }
  }

从这里也可以看出HashMaprehash是单线程的,不是一个线程安全类,在rehash过程中另一个线程进行get(key)操作可能会出现数据不一致问题。

第一次写博客,说的也都是很基础的东西,不足之处希望大家多多指教。

你可能感兴趣的:(java,HashMap)