HashMap源码理解

导语

HashMap是常用的数据结构,了解HashMap,对提高代码的效率有很大的帮助。HashMap在JDK1.8中对数据结构进行了优化:提高了查询和删除的效率。当然,这也导致了结构更加的复杂;但通过认真阅读源码,还是可以掌握其要领的。

读完本篇文章,你应该理解的内容

点击这里查看大图

说明:HashMap的数据结构是个Hash表(可以理解为数组),每个槽中存放着一些节点。

  1. 一般情况下,一个槽中存放一个节点;
  2. 数据量较大时,一个槽中可能存放多个节点,此时,各个节点以链表的方式连接在一起;
  3. 当一个槽中的节点数很多时(8个以上),会以红黑树的方式来保存这些节点

源码理解

成员变量

//数组默认的大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//数组的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

//加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//阈值:当槽中节点的数量逐渐增大,超过该值时,节点会从链表的形式转换成红黑树的形式
static final int TREEIFY_THRESHOLD = 8;

//阈值:当槽中节点的数量逐渐减小,超过该值时,节点会从红黑树的形式转换成链表的形式
static final int UNTREEIFY_THRESHOLD = 6;

//红黑树的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;

//数组,真正用来保存数据的容器
transient Node<K,V>[] table;

//用于遍历,本篇不做介绍
transient Set<Map.Entry<K,V>> entrySet;

//大小
transient int size;

//修改的次数
transient int modCount;

//阈值:当数组中的数据的个数大于该值时,数组会扩充
int threshold;

//加载因子
final float loadFactor;

说明:从table中可以看出,HashMap最基本的数据结构是个数组;其余的成员变量单独分析是得不到什么结果的,需要结合下面的内容来理解。从常用到的put(),get(),remove()开始理解。

构造方法

在此之前,当然要看看它的构造方法是怎样的:

public HashMap() {
    //加载因子为默认值
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //这里并没有初始化数组
}

//自定义initialCapacity,加载因子使用默认值
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//自定义initialCapacity,和loadFactor
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;

    //设置阈值;阈值大小为2的次方
    //例如:initialCapacity = 17 ,阈值为 32
    //      initialCapacity = 5 ,阈值为 8
    //      initialCapacity = 55 ,阈值为 64
    this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(Map<? extends K, ? extends V> m) {
    //加载因子为默认值
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //将m中的数据存到当前的Map中
    putMapEntries(m, false);
}

说明:前三个构造方法中,只是初始化了一些参数,没有过多的操作;第四个构造方法比较复杂,本篇读完后,再去看源码就容易理解了,这里不做讨论。

put()方法

//间接键值对
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //  tab --数组,用来保存数据的容器
    //  p   --i所对应数组槽中的第一个节点
    //  n   --数组的大小
    //  i   --当前键值对应该存储在数组中的位置
    Node<K,V>[] tab; Node<K,V> p; int n, i;

    //第一次添加数据的处理
    if ((tab = table) == null || (n = tab.length) == 0)
        //数组大小使用默认值
        n = (tab = resize()).length;

    //相应的槽中没有节点的处理
    if ((p = tab[i = (n - 1) & hash]) == null)
        //添加新的节点
        tab[i] = newNode(hash, key, value, null);

    //相应的槽中节点的处理
    else {
        //  e--  用来标记符合条件的节点
        //  k--  键
        Node<K,V> e; K k;

        //槽中第一个节点符合要求的处理
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;

        //槽中的数据结构为红黑树的处理
        else if (p instanceof TreeNode)
            //会尝试从“树”上找到相应的节点并更新
            //如果没有找到,创建新的节点,添加到“树”上
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        //槽中的数据结构为链表的处理
        else {
            //遍历链表,找到对应的值
            for (int binCount = 0; ; ++binCount) {
                //遍历到链表的末尾,仍然没有找到相应的节点,那么新建一个节点,将键值对封装到该节点,添加到链表的尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果链表的长度大于阈值(8),那么将链表转换成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }

                //说明当前e符合条件,结束遍历
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }

        //当有节点符合要求,更新节点中数据
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;

            //LinkedHashMap中会用到,这里没处理
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //如果当前的大小大于阈值,扩充数组的大小
    if (++size > threshold)
        resize();

    //LinkedHashMap中会用到,这里没处理
    afterNodeInsertion(evict);

    return null;
}

说明:实现的细节非常繁琐,但是总结起来就很简单了:

  1. 没有相应的节点,就创建节点,并放到合适的位置
  2. 有相应的节点找到对应的节点,更新其中的数据

额外说明:

  1. put()不会重复保存key,HashSet就是利用了这点来实现去重的
  2. LinkedHashMap会重写其中的一些方法来实现相应的特性

get()方法

//根据key找到value
public V get(Object key) {
    Node<K,V> e;
    //找到相应的节点,返回value
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//找到对应的节点
final Node<K,V> getNode(int hash, Object key) {
    //  tab     --  数组
    //  first   --  数组对应槽中的第一个节点
    //  e       --  对应的节点
    //  n       --  数组的长度
    //  k       --  键
    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;
}

说明:get()可以分为这么几个步骤:

  1. 锁定槽
  2. 从槽中查找相应的节点
  3. 返回合适的数据

remove()方法

//移除相应的节点
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

//移除相应的节点
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    //  tab --  数组,用来保存数据的容器
    //  p   --  index所对应数组槽中的第一个节点
    //  n   --  数组的大小
    //index --  当前键应该存储在数组中的位置
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        //node  --  符合要求的节点
        //  e   --  标记当前节点的下一个节点
        //  k   --  key
        //  v   --  value
        Node<K,V> node = null, e; K k; V v;

        //最上面那张图有个提醒:一般情况下,一个槽中只有一个数据,所以
        //一般情况下先检查第一个节点是否符合要求,符合,直接返回该节点,否则继续查找
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;

        //第一个节点不符合的处理
        else if ((e = p.next) != null) {
            //数据结构为红黑树的处理
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            //数据结构为链表的处理
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }

        //判断node是否符合删除的条件
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            //结构为红黑树时的操作
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

            //要删除的节点是链表且为槽中的首个节点的处理
            else if (node == p)
                tab[index] = node.next;

            //数据结构为链表的处理
            else
                p.next = node.next;
            ++modCount;
            --size;

            //LinkedHashMap中会用到,这里没处理
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

说明:简单来说就是:找到相应的节点并删除并且按照规则移动槽中剩余的节点。

结语

这时再去看第四个构造方法,无非就是变量传进来map,将数据封装到HashMap中来。

本文对链表以及红黑树的的操作没有做进一步的分析。个人认为,阅读源码,如果过分的关注细节可能会难以把握整体的思路;当然,有些时候看源码需要关注细节,这之间需要我们进行平衡,源码看多了,这种平衡感就会有的。(链表和红黑树的操作之后的文章会单独做一些说明)

最后,再一次将核心部分,也就是最开始的那张图贴一下。

点击这里查看大图

转载请标明出处http://blog.csdn.net/qq_26411333/article/details/51723828

时间: 2024-10-13 11:38:46

HashMap源码理解的相关文章

HashMap源码理解与分析

/**   * HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现.与HashTable主要区别为不支持同步和允许null作为key和value.   * HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致.   * 如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap.   * 在JDK1.6中,HashMap采用数组

十分钟深入理解HashMap源码

十分钟就要深入理解HashMap源码,看完你能懂?我觉得得再多看一分钟,才能完全掌握! 终于来到比较复杂的HashMap,由于内部的变量,内部类,方法都比较多,没法像ArrayList那样直接平铺开来说,因此准备从几个具体的角度来切入. 桶结构 HashMap的每个存储位置,又叫做一个桶,当一个Key&Value进入map的时候,依据它的hash值分配一个桶来存储. 看一下桶的定义:table就是所谓的桶结构,说白了就是一个节点数组. transient Node<K,V>[] tab

[Java] HashMap源码分析

1.概述 Hashmap继承于AbstractMap,实现了Map.Cloneable.java.io.Serializable接口.它的key.value都可以为null,映射不是有序的. Hashmap不是同步的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Collections.synchronizedMap(new HashMap()); (除了不同步和允许使用 null 之

HashMap源码分析(转载)

一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Coll

HashMap源码分析(JDK1.8)- 你该知道的都在这里了

我的csdn博客地址: http://blog.csdn.net/brycegao321 HashMap是Java和Android程序员的基本功, JDK1.8对HashMap进行了优化, 你真正理解它了吗? 考虑如下问题: 1.哈希基本原理?(答:散列表.hash碰撞.链表.红黑树) 2.hashmap查询的时间复杂度, 影响因素和原理? (答:最好O(1),最差O(n), 如果是红黑O(logN)) 3.resize如何实现的, 记住已经没有rehash了!!!(答:拉链entry根据高位b

【转】Java HashMap 源码解析(好文章)

- .fluid-width-video-wrapper { width: 100%; position: relative; padding: 0; } .fluid-width-video-wrapper iframe, .fluid-width-video-wrapper object, .fluid-width-video-wrapper embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } [

【Explore SRC】一起看看HashMap源码

HashMap源码一直是众多Java程序员的必经之路,今天我也看看,大家凑热闹不?基于水平有限,有些地方理解错误.理解不了,请大家指出哦~~ > 版本说明 查看的版本是jdk1.7.0_71 > 结构概要图 > 从构造方法看起吧 public HashMap(int initialCapacity, float loadFactor) public HashMap(int initialCapacity) public HashMap() public HashMap(Map<?

Java集合:HashMap源码剖析

一.HashMap概述二.HashMap的数据结构三.HashMap源码分析     1.关键属性     2.构造方法     3.存储数据     4.调整大小 5.数据读取                       6.HashMap的性能参数                      7.Fail-Fast机制 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null

Java中HashMap源码分析

一.HashMap概述 HashMap基于哈希表的Map接口的实现.此实现提供所有可选的映射操作,并允许使用null值和null键.(除了不同步和允许使用null之外,HashMap类与Hashtable大致相同)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Collections.sync