Java集合学习之HashMap 一(JDK1.8)

Java集合学习之HashMap 一(JDK1.8)

  • 既然走上了研发这条路, 首选的语言又是Java, 吃饭的家伙就需要了解的透彻一些
  • 要想深入使用Java, 集合一定是绕不过的坎儿, 从初级到高级甚至资深的水平, 反正是都要过一遍JDK中集合的源代码
  • 下载JDK源码的时候已经是Java1.8了, 所以和网上的博客们和教程们可能会有一点冲突

HashMap概述

  • Map接口基于散列表的实现, 继承自抽象类AbstractMap
  • 插入和查询Key-Value对的开销是固定的
  • 可以通过构造器设置容量负载因子, 以调整容器的性能
  • HashMap不保证映射的顺序, 特别是它不保证该顺序恒久不变
  • HashMap**非线程安全**, 如果多个线程同时访问一个 HashMap, 如果其中有一个线程需要从结构上(指添加或者删除一个或多个映射关系的任何操作)做了修改, 则需要通过外部来保持同步

HashMap的数据结构

HashMap的底层数据结构是通过数组和链表的结合来实现的, 如图所示:

通常的文章都把数组的每个元素称作一个桶
从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;
    this.threshold = tableSizeFor(initialCapacity);
}

和之前版本的JDK不同, 在初始化HashMap的时候并不会创建存放键值对的数组,该数组的创建在第一次使用的时候在resize()方法中执行

HashMap维护几个重要的数据域:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量, 2的30次方
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认的负载因子
static final int TREEIFY_THRESHOLD = 8; // 每个桶的阈值, 即每个桶装多少为上?
static final int UNTREEIFY_THRESHOLD = 6;

transient Node<K,V>[] table;

JDK1.8存储键值对的对象不再是Entry<K,V>, 而是改用Node<K,V>,不过基本实现没有变化:

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

暂时猜测与新增的TreeNode有关

Node是一个静态类,是存放数据元素的对象,除了包含key和value的值外,还包含了一个指向另一个Node的next指针,从而构建了拥有相同hash值的键值对的链表

HashMap的核心方法解读

存储键值对

暴露出来的方法接口依然是put(),但是真正的实现是putVal()方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;    // 初次使用该HashMap的对象时, 给table分配空间
    if ((p = tab[i = (n - 1) & hash]) == null)  // key值经过hash后得到其在table中桶的下标索引
        /* table中在该下标索引的桶中没有元素, 则直接把该Node元素放置到该桶中 */   
        tab[i] = newNode(hash, key, value, null);   
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;  // 桶中的第一个元素的key值和要存储的元素的key值相同
        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) {
                    /* 遍历到链表的尾部, 没有找到相同的key, 则新增一个Node */
                    p.next = newNode(hash, key, value, null);  
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                /* 桶中的某个元素的key值和要存储的元素的key值相同 */
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }

        /* 此时的e是某个Node<K, V>,可能是原先就存在桶中的, 也可能是新增的 */
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;  // 更新value
            afterNodeAccess(e);
            return oldValue;  // 返回"旧"的value
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

总结一下put操作的步骤:

  1. 第一次执行put操作时,会给HashMap底层的数据结构table分配空间
  2. 对key值进行hash后获得桶的下标索引
  3. 如果在该下标索引的桶中没有存放数据, 则直接把该数据放置在这个桶中
  4. 如果该下标索引的桶中有数据, 则遍历该桶的链表
  5. 如果找到相同的key值, 则更新该Node的value值, 并返回旧的value值; 如果key值不存在, 则new 一个新的Node, 返回null
  6. 当这个HashMap的size大于阈值的时候, 执行resize()方法重新给table分配空间

简单讨论一下关于key值的hash函数

  • 当系统决定存储HashMap中的键值对时, 完全没有考虑Node中的value, 仅仅只是根据key来计算并决定每个Node属于哪个桶
  • 我们完全可以把Map集合中的value当成key的附属品, 当系统决定了key的存储位置之后, value 随之保存在那里即可

对与key的hash计算并决定其存放的桶, 共有两步:

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

/* 桶的下标索引 */
i = (n - 1) & hash   // n = table.length
  • 桶的下标索引算法实际上是一种技巧性的取模运算, 因为HashMap的底层数组的容量总是2的n次方
  • 当length总是2的n次方时, h&(length-1)运算等价于对length取模, 也就是h%length, 但是&比%具有更高的效率

读取键值对

暴露出来的方法接口依然是get(),但是真正的实现是getNode()方法:

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) {      // 判断是否能通过key值的hash找到相应的链表
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))     // 根据key值确定链表的第一个元素就是满足的元素
            return first;

        /* 遍历该链, 寻找是否有该key值的value */
        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. 根据key值的hash值判断相应下标索引的桶是否存放数据元素, 没有则返回null
  2. 根据key值判断该桶的第一个数据元素是不是就是要找的数据, 找到则返回该Node
  3. 在上一步没有找到想要的值, 则遍历该桶的链表, 根据key值来判断是否存在正在寻找的数据元素, 找到则返回该Node
  4. 没有找到相应的key, 返回null

resize()方法

和之前版本的resize()方法相比, JDK1.8中还承担了初始化table时分配空间的任务, 初始的table的容量大小是16(DEFAULT_INITIAL_CAPCITY), 初始的阈值是12(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPCITY)

resize由两个步骤组成:

  1. 扩容, 以成倍的方式(乘以2)
  2. 重新hash, 保持各个桶原有的链表的元素的顺序不变
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值