【Java源码解析】-- HashMap源码解析

目录

  • 源码解析

    • 1.构造方法

      • 无参构造方法
      • int型参数的构造方法
      • int,float两个参数的构造方法
      • hsah方法
    • 2.添加元素(put()方法)
    • 3.扩容方法(resize()方法)
    • 4.获取元素(get()方法)
    • 5.移除元素(remove())
    • 6.树化(treeifyBin())
    • 关于HashMap常见的问题
      • 1.为什么容量始终是2的幂次?
      • 3.既然红黑树那么好,为啥hashmap不直接采用红黑树,而是当大于等于8个的时候才转换红黑树?
      • 4.JDK1.7 扩容死锁产生原因
      • 5.JDK1.8 为什么不会形成环,如果做到无需rehash?
      • 6.modCount的作用
      • 7.为什么用红黑树而不是其他数结构
      • 8.HashMap 和 HashTable 的区别

讲HashMap就不得不说到hash算法

散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在記憶體儲存位置的数据结 构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

源码解析

1.构造方法

无参构造方法

static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

loadFactor 是其负载因子,DEFAULT_LOAD_FACTOR默认值0.75f。该值用来判断当hashmap中以使用空间达到该占比是进行扩容。如默认空间大小为16.16*0.75=12.所以当hashmap内的数组有12个空间被使用时就开始扩容resize();

int型参数的构造方法

public HashMap(int initialCapacity) {
    //内部调用了传两个参数的构造方法。默认负载因子0.75f
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

int,float两个参数的构造方法

static final int MAXIMUM_CAPACITY = 1 << 30;//2^30
public HashMap(int initialCapacity, float loadFactor) {
    //判断传入的容量是否小于0
    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);
}
//该方法计算出的值为:2^次方>=initialCapacity
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;
}

参数的构造方法

public HashMap(Map<? extends K, ? extends V> m) {
    //设置默认的负载因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //传入集合的大小
    int s = m.size();
    //判断集合中是否有元素
    if (s > 0) {
        //判断transient Node<K,V>[] table;是否是null
        if (table == null) { // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;
            //设置其大小
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                //t大于下一次要扩容的大小时,改变其值
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
            //对象中table不为null且传入集合大小大于下一次扩容大小时,进行扩容
            resize();
         //把传入集合的内容存入当前集合
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            //调用了hash函数
            putVal(hash(key), key, value, false, evict);
        }
    }
}

hsah方法

static final int hash(Object key) {
    int h;
    //对象的hashcode值^(异或)其hashcode的高16位的值
    //目的:提高hashcode的随机性,减少hash冲突
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

扰动函数

2.添加元素(put()方法)

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
//返回值为当前key的上一个vlaue,如果没有则为null
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断table数组为0或null时,
    if ((tab = table) == null || (n = tab.length) == 0)
        //进行初始化
        n = (tab = resize()).length;
    //判断key是否已经存在于数组中,即是否发生hash冲突
    //(n - 1) & hash该计算对应位置的方法是
    //n为数组长度,为2的次方如16=00010000
    //n-1为00001111
    //而无论hash值为多少。如11110010
    //(n - 1) & hash计算出的结果都小于n
    //所以这便是数组长度为什么始终是2的次幂,保证了不会越界
    if ((p = tab[i = (n - 1) & hash]) == null)
        //不冲突,节点不存在,创建新的节点
        tab[i] = newNode(hash, key, value, null);
    else {
        //冲突,节点有元素
        Node<K,V> e; K k;
        //如果要存入元素key与冲突位置的key相同,把e指向当前元素
        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);
                    //链表长度大于等于7则对链表进行树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //链表中存在指定元素,则break
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //key存在映射则把旧值用新值替换并返回旧值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
             // 在节点被访问后,做点什么事,hashMap中该方法并没有被实现
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果键值对个数大于阈值,则扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

该添加元素的过程是:先判断是否存在hash冲突,不存在则直接把元素存入hashMap。hash冲突时,再判断冲突位置的key是否与要存入的key相同,相同就替换旧值。不相同新建一个节点存入红黑树中或挂在链表尾部。

3.扩容方法(resize()方法)

源码:

/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
final Node<K,V>[] resize() {
    //旧的数组
    Node<K,V>[] oldTab = table;
    //旧数组长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //旧扩容阈值
    int oldThr = threshold;
    int newCap, newThr = 0;
    //判断旧数组是否为空。是不是第一次创建
    if (oldCap > 0) {
        //判断旧数组是否达到容量的最大值,如果已经达到就不在扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //如果旧数组长度的2倍小于最大容量,且旧容量大于默认初始容量就把容量与阈值同时扩大2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //使用非默认方法创建map时,第一次插入会走这里
    //如果旧容量为0且旧阈值大于0,则把新容量赋值为旧阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    //使用默认方法创建map时,第一次插入会走这里。来初始化hashmap的内置数组
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果新阈值为0,则计算为容量*装载因子,但不能超过最大容量
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //把新的阈值赋值给类的内置阈值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //创建新的数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //把新数组赋值给table
    table = newTab;
    //对已存在数组扩容时,从旧数组向新数组转移元素
    if (oldTab != null) {
        //遍历旧数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //判断j位置是否为空,为空不处理
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //判断该节点是否有后继节点,无后继节点则直接把旧数组j位置的数放在新数组对应位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //节点有后继节点
                //判断节点是否为一个数节点,如果是则将树分化为两个树,放入到链表中
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //如果是一个链表对链表进行拆分,然后放入数组中
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        //使用尾插法进行插入
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

扩容方法被调用有两种情况,一是进行hashmap中的数组初始化时,二是真正进行扩容时。初始化没什么好讲的。不过扩容时把数组长度,和阈值扩展为原来的二倍。创建新数组后把旧数组中的元素往旧数组中移动。单节点直接移动到新数组对应位置。如果是一个红黑树则把树进行拆分。如果是链表则也对链表进行拆分。

4.获取元素(get()方法)

源码:

public V get(Object key) {
    Node<K,V> e;
    //判断对应key的value是否存在,
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
 * Implements Map.get and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //在数组存在且key对应的位置的数组有值时进行下一步动作
    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;
}

5.移除元素(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) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    //判断table数组存在且key对应位置存在节点
    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))))
            //如果对应位置的key和要移除的相同,则直接把p赋值node
            node = p;
        else if ((e = p.next) != null) {
            //如果p是树节点,在树中查找key
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
            //在链表中查找key
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != 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为该节点的前一个节点,所以p.next = //node.next;来删除节点
                p.next = node.next;
            ++modCount;
            --size;
            //留给LinkedList使用
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

6.树化(treeifyBin())

源码:

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //判断table数组不存在,或者数组长度没有达到最小树化长度时进行扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    //对应位置链表存在,开始扩容
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            //将普通节点转化为树节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            //把单向链表转化为单项链表
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            //把双向链表进行树化
            hd.treeify(tab);
    }
}

关于HashMap常见的问题

1.为什么容量始终是2的幂次?

  • 因为计算key所在的桶是 (hash & n-1),如果n是2的幂次,则保证了key所在桶的范围是0 <= index <= n-1。
  • 在进行扩容时,原链表会分化为两条链表,高位的位置时 旧容量+从前的位置

    2.加载因子为什么是0.75?

    假设loadfactory = 1,则键值对个数达到数组容量时,进行扩容,能够极大的利用空间,但是查询慢。假设loadfactory = 0.5,则键值对达到数组容量一半时,进行扩容,查询快,但是利用的空间较少。而0.75则是为了再空间与时间取一个平衡。

3.既然红黑树那么好,为啥hashmap不直接采用红黑树,而是当大于等于8个的时候才转换红黑树?

根据泊松分布的概率学统计,当key所在的桶的链表长度增加,那么新的key到这个桶的概率在不断降低。当链表8长度为时,下一个键值对到这个链表的概率接近于0,所以产生红黑树的概率也不高。

4.JDK1.7 扩容死锁产生原因

因为jdk1.7的扩容方法扩容时

void resize(int newCapacity){
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if(oldCapacity == MAXIMUM_CAPACITY){
        threshold = Integer.MAX_VALUE;
        return ;
    }
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable,initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactory,MAXIMUM_CAPACITY + 1);
}

void transfer(Entry[] newTable,boolean rehash){
    int newCapacity = newTable.length;
    for(Entry<K,V> e:table){
        while (null != e){
            Entry<K,V> next = e.next;
            if(rehash){
                // ...
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
}

在多线程的情况下,可能会造成循环引用

5.JDK1.8 为什么不会形成环,如果做到无需rehash?

通过两组指针,将原链表直接截断分为两组高低位链表,避免了向1.7那样节点间相互翻转,形成环,同时也不需要rehash。

6.modCount的作用

modCount是用来记录HashMap中数组被修改的次数。在迭代器迭代时会比较modCount与迭代器对象自身记录的修改次数。如果modCount发生变化会发生并发修改异常。所以如果在迭代的过程中想要删除元素,使用迭代器自带的删除方法。

7.为什么用红黑树而不是其他数结构

红黑树牺牲了一些查找性能 但其本身并不是完全平衡的二叉树。因此插入删除操作效率略高于AVL树,AVL树用于自平衡的计算牺牲了插入删除性能,但是因为最多只有一层的高度差,查询效率会高一些。

8.HashMap 和 HashTable 的区别

  • 线程安全性:HashMap 是线程不安全的,而HashTable 是线程安全的,大部分方法都是由 synchronized修饰。
  • 效率:HashMap 由于不是线程安全的,所以就单线程环境下,效率由于 HashTable
  • 容量:HashMap 默认初始容量为 16,而 HashTable 模式初始容量为 11。如果指定初始容量的话,HashMap 为第一个大于等于 2 的整数次幂的值,而 HashTable 则为指定值。
  • 扩容:HashMap 每次扩容容量为原来的 2 倍,而HashTable 为 2n + 1
  • 底层数据结构:HashMap 1.7 后,采用数组+链表+红黑树的数据结构,而HashTable并没有采用红黑树的数据结构。

原文地址:https://www.cnblogs.com/wf614/p/12382677.html

时间: 2024-10-06 14:02:26

【Java源码解析】-- HashMap源码解析的相关文章

【源码】HashMap源码剖析

//----------------------------------------------------------------------------------- 转载需注明出处:http://blog.csdn.net/chdjj //----------------------------------------------------------------------------------- 注:以下源码基于jdk1.7.0_11 之前的几篇文章介绍了List集合中一些比较常见

Java集合系列之HashMap源码分析

一.HashMap简介 HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射.此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能. ps:本文中的源码来自jdk1.8.0_45/src. 1.重要参数 HashMap的实例有两个参数影响其性能. 初始容量:哈希表中桶的数量 加载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度 当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的

java集合系列之HashMap源码

HashMap的源码可真不好消化!!! 首先简单介绍一下HashMap集合的特点.HashMap存放键值对,键值对封装在Node(代码如下,比较简单,不再介绍)节点中,Node节点实现了Map.Entry.存放的键值对的键不可重复.jdk1.8后,HashMap底层采用的是数组加链表.红黑树的数据结构,因此实现起来比之前复杂的多. static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K k

java集合框架08——HashMap和源码分析

本文为博主原创文章,转载请注明出处:http://blog.csdn.net/eson_15/article/details/51154989 上一章总体分析了Map架构,并简单分析了一下AbstractMap源码,这一章开始我们将对Map的具体实现类进行详细的学习.本章先研究HashMap.依然遵循以下步骤:先对HashMap有个整体的认识,然后学习它的源码,深入剖析HashMap. 1.HashMap简介 首先看一下HashMap的继承关系 java.lang.Object ? java.u

jdk1.7源码之-hashMap源码解析

背景: 笔者最近这几天在思考,为什么要学习设计模式,学些设计模式无非是提高自己的开发技能,但是通过这一段时间来看,其实我也学习了一些设计模式,但是都是一些demo,没有具体的例子,学习起来不深刻,所以我感觉我可能要换一条路走,所以我现在想法是看一些源码的东西,一方面是因为自己大部分的源码其实没有看过,另一方面源码中可能会涉及到一些编码风格和设计模式的东西,我也可以学习. 使用jdk版本:1.7.0_80 先从最简单的开始: public static void main(String[] arg

深入理解JAVA集合系列:HashMap源码解读

初认HashMap 基于哈希表(即散列表)的Map接口的实现,此实现提供所有可选的映射操作,并允许使用null值和null键. HashMap继承于AbstractMap,实现了Map.Cloneable.java.io.Serializable接口.且是不同步的,意味着它不是线程安全的. HashMap的数据结构 在java编程语言中,最基本的结构就两种,一个是数组,另一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的.HashMap也不例外,它是一个“链表的数组”的数据结构

【源码】HashMap源码及线程非安全分析

最近工作不是太忙,准备再读读一些源码,想来想去,还是先从JDK的源码读起吧,毕竟很久不去读了,很多东西都生疏了.当然,还是先从炙手可热的HashMap,每次读都会有一些收获.当然,JDK8对HashMap有一次优化. 一.一些参数 我们首先看到的,应该是它的一些基本参数,这对于我们了解HashMap有一定的作用.他们分别是: 参数 说明 capacity 容量,默认为16,最大为2^30 loadFactor 加载因子,默认0.75 threshold resize的阈值,capacity *

HashMap 源码详细解析 (JDK1.8)

概要 HashMap 最早出现在 JDK 1.2 中,底层基于散列算法实现.HashMap 允许 null 键和 null 值,在计算哈键的哈希值时,null 键哈希值为 0.HashMap 并不保证键值对的顺序,这意味着在进行某些操作后,键值对的顺序可能会发生变化.另外,需要注意的是,HashMap 是非线程安全类,在多线程环境下可能会存在问题. HashMap 底层是基于散列算法实现,散列算法分为散列再探测和拉链式.HashMap 则使用了拉链式的散列算法,并在 JDK 1.8 中引入了红黑

【转】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%; } [

Java 集合系列 09 HashMap详细介绍(源码解析)和使用示例

此页面为WP8"Surface Pro 3"应用的发布页面. "Surface Pro 3"是一款收集Surface Pro 3的玩机技巧的WP8程序,更好的帮助Surface用户理解并使用它. 此页面主要记录开发进度.APP发布等情况. -------------------相关进度--------------------- 目前进度:UI相关资源前期准备中,各相关开放平台的AppID申请中... Java 集合系列 09 HashMap详细介绍(源码解析)和使用