HashMap源码分析

HashMap

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

1、先看HashMap实现了那些接口

HashMap源码分析

2、属性

// ***
  private static final long serialVersionUID = 362498820763181265L;
  // 默认的初始容量是16
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
  // 最大容量
  static final int MAXIMUM_CAPACITY = 1 << 30;
  // 默认的填充因子
  static final float DEFAULT_LOAD_FACTOR = 0.75f;
  // 当桶(bucket)上的结点数大于这个值时会转成红黑树
  static final int TREEIFY_THRESHOLD = 8;
  // 当桶(bucket)上的结点数小于这个值时树转链表
  static final int UNTREEIFY_THRESHOLD = 6;
  // 桶中结构转化为红黑树对应的table的最小大小
  static final int MIN_TREEIFY_CAPACITY = 64;
  // 存储元素的数组,总是2的幂次倍
  transient Node<k,v>[] table;
  // 存放具体元素的集
  transient Set<map.entry<k,v>> entrySet;
  // 存放元素的个数,注意这个不等于数组的长度。
  transient int size;
  // 每次扩容和更改map结构的计数器
  transient int modCount;
  // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
  int threshold;
  // 加载因子
  final float loadFactor;
  • Node[] table 的初始化长度 length(默认值是 16),loadFactor 为负载因子 (默认值 DEFAULT_LOAD_FACTOR 是 0.75),threshold 是 HashMap 所能容纳的最大数据量的 Node(键值对) 个数。
  • threshold = length * loadFactor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
  • 这里我们需要加载因子 (load_factor),加载因子默认为 0.75,当 HashMap 中存储的元素的数量大于 (容量 × 加载因子),也就是默认大于 16*0.75=12 时,HashMap 会进行扩容的操作。

3、构造器

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    //使用控构造器时,加载因子等于默认的0.75f
}
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;
    this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
       this(initialCapacity, DEFAULT_LOAD_FACTOR);
   }
public HashMap(Map<? extends K, ? extends V> m) {
       this.loadFactor = DEFAULT_LOAD_FACTOR;
       putMapEntries(m, false);
   }

4、内部结构(还未转化为红黑树)

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

HashMap源码分析

5、添加元素

public V put(K key, V value) {
      return putVal(hash(key), key, value, false, true);
  }

6、hash算法

先得到key.hashcode,然后在进行二次hash

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

对象保存的位置为(table.lenth-1)&hash(key)

6、putVal()

HashMap源码分析
HashMap源码分析

7、get()

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

大抵的思路是先计算hash值然后在通过h&(table.lenth-1)得到桶的位置,然后再遍历节点。

参考资料:

https://www.jianshu.com/p/b40fd341711e
https://github.com/Snailclimb/JavaGuide