美文网首页Java 杂谈
java 集合 3 - HashMap

java 集合 3 - HashMap

作者: DJN_ | 来源:发表于2018-02-03 00:32 被阅读0次

参考文章:什么是HashMap?图解集合4:HashMap

接下来通过 jdk 1.8 进行分析

HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。

特点:

  1. HashMap的key和value都允许null值,当key相同时, 值会覆盖,即key是唯一的。
  2. HashMap是无序的,存值时是根据key的hashcode进行映射,因而存入顺序在数组中的实际顺序会被打乱。
  3. 线程不安全

构造器

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 只选取了两个较常用的构造器
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

loadFactor指加载因子,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 resize方法将容量翻倍。

可见默认的加载因子为0.75,即HashMap中元素个数超出总大小的75%时,容量将扩大。

数组实体


    /**
     * 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;

注释:数组实体,第一次使用时初始化,必要时进行扩容,当分配容量时,数组大小始终为2的幂。

数组元素数据结构

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

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    ....
    }

可见每一个数组元素都为一个链表结构。

put方法

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

put方法内部调用了putVal方法,在putVal方法中执行具体的插入逻辑。
其中hash方法对key做了如下处理:
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
该操作称之为“扰动函数”,旨在使键的hash值固定在一定范围,减少hash冲突。

假设有key的hashCode为3029737,其对应的32为二进制数据为:
0000 0000 0010 1110 0011 1010 1110 1001,通过“扰动函数”,其最终hash为:
0000 0000 0000 0000 0000 0000 0000 1010
参考文章:JDK 源码中 HashMap 的 hash 方法原理是什么

这里写图片描述

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

先看最上面的if判断,该判断用于判断数组实体是否为null或者数组长度为0,两种情况下都需要对数组进行扩容,扩容后给局部变量tab和n从新赋值。
if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;

注意tab[i=(n-1)&hash]),这里n为数组长度,(n-1)&hash计算出指定key对应元素在数组实体中的下标。之后取得该位置元素并赋值给局部变量p。

如果p为null,即当前下标对应元素(链表)还没有元素,直接将新元素放当前位置即可,此时没有hash冲突。

如果p不为null,意味着发生了hash冲突,需要进一步检查。

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))判断:如果链表头元素的hash与新值hash相等并且头元素的键与新键相同(== 用于比较内存地址);不同的key由hash方法得到的hash值可能相等,但两个key并不相同,即除了hash值要相同外,还需用==比较两个key的内存地址,确认两个key其实指向堆上的同一个实例。这个时候才确认为重复key,才能用新值替换旧值。另一种情况是新键不为null并且两个key的equals返回true,也确认为重复key。

else if (p instanceof TreeNode)如果结点的链表结构为红黑树(下一个else分支控制了在何种情况下会转化为红黑树),则通过putTreeVal方法查找是否为重复key。

else,排除了上面的两种情况(新键与链表头结点重复,链表为红黑树),即表示新键可能在头结点之后的某一个结点重复;遍历链表结点,如果下一个结点为null,那表示已经遍历到链表尾,也就表示没有找到重复的键,直接创建新结点,插入链表尾即可,接下来还需判断如果遍历结点个数大于TREEIFY_THRESHOLD(其值为8),则将链表转为红黑树(通过treeifybBin方法),这里隐含当前else分支只有在链表个数小于8时才会执行(如果结点为红黑树就在上一个判断分支截断了),之后结束遍历;另一种情况如果当前链表结点不是尾结点,则判断key是否重复,重复就结束遍历。

在上面的“找重复key”结束后,判断e值,不为null表示找到重复key,如果onlyIfAbsent参数为false或重复key对应的值为null,就替换重复key的值,并返回被替换的值,整个方法结束。

元素插入的逻辑到这里就结束了。

最后判断数组容量,如果实际元素个数大于容量 * 加载因子,就扩容。

其中afterNodeAccessafterNodeInsertion为回调方法,子类可实现这些方法以接收动作回调。

get方法

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

get方法调用的是getNode方法,找到Node的或就获取其value返回,否则为null,即找不到值。

getNode方法

    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 && // 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;
    }

查看get方法知道参数hash通过hash(Object key)方法计算。

if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null)
这个判断语句排除了数组实体为null(未put就get),数组长度为0以及hash值对应下标元素为null的情况。

之后若进入第一个if分支,表示上述条件满足。之后优先检查链表头元素 ,是目标元素直接返回,查找结束,否则遍历查找(分普通链表和红黑树链表两种查找情况)。

线程安全性(高并发死循环导致死锁)

参考文章:Java 8系列之重新认识HashMap

HashMap是线程不安全的,如果需要线程安全,应使用ConcurrentHashMap。

在 jdk 1.7 的环境下,map初始化为一个长度为2的数组,loadFactor=0.75,threshold=20.75=1,也就是说当put第二个key的时候,map就需要进行resize,在并发的情况下极有可能将链表错误构造为双端链表*,此时通过get方法查找一个不存在的key,get方法中通过do{..}while((e = e.next) != null)遍历查找key,双端队列导致e != null始终为true,死循环就出现了。

相关文章

  • Java集合系列-HashMap 1.8(一)

    原创文章,转载请标注出处:《Java集合系列-HashMap 1.8(一)》、《Java集合系列-HashMap ...

  • 收藏夹

    博文 Java 集合:Java 集合学习指南 Java 集合:Java 集合源码剖析 HashMap:HashMa...

  • java基础之集合略解

    Java集合:整体结构 HashMap剖析 Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例...

  • Java集合框架源码解读(3)——LinkedHashMap

    Java集合框架源码解读(3)——LinkedHashMap 概述 LinkedHashMap是HashMap的子...

  • 计划

    1、java集合类:HashMap ConcurrentHashMap; HashMap:https://ww...

  • java 集合 3 - HashMap

    参考文章:什么是HashMap?、图解集合4:HashMap 接下来通过 jdk 1.8 进行分析 HashMap...

  • HashMap剖析

    Java集合:HashMap源码剖析 一、HashMap概述 二、HashMap的数据结构 三、HashMap源码...

  • 面试的问题

    java集合框架: 1:介绍一下java的集合框架 2:HashMap遇见哈希冲突会如何怎么办?HashMap是线...

  • Java集合:HashMap源码剖析

    非常推荐Java集合:HashMap源码剖析 1.HashMap概述     HashMap基于哈希表的 Map ...

  • Java并发包之ConcurrentHashMap

    之前整理了一份Java中常用的集合类的基本特性:Java常用集合类图解详细介绍了HashMap:HashMap之浅...

网友评论

    本文标题:java 集合 3 - HashMap

    本文链接:https://www.haomeiwen.com/subject/kdmyzxtx.html