HashMap源码剖析(上) 一、HashMap的数据结构 二、HashMap的构造 三、元素的添加

HashMap源码剖析(上)

文章目录

  • HashMap源码剖析(上)
    • 一、HashMap的数据结构
    • 二、HashMap的构造
      • 2.1、HashMap的无参构造
      • 2.2、HashMap的其他几个构造方法
    • 三、元素的添加

对于每一个Java程序员来说,HashMap你一定不陌生,作为经典面试题,从HashMap上可以考察的知识点太多了。于是乎希望总结一份HashMap的源码级剖析,来检验自己对于Java知识体系的掌握程度

一、HashMap的数据结构

大家或多或少都了解过HashMap

在JDK1.7之前HashMap的结构是数组+链表的形式

在JDK1.8之后HashMap的结构就改成了数组+链表+红黑树的形式

tips:如果大家对红黑树不了解的,可以参考我其他有关红黑树博客

二、HashMap的构造

2.1、HashMap的无参构造

Map map = new HashMap();

通过new HashMap()我们可以很容易的获取一个HashMap的对象

之后让我们进入源码看一看HashMap究竟是如何构造的吧

HashMap源码剖析(上) 一、HashMap的数据结构 二、HashMap的构造 三、元素的添加_第1张图片

源码中HashMap一共有四个构造方法,我们最常用的还是无参的构造

//无参HashMap构造源码
    public HashMap() {
     
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

可以看到在构造方法中并没有创建一个数组来存储元素,只是将loadFactor进行赋值。

那么什么是loadFactor呢?

	/**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;

来看源码注释:loadFactor是hash table的加载因子。

大家都知道数组是固定长度的,但是HashMap我们创建时并没有指定其长度,也就意味着理论上HashMap是可以无限增长的(当然看源码之后会发现有一个最大值的限制),这样数组的定长和HashMap得到无限长之间就出现了矛盾。为了解决这个矛盾就需要loadFactor的作用了,他相当于一个阈值,当数组元素数量超过阈值时可以对数组进行扩容(后面会详细说)

那么在构造HashMap时的默认加载因子是多少呢?

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

源码告诉我们是0.75。为什么是0.75呢?其实这个数字可以在Hash碰撞和空间利用率之前寻找一个最好的平衡。

2.2、HashMap的其他几个构造方法

public HashMap(int initialCapacity) {
     
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

该构造方法传入一个初始容量后调用了其他的构造方法,又看到了默认加载因子,我们继续追踪

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);
    }

我们来到了HashMap的另一个构造方法,这个构造的代码还比较多,但是也还比较简单

  • 最初先对指定初始容量值进行数据合法性检验
  • 又看到一个新的常量MAXIMUM_CAPACITY,追入源码
    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

MAXIMUM_CAPACITY规定了HashMap数组的最大长度,采用位运算左移30位

注释告诉我们HashMap的容量必须是2的幂并且容量要小于MAXIMUM_CAPACITY

大家思考思考为什么一定需要是2的幂?答案以后揭晓

  • 回到HashMap的构造方法,接下来对加载因子进行数据合法性检验
  • 之后将加载因子赋值给HashMap的成员变量
  • 之后对容量进行操作并赋值给HashMap的一个成员变量,追入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;
    }

源码将容量转换为2的幂,对n进行一系列的无符号右移和或运算将cap转换为2的幂,构造结束

再看最后一个构造方法

    public HashMap(Map<? extends K, ? extends V> m) {
     
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
  • 传入一个Map,将已有的Map放入新创建的HashMap中,方法就不继续追了,大家可以自己追一下看看,是循环将元素复制到本HashMap中

可以看出除了传入Map的构造方法外其余方法都只是对变量进行了初始化而并没有实际创建数组,实现了懒加载的效果。

三、元素的添加

    public V put(K key, V value) {
     
        return putVal(hash(key), key, value, false, true);
    }
  • 调用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;
        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 = 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);
                        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) {
      // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  • putVal方法是HashMap的一个核心方法之一了
  • 声明了Node[]数组tab;Node结点p;变量n和i
  • 再了解一个成员变量table,HashMap的元素数组
/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
transient Node<K,V>[] table;
  • 回到putVal方法,当table为空也就是当前HashMap对象仍为空的时候调用resize()方法(扩容方法,以后会说)先理解为创建一个Node[]数组

  • if ((p = tab[i = (n - 1) & hash]) == null) 通过hash值和table长度n进行与操作确定元素要放入的位置并判断其位置是否为null,如果为空直接创建新结点将元素放入tab[i]位置

  • 否则也就是之前的位置上已经有元素,则需要进入else,进行进一步操作

    •  if (p.hash == hash &&
                      ((k = p.key) == key || (key != null && key.equals(k))))
      /*
      判断当前插入位置的原有元素p的hash值和待插入元素hash值是否相同,并判断是否为同一个key,如果是同一个key,则e = p,之后判断e!=null,将新的value覆盖掉之前的val,并将旧结点p的value返回
      */
      
    • 如果key不相同或hash与原有元素p不同else if判断原有结点是否为TreeNode类型,如果是则调用红黑树的插入元素方法将新元素插入

    • 否则进入else,也就是普通的链表结点进入else进行处理,就是普通的链表遍历寻找是否有插入位置,并检查链表长度判断是否需要树化

      • 如果找到一个位置可以放入新结点,则创建新结点存在链表的尾部,并判断是否需要树化,如果长度超过TREEIFY_THRESHOLD-1,则进行树化并退出循环,e为null,size++并判断是否需要扩容,返回null
    • 如果在链表中找到一个相同元素则直接break,e不为null,将新value覆盖旧的value并返回旧的value

你可能感兴趣的:(Java源码,链表,数据结构,java,hashmap)