zoukankan      html  css  js  c++  java
  • Java容器之HashMap源码分析0

    HashMap继承自AbstractMap,AbstractMap实现了Map接口。 

    1、HashMap的组成结构

    HashMap采用键值对存储,一对键值称为entry,而HashMap也就是entry的集合。通过源码中HashMap定义的变量,我们可以看出其组成结构。
    要理解HashMap的基本组成结构,我们首先需要知道几条概念: 
    • HashMap由线性表与链表/红黑树组成
    • HashMap的size是指所有entry/node的数量
    • HashMap的容量是指桶的数量,桶也就是线性表中存储的每一个节点 
    //HashMap.java
    /*静态变量*/
    // 初始容量 16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 最大容量 2^30
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认填充因子 0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    // 链表转换成红黑树,链表长度阈值(桶中节点数量)
    static final int TREEIFY_THRESHOLD = 8;
    // 树转换成链表的阈值,树的size阈值(桶中节点数量)
    static final int UNTREEIFY_THRESHOLD = 6;
    // 转换成树的最小容量阈值(table的长度)
    // table过短则进行扩容操作,不允许进行树化
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    /*实例变量*/
    // 存储桶的数组
    transient Node<K,V>[] table;
    // 存储entry的set
    transient Set<Map.Entry<K,V>> entrySet;
    // map的size,即线性表与链表、红黑树中节点的总数量
    transient int size;
    // 修改次数
    transient int modCount;
    // 扩容阈值 capacity * loadFactor,当Map中存放的元素个数大于threshold则进行resize
    int threshold;
    // 装载因子 size / capacity
    final float loadFactor;

    由源码中对变量的定义我们可以窥见,HashMap中的元素首先是存储在名为table的数组中,发生冲突时,再由table中的节点横向扩展为链表或者红黑树。也就是说table存储的基本元素是桶。 
    // 在HashMap中,table为节点线性表,即存放桶的数组
    transient Node<K,V>[] table;

    其次,我们看到源码中定义了装载因子**load factor**。这个变量的设计是用于指定元素填充的比例阈值,默认为0.75,也就是说当总节点数量超过容量的75%,则进行resize操作,也就是进行扩容。 
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    final float loadFactor;

    另外,我们还可以注意到三个与树有关的常量。这是在Java 8中设计的一种优化的数据结构,在Java 8以前,HashMap的底层存储结构为线性表+链表,索引的算法复杂度是O(n),在Java 8中为了提高HashMap的索引效率,当链表达到一定长度的时候,采用红黑树来替代链表,这样索引的算法复杂度可以降低到O(logn)。那么下面的几个常量就定义了链表与红黑树结构之间切换的阈值条件。 
    // 链表转换成红黑树的阈值。当桶中的节点数大于该值,则链表树化
    static final int TREEIFY_THRESHOLD = 8;
    // 树转换成链表的阈值,当桶中节点数量小于该值,则进行去树化
    static final int UNTREEIFY_THRESHOLD = 6;
    // 当table中桶的数量大于该值,才允许进行树化
    // 这是为了减少数据冲突,table中桶过少,应该进行纵向扩容,而不是横向树化。
    static final int MIN_TREEIFY_CAPACITY = 64;

    由这些源码对实例变量以及静态变量的定义我们可以窥见HashMap的大致存储结构是“线性表+链表/红黑树”的形式。图中线性表(黄色的数据结构即table) 

    2、构造器 

    HashMap提供了四种构造器如下: 
    /*
    * 指定容量和装载因子的构造器
    * 1. 参数检查
    * 2. 参数赋值,装载参数,threshold
    */
    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;
        // 注意,此处虽然将初始容量计算后的数值赋给了threshold,但是这里仅将容量暂时存放在threshold,在resize的时候会将值拿出来赋给capacity
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    /*
    * 指定容量的构造器
    *   使用默认的装载因子
    */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    /*
    * 无参构造器
    *   使用默认的装载因子,默认的threshold为0
    */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    
    /*
    * 指定Map为参数的构造器
    *   设置默认装载因子,调用putMapEntries装填map
    */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    此处我们注意到构造器中指定初始容量的时候,调用了名为tableSizeFor方法,并将方法返回结果用于设置threshold。而在Map的变量定义中,也未曾发现有初始容量这一变量。我们先看tableSizeFor方法的作用。 
    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

    根据注释,tableSizeFor方法的作用是计算并返回离目标容量数字最近的2的幂。从这里我们可以猜测,HashMap的存储长度都是2的幂,如果初始容量不是2的幂,则会通过tableSizeFor找到最近的2的幂。如输入15,则输出16;输入17,则输出32。单单看这个算法,它的作用是寻找大于等于输入数字的2的幂。做法是通过移位将n的有效位全部置1,然后进行加1进位。 
     
    0000 0000 0000 0000 0000 0000 0100 1001 73
    0000 0000 0000 0000 0000 0000 0110 1101 移1位并按位或
    0000 0000 0000 0000 0000 0000 0111 1111 移2位并按位或
    ...
    0000 0000 0000 0000 0000 0000 1000 0000 加1 => 128
    经过几次移位之后,100 1001 -> 111 1111 -> 1000 0000

    由此我们可以总结出,HashMap在进行构造的时候只是初始化了**thresold**和**loadFactor**两个参数,其中threshold用于定义resize的阈值,loadFactor为装载因子。
    • threshold = capacity * loadFactor
    • 当HashMap的size,也就是所有节点个数大于threshold时,触发扩容。 

    3、HashMap的基本操作 

    先抛开HashMap的底层实现原理,我们看一下在表层HashMap提供的一些基本操作。 

    3.1、添加元素

    首先是添加元素,在HashMap中提供的关于添加元素的操作如下: 
    /*
    * 添加单个元素
    *  传入添加元素的键和值,调用了putVal方法
    */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    /*
    * 批量添加元素
    * 1. 容量检查
    * 2. 若table为null,则初始化threshold
    * 3. 若超出threshold,resize
    * 4. 循环遍历需要添加的元素,依次调用putVal方法
    */
    
    public void putAll(Map<? extends K, ? extends V> m){
        putMapEntries(m, true);
    }
    
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                // 创建HashMap时调用
                float ft = ((float)s / loadFactor) + 1.0F;// 计算所需的最大容量,即刚好装载量达即将达到loadFactor,此处为float型
                int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // fload转整型,同时判断是否超过最大容量
                if (t > threshold)
                    // 初始化threshold
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                // 批量添加元素,添加个数超过阈值,扩容
                resize();
            // 循环添加节点
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

    上面的两个方法都调用了putVal方法为map添加元素,下面我们就着重看一下putVal方法。 
    /*
    * putVal方法添加元素
    */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab;  // tab表示存储的数组
        Node<K,V> p;  // 节点
        int n, i; // n记录数组长度
        
        if ((tab = table) == null || (n = tab.length) == 0)
            // 若table未初始化或者长度为0,则调用resize方法
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 若待插入节点处没有元素,则创建新节点插入,完成
            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
                e = p;
            else if (p instanceof TreeNode)
                // 若p为树节点,则调用方法对树进行插入,并记录节点到e
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 处理链表插入
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        // 寻找链表尾部进行插入,并记录节点到e
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 若链表长度够长,进行树化操作
                            treeifyBin(tab, hash);
                        break;
                    }
                    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;
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            // 若size超过threshold,调用resize()
            resize();
        afterNodeInsertion(evict);
        // 插入新节点,返回null
        return null;
    }

    通过上述的插入元素的源码分析,我们大致知道了插入的流程。
    • 首先检查table是否初始化,若没有,则通过调用resize进行初始化。
    • 先在table中寻找哈希值对应的键中是否已有节点插入,若没有则创建新节点插入,程序结束。
    • 出现哈希冲突。首先判断冲突的节点哈希值与键是否相同,若相同,则进行值替换,或者不替换(由onlyIfAbsent参数决定)。
    • 若table中冲突节点的键值并不相同,则判断冲突处是否为树节点,若为树节点则进行树的插入/替换节点操作。
    • 若冲突处不是树节点而是链表结构,则进行链表的插入/替换节点操作。
    • 若新节点为插入,则返回null。
    • 若新节点为替换,则返回旧值。 

    程序流程如下图

    3.2、读取元素 

    下面介绍读取map的元素方法。接口中通过get方法来获取元素,在源码中相关的方法如下: 
    /*
    * 通过key获取value
    *  调用getNode方法,传入hash和key,返回null或者读出的value
    */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    /*
    * 设置默认值的读
    *   调用getNode方法,传入hash和key,返回null或者读出的value
    */
    @Override
    public V getOrDefault(Object key, V defaultValue) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
    }
    
    /*
    * 根据hash值和key值查找节点
    */
    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 && ((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;
    }

    3.3、修改元素 

    修改元素操作可以由添加元素来完成,如put操作用新值替换旧值。HashMap还提供了replace方法用于替换旧值。 
    /*
    * 替换指定key,oldValue为新value
    *   通过getNode方法查找指定元素,替换为新的value
    */
    @Override
    public boolean replace(K key, V oldValue, V newValue) {
        Node<K,V> e; V v;
        if ((e = getNode(hash(key), key)) != null && ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
            e.value = newValue;
            afterNodeAccess(e);
            return true;
        }
        return false;
    }
    
    /*
    * 替换指定key的元素值
    *   通过getNode方法查找指定的元素,然后覆盖value
    */
    @Override
    public V replace(K key, V value) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) != null) {
            V oldValue = e.value;
            e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
        return null;
    }

    3.4、删除元素 

    HashMap提供的删除元素的方法是remove,与之相关的代码: 
    /*
    * 按key删除元素
    *   调用removeNode方法,并根据其返回值确定remove方法的返回值
    */
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
    }
    
    /*
    * 按key和value删除元素
    *   调用removeNode方法
    */
    public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }
    
    /*
    * removeNode方法,删除哈希表中的节点
    */
    final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
        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<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)
                    // p为树节点,则从树中寻找目标节点
                    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);
                }
            }
            
            // 对标注的节点进行判断
            // 节点不为null, 且进行值匹配
            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;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

    由上述代码我们可以看出,删除节点的操作首先是寻找目标节点,有三种情况:
    • 目标节点在线性表中,直接使线性表中相应位置节点赋值为其链表或者树的子节点
    • 目标节点在链表中,修改节点的后继
    • 目标节点在红黑树中,对红黑树进行节点删除操作 

    3.5、其他操作 

    作为集合,HashMap还提供了如contains,isEmpty等方法 
    /*
    * 判断是否存在key
    *   通过getNode函数查找节点是否存在
    */
    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }
    
    /*
    * 判断是否存在value
    *   嵌套循环遍历HashMap中的所有节点,判断节点的value是否符合要求
    */
    public boolean containsValue(Object value) {
        Node<K,V>[] tab; 
        V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }
    
    /*
    *  判断是否为空
    */
    public boolean isEmpty() {
        return size == 0;
    }

    4、其他 

    这篇笔记里我们根据源码首先简要分析了HashMap的本质组成结构,以及其底层的存储形式。然后分析了一下HashMap源码提供的一些基本操作,如添加,读取,修改,删除等操作,从这个过程中我们能够加深对HashMap的存储结构的理解,并能学习到HashMap对元素的插入和遍历的底层实现。
    HashMap的设计还有很多值得分析的点,如
    • hash函数的设计
    • 它的存储结构(链表、红黑树)实现
    • 存储结构之间的转换过程
    • 扩容过程
    等等 
  • 相关阅读:
    hdu 3085
    hdu 3295 模拟过程。数据很水
    hdu 2181 水搜索
    pku ppt some problem
    2-sat
    The 2014 ACM-ICPC Asia Mudanjiang Regional First Round
    nenu contest3
    The 10th Zhejiang Provincial Collegiate Programming Contest
    最小费用最大流
    多源最短路
  • 原文地址:https://www.cnblogs.com/zhengshuangxi/p/11053019.html
Copyright © 2011-2022 走看看