HashMap - 基本思想


HashMap会分四篇博客介绍,分别是基本思想、优化、线程安全和拓展。本文介绍基本思想。


HashMap 基本数据结构

Java HashMap解决哈希冲突,使用了成链法,故采用了数组Node[]加链表Node.next的数据结构。
Java8为降低链表查询的时间复杂度,引入了红黑树TreeNode

class HashMap<Key, Value> {

    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    private static final int DEFAULT_TABLE_SIZE = 16;
    private static final int TREEIFY_THRESHOLD = 8;

    private int size; // HashMap 大小,即存储键值对的个数
    private float loadFactor; // 负载因子
    private int threshold; // 当前 table 长度和 loadFactor 负载因子下,能容下的最大键值对数
    private Node[] table; // 存储键值对的数组

    public HashMap() {
        this(DEFAULT_TABLE_SIZE, DEFAULT_LOAD_FACTOR);
    }

    public HashMap(int initSize, float initLoadFactor) {
        this.loadFactor = initLoadFactor;
        // table 的长度必须是 2^n,后面解释为什么这样设计
        int tableSize = tableSizeFor(initSize);
        // JDK 源码中 table 数组不在在构造函数中初始化,是 resize 时初始化
        this.table = new Node[tableSize];
        // 初始化阈值
        this.threshold = (int) (this.table.length * this.loadFactor);
    }

    /**
     * 节点数据结构,存储 Key 和 Value
     */
    private static class Node<K, V> implements Map.Entry<K, V> {
        int hash;
        K key;
        V value;
        Node next;
    }

    /**
     * 红黑树数据结构
     */
    private static class TreeNode<K, V> extends Node<K, V> {
        TreeNode<K, V> parent;
        TreeNode<K, V> left;
        TreeNode<K, V> right;

        /**
         * 将一个节点插入当前的红黑树
         */
        TreeNode<K, V> putTreeVal(Node<K, V> node) { }
    }

    /**
     * 求大于等于 cap 且为 2^n 的数
     */
    static final int tableSizeFor(int cap) { }
}

如何存储 - 第一步:找桶

public void put(Key key, Value value) {
    int hashCode = key.hashCode();
    int hash = hash(hashCode);
    // 等价于 index = hash % table.length
    int index = hash & (table.length - 1);
}

/**
 * 扰动函数:目的是增大 hashCode 的随机性,使其更散列。
 */
private int hash(int hashCode) { }
  • 问题一:数组长度为什么设计成2^n

    用位运算代替乘除和求余运算,提高效率。
    扩容是 2 倍扩容,可以直接使用左移运算;与运算可代替求余运算。
  • 问题二:为什么与运算等价于求余运算?

    首先明确,与运算不等价求余运算m % n = m & (n-1) 当且仅当 n 是 2 的次幂。
    例如:
    131101% 81000= (81000 + 50101) % 8 = 5(保留了 8 的后三位)
    151111% 40100= (121100 + 30011) % 4 = 3(保留了 15 的后两位)
    上面观察出,保留的位数正好是求模数1后面0的个数。从位运算看,就是和这么多个1做按位与。
    即:131101% 81000= 131101 & 70111;151111% 40100= 151111 & 30011
    图解

如何存储 - 第二步:找坑

public void put(Key key, Value value) {
    Node head = table[index];
    // 如果当前位置还没有存储,则直接创建节点
    if (head == null) {
        table[index] = new Node<>(hashCode, key, value);
    }
    // 红黑树则插入
    else if (head instanceof TreeNode) {
        ((TreeNode<Key, Value>) head).putTreeVal(new Node<>(hash, key, value));
    }
    // 否则要遍历当前链表,为其找到合适位置
    else {
        int nodeCount = 0;
        Node tail = null;
        while (head != null) {
            nodeCount++;
            // 如果 key 已经存在,则直接替换 value,直接 return,不会 size++
            if (head.hash == hashCode && head.key.equals(key)) {
                head.value = value;
                return;
            }
            // 找到尾结点
            if (head.next == null) {
                tail = head;
            }
            head = head.next;
        }
        // 循环结束,表示遍历到尾,未找到 key 存在,则新建一个节点
        tail.next = new Node<>(hashCode, key, value);
        // 链表长度超过阈值,转为红黑树
        if (nodeCount > TREEIFY_THRESHOLD) {
            treeifyBin(index);
        }
    }
    // 最后记录键值对个数增加
    size++;
}

/**
 * 将 index 所在的链表转换为红黑树
 */
private void treeifyBin(int index) { }

扩容

private void resize() {
    if (size <= threshold) {
        return;
    }
    Node<Key, Value>[] oldTable = table;
    int oldCap = oldTable.length;
    // 计算新数组长度,并初始化新数组
    int newCap = oldCap << 1;
    table = new Node[newCap];
    threshold = (int) (newCap * loadFactor);
    // 遍历每个桶
    for (int i = 0; i < oldCap; i++) {
        Node<Key, Value> head = oldTable[i];
        if (head == null) {
            continue;
        }
        // 红黑树
        if (head instanceof TreeNode) {
            reassignTreeNode(head);
        }
        // 遍历链表,重新分配
        else {
            reassignNodeChain(head, i, oldCap);
        }
    }
}

/**
 * 将链表上的节点重新分配
 */
private void reassignNodeChain(Node<Key, Value> head, int currentIndex, int oldCap) {
    // 将一条链表拆成两条链表
    Node<Key, Value> lowHead = null, lowTail = null;
    Node<Key, Value> highHead = null, highTail = null;
    do {
        // 低位链,后面解释为什么这样判断
        if ((head.hash & oldCap) == 0) {
            if (lowHead == null) {
                lowHead = head;
            }
            if (lowTail != null) {
                lowTail.next = head;
            }
            lowTail = head;
        }
        // 高位链
        else {
            if (highHead == null) {
                highHead = head;
            }
            if (highTail != null) {
                highTail.next = head;
            }
            highTail = head;
        }
    } while ((head = head.next) != null);
    // 将两条链表放入新 table,后面解释为什么这样放
    table[currentIndex] = lowHead;
    table[currentIndex + oldCap] = highHead;
}

/**
 * 将红黑树上的节点重新分配
 */
private void reassignTreeNode(Node<Key, Value> head) { }
  • 问题一:为什么一条链表上的节点一定会分为两条链?

    设扰动后15,新数组长度8,原索引3
    根据上面解释的与运算原理,则新索引只会等于00110111。所以一定会分布在两条确定的链上。
  • 问题二:新两条链在 table 中位置如何确定?

    由问题一观察,0011 = 0011 | 00000111 = 0011 | 0100,即新索引等于原索引+0+原数组长度
    图解

  • 感谢

博客中使用了如下两篇博客中的图片,谢谢。同时这两篇博客也是非常好的学习HashMap的博客。
感谢 @前利 老师
感谢 @Carson_Ho 老师

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357

推荐阅读更多精彩内容

  • Java集合:HashMap源码剖析 一、HashMap概述 二、HashMap的数据结构 三、HashMap源码...
    记住时光阅读 731评论 2 1
  • 1. HashMap工作原理 HashMap作为优秀的Java集合框架中的一个重要的成员,在很多编程场景下为我们所...
    五道杠小学生阅读 8,262评论 1 2
  • 一、HashMap概述 HashMap基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用nul...
    小陈阿飞阅读 639评论 0 2
  • HashMap底层原理解析 1.基本、常用性质HashMap储存的是键值对HashMap 允许 null 键和 n...
    潇萧之炎阅读 633评论 0 1
  • leader选举 目标 :确保所以log的entry 传送方向是: leader -> follower策略:选出...
    娄云阅读 339评论 0 0