1,hashMap

1,前述

很早之前就想写博客,一直想找个平台,github的自定义博客不赖,有些难度自己也没时间来研究,既然简书很好那就从简书开始吧

还有一直想研究个东西,jdk源码,虽然是java出身,对前景一直有迷惑,不知道该干嘛,想想数据结构和算法是重要的,并发又是很重要的一块,那就从研究ConcurrentHashMap开始吧

public void main(){
    System.out.print("hello word !");
}

2,map接口

public interface Map<K,V> {
    int size();
    boolean isEmpty();
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    V get(Object key);
    V put(K key, V value);
    V remove(Object key);
    void putAll(Map<? extends K, ? extends V> m);
    void clear();
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();
    interface Entry<K,V> {
        K getKey();
        V getValue();
        V setValue(V value);
        boolean equals(Object o);
        int hashCode();
    }
    boolean equals(Object o);
    int hashCode();
}

上面列出的是jdk6中的interface内容,jdk8新增了一些方法,jdk8开始加入了default修饰符,可以在interface中写入具体的实现方法,在实现类中可以选择性决定方法的实现,本文还是以jdk8 中的内容来讲解,重点方法put () resize(),其他方法都是围绕此来展开

3,hashmap

3-1,属性

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
{
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始化大小16
    static final int MAXIMUM_CAPACITY = 1 << 30;  //默认最大容量
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  //默认扩容因子
    transient Entry[] table;//真正的存储结构
    transient int size;//当前已经存储元素的个数
    int threshold; //判断size是否需要扩容的阈值。也是当前的容量,可以在后面的resize方法分析出来
    final float loadFactor;  //扩容因子,扩容后的容量为 loadFactor * threshold
    transient volatile int modCount;  //map 修改的次数,包括put 和 delete
}

transient 修饰符表示序列化时不序列化此部分, HashMap 中的存储数据的是数组,其中没有被使用到的空间被序列化没有意义。所以需要手动使用 writeObject() 方法,只序列化实际存储元素的数组
默认的参数表示如果用户不在构造器中设置则以默认为准

3-2构造

public HashMap(int initialCapacity, float loadFactor) {}
public HashMap(int initialCapacity){}
public HashMap(){}
public HashMap(Map<? extends K, ? extends V> m){}

/*
     列举两个范例
*/
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);
}
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

所有的构造器都只是对属性进行了赋值,但构造器并没有初始化 table ,这一操作在resize 方法中
loadFactor 扩容比例,如果用户不指定则以默认为准
threshold 是扩容的阀值,有时候和当前数组的容量一样,具体细节会在resize方法中分析出来,现在只是简单介绍下,分为几个情况:
如果构造中输入了initialCapacity参数,会在构造中通过tableSizeFor()计算得出,在resize方法执行时,这个值就是数组的容量,所以要保证2的次幂特性;
如果构造器中没有输入initialCapacity参数,会在resize第一次执行扩容时容量初始化为16,threshold 初始化为 12,

其中构造中的计算方法如下

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方法是保证函数返回值是大于等于参数 initialCapacity 最小的2的幂次方的数值,比如initialCapacity是20,返回值32(2的5次幂)

/*
    | 或运算
    |= 等价于 n = n | a
    >>> 无符号位的左移

    int n = cap - 1; 
    当前cap本身就是2的次幂时,如果不减一,最终结果会变成 cap * 2,不符合我们预期
    n |= n >>> 1;
    右移一位,即n的二进制最高位右移1位,再与n本身或操作,最终n的高1-2位都为1
    n |= n >>> 2;
    右移2位,即n的二进制最高1-2位右移2位,与n本身或操作,最终n的高1-4位都是1
    n |= n >>> 4;
    。。。最终n的高位1-8都是1
    n |= n >>> 8;
    。。。最终n的高位1-16都是1
    n |= n >>> 16;
    。。。最终n的高位1-32都是1

    这里可以举个栗子,假设给定的cap的值为20
    二进制表示:0001 0011,最终执行完的结果为 0001 1111,再加1 为 0010 0000 = 32
*/
为什么cap要保持为2的幂次方?

存储数据的数组table的下标是由key的Hash值决定的。在HashMap存储数据的时候,我们期望数据能够均匀分布,以避免哈希冲突。自然而然我们就会想到去用%取余的操作来实现我们这一构想。

这里要了解到一个知识:取余(%)操作中如果除数是2的幂次方则等同于与其除数减一的与(&)操作。(这样做比直接取余操作效率要好)

如果newCap是2的次幂时,下面成立:
 index = e.hash & (newCap - 1) 
 等同于:
 index = e.hash % newCap

3-3 node

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

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

Node<K,V> 类是HashMap中的静态内部类,实现Map.Entry<K,V>接口。 是map中value值的载体,除了key键、value值之外,还有next节点,元素之间可以构成单向链表。

/*
    table 是map的数组,是所有数据的载体,如果key 的hash值有冲突时,就以链表存在,node提供了这种支持
*/
transient Entry[] table;

HashMap存储的数据结构:维护了一个数组,每个数组又维护了一个单向链表。之所以这么设计,考虑到遇到哈希冲突的时候,同index的value值就用单向链表来维护。

3-4 hash方法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/*
     index = e.hash & (newCap - 1) 
     ^ 异或操作,一样则为0
*/

因为hash 的值最终要和 newCap-1 的值进行与操作,而 newCap 是2的次幂,减一后高位全为0,与操作时 hash 的高位就没有参与进来,index 冲突的几率会上升,h >>> 16 是将高位也参与到hash 中来能够降低 index 冲突的几率。(这是参考其他人的说法,个人不知道为何会降低)

3-5 put方法

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

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)
        // tab 为空,调用resize()初始化tab。
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 通过hash和key确定要存储在table中的下标,如果没有被占用,将value封装为Node存储
        tab[i] = newNode(hash, key, value, null);
    else {
        //当前key获取的下标位置已经被占用,可能需要用链表形式存储
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // p为数组的第一个元素,如果key相同,value值应该直接被覆盖,此时e = p为了在后面的方法中对e进行操作
            e = p;
        else if (p instanceof TreeNode)
            // 如果p是红黑树类型,调用putTreeVal方式赋值
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // index 相同的情况下,但key不同,可能继续以链表形式存放或者转为红黑树存放
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    // 如果p的next为空,将新的value值添加至链表后面
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 如果链表长度大于8,链表转化为红黑树,执行插入
                        treeifyBin(tab, hash);
                    break;
                }
                // key相同则跳出循环,key相同时,会继续判断是否将老的值进行替换
                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;
            //根据规则选择是否覆盖value
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); //看源码注释好像是为LinkedHashMap准备的
            return oldValue;
        }
    }
    ++modCount; //map被修改的次数
    if (++size > threshold)
        // size大于加载因子,扩容
        resize();
    afterNodeInsertion(evict);  //看源码注释好像是为LinkedHashMap准备的
    return null;
}

注释写的已经很好了,不需要再解释

3-6 resize方法

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//旧table 大小
    int oldThr = threshold;
    int newCap, newThr = 0;
    //分为2种情况,1是table已经存在,2是table 不存在需要初始化
    //初始化分为两种:1threshold > 0 即初始化为自定义的长度,2反之则初始化为默认大小 (这里和构造中有关)
    if (oldCap > 0) {
        // table已存在
        if (oldCap >= MAXIMUM_CAPACITY) {
            // oldCap大于MAXIMUM_CAPACITY,threshold设置为int的最大值,并且不再继续扩容,新的元素放到链表或树
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //newCap设置为oldCap的2倍并小于MAXIMUM_CAPACITY,且大于默认值, 新的threshold增加为原来的2倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        // threshold>0, 将threshold设置为newCap,所以要用tableSizeFor方法保证threshold是2的幂次方
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 默认初始化,cap为16,threshold为12。除此之外 threshold 等于 数组的容量
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        // newThr为0,newThr = newCap * 0.75
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //当扩容后,则在现在将容器容量重新赋值给扩容阀值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        // 新生成一个table数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // oldTab 复制到 newTab
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                   // 链表只有一个节点,直接赋值
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // e为红黑树的情况
                    ((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;
}
扩容阀值和数组容量的关系

扩容分为两种情况:1是数组还不存在时需要对数组进行初始化,2是数组已经存在进行初始化

数组不存在时:
1,threshold>0, 将threshold设置为newCap也就是数组的大小,所以要用tableSizeFor方法保证threshold是2的幂次方
2,threshold = 0,将执行默认初始化数组长度为16,扩容阀值为12
数组已经存在时:
1,长度已经超过最大值,不再进行扩容,扩容阀值设置为integer的最大值,新增的元素只能以链表或tree的方式存放
2,长度没有超过最大值,则将长度设置为原有的2倍,如果扩大2倍后还没有超过数组最大值,则扩容阀值也扩大2倍

这里有个细节:如果构造器中对threshold进行了赋值,那么数组的容量和阀值一样,如果没有赋值,阀值默认为12之后扩容时阀值也会变为之前的2倍,但和数组容量并不一致

3-7remove(key) 方法 & remove(key, value) 方法

default boolean remove(Object key, Object value) {
    Object curValue = get(key);
    if (!Objects.equals(curValue, value) ||
        (curValue == null && !containsKey(key))) {
        return false;
    }
    remove(key);
    return true;
}
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

这两个方法最终都调用了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;
    //判断tab不为null,长度大于0,hash 对应的数组元素 也不为null 才可以进行删除
    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))))
            // index 元素只有一个元素,node 是需要删除的节点,先找到再后面进行删除
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                // index处是一个红黑树,则在红黑树中找到需要删除的node
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // index处是一个链表,遍历链表寻找 node
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    //此处p的值已经不是链表的第一个元素,是需要删除node 的上一个元素
                    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)
                //如果是第一个元素直接用next替换掉第一个
                tab[index] = node.next;
            else
                //如果不是第一个,则把上一个的next 直接指向下一个元素,p的元素在执行到这一行时,已经不是第一个元素
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

注释写的很清楚,不需要再进行解释,这里红黑树的加入增加了处理的难度,但在目前的分析中有关treenode的部分,都有特定的方法,可以暂时不用分析也能看懂大概的逻辑,有关treenode部分,再下一节中介绍

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 继续深入Java基础系列。今天是研究下哈希表,毕竟我们很多应用层的查找存储框架都是哈希作为它的根数据结构进行封装的...
    JackFrost_fuzhu阅读 1,280评论 0 4
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,008评论 25 707
  • 写在读完《人间失格》之后。--- 某石 ***************** 所谓人间的失格,大概是:源于自卑,而恶...
    澜南潜石阅读 225评论 0 1
  • 我喜欢你打哈欠的声音 所以慢慢学会了你的哈欠 就在刚刚 我打了一个哈欠 心头撞得震颤
    王不烦阅读 187评论 0 1
  • 默子江/文 喧嚣声 零零碎碎 寒风瑟瑟的街角 又出现了你的那双跛脚 陶瓷花盆落地的那一瞬间 我心碎 朝阳跨过田...
    默子江阅读 121评论 0 0