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

来源:互联网 发布:linux swap分区 编辑:程序博客网 时间:2024/05/17 04:53

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 0
原创粉丝点击