Java集合(九)HashMap接口

HashMap小结:

(1)Map接口的常用实现类:HashMap、Hashtable和Properties

(2)HashMap是Map接口使用频率最高的实现类

(3)HashMap是以key-val对的方式来存储数据(HashMap$Node类型)

(4)key不能重复,但是值可以重复,允许使用null键和null值

(5)如果添加相同的key,则会覆盖原来的key-val,等同于修改.(key不会替换,val会替换)

我们通过代码来对源码进行解读:

package com.rgf.map;

import java.util.HashMap;
import java.util.Map;

@SuppressWarnings({"all"})
public class Map_ {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("no1","rgf");//k-v
        map.put("no2","张无忌");
        map.put("no1","张三丰");//当有相同的key值时,会替换掉上面的value
        map.put("no3","张三丰");
        map.put(null,null);
        map.put(null,"abc");//等价替换
        map.put("no4",null);//k-v
        map.put("not5",null);//k-v
        map.put(1,"赵敏");//k-v
        map.put(new Object(),"金毛狮王");//k-v
        //通过get方法,传入key,会返回对应的value
        System.out.println(map.get(1));
        System.out.println("map="+map);



    }
}

我们来把断点放在map.put("no1","张三丰“);

我们下来进行查看: 

此时我们的table表为:

 我们继续往进走:

 我们发现我们进去的hash值与此前的no1的值相同:

 我们进入的源码如下所示:

 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语句。
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
//这里算出来的i的位置为原来存储rgf所在的索引值,为1.。为p不为空。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
//这条if语句比较的是key,而不是value。这条语句成立,我们将p即为no1赋值给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);
                        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;
    }

 hash值相同,导致i相同:

 因为不为null,该条语句不符合,继续执行:

 该条语句符合,我们将该值赋值给e。

符合之后,我们执行如下语句:

 我们将新值赋给之前的value,即e指向的之前的rgf的结点,从而完成替换。

 我们发现此时替换成功:

(6)与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的(jdk8的hashMap底层:数组+链表+红黑树)

(7)HashMap没有实现同步,因此是线程不安全的。方法没有做同步互斥的操作,没有synchronized

HashMap底层机制及源码剖析:

(1)HashMap底层维护了Node类型的数组table,默认为null。里面的数组为table数组 ,里面的每一个结点为HashMap$Node1结点,而这个实现了Map接口里面的Entry接口。

(2)当创建对象时,将加载因子(loadfactor)初始化为0.75

(3)当添加key-val时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加,如果该索引处有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。

(4)第一次添加,则需要扩容table容量为16,临界值(threshold)为12。(16*0.75=12)

(5)以后再扩容,则需要扩容table容量为原来的2倍,即32,临界值为原来的2倍,即24,依此类推

(6)再Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)。

我们来进行源码分析:

我们设计如下代码进行源码分析:

package com.rgf.map;

import java.util.HashMap;

@SuppressWarnings({"all"})
public class HashMapSources1 {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        map.put("java",10);
        map.put("php",10);
        map.put("java",20);//替换value

        System.out.println("map="+map);
    }
}

运行界面如下所示:

 我们从HashMap map =new HashMap( )这条开始进行Debug.

1.我们发现先初始化了加载因子:

 DEFAULT_LOAD_FACTOR,加载因子为0.75.

完成之后,我们发现此时的table=null.

2.然后我们进入put方法:

我们先进行自由装箱:

 退出去之后,我们进入put方法:

 里面的hash(key)我们可以通过此算法来计算出hash值。

3.之后我们进入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; //辅助变量
//如果底层的table数组为null,或者length=0,就扩容到16
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
//取出hash值对应的table表的索引的位置的Node,如果为null,就直接把加入的k-v创建成一个Node,加入该位置即可。
        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 &&  //如果table表的索引位置的key的hash值和新的key的hash值相同并且满足(现有的结点的key和添加的key是同一个对象 ||equals返回真,就认为不能加入新的k-v.
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)//如果当前存在的table的已有的Node是红黑树,按照红黑树的方式处理
                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);
//加入后,判断当前链表的个数,是否已经到8个,到达8个后,就调用treeifyBin进行红黑树的转换。
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash && //如果再循环比较过程中,发现有相同,就break,就只是替换value
                        ((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;//替换,key对应的value。
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//每增加一个Node,就size++
        if (++size > threshold)//刚开始12,后面24,48.。。,如果size大于临界值,就进行扩容。
            resize();
        afterNodeInsertion(evict);
        return null;
    }

我查看树化的代码:
 

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
//如果table表为空,或者table表当前的大小是小于64,就暂时不树化,而是进行数组的扩容。
//否则才会进行真正的树化。
//剪枝:树化后随着结点的减少又会退化成链表。
        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);
        }
    }

 

如股票我们判断table为空的话,我们进行resize进行扩容。

我们进入resize扩容代码如下所示:

 

 当我们的oldCap不大于0的时候,我们进入下面的else语句:

 里面的DEFAULT_INITIAL_CAPACITY默认的初始化的大小为16,

newCap的初始化大小为16,而新建的table表的类型就是Node。

我们从扩容里面跳出来之后,我们发现新建的表的大小为:

  

 之后我们从这个hash值得到这个索引位置:

如果当前位置为空的话,我们就把这个Node值给放到对应的索引位置。 

完成这一步之后,我们继续进行下一步,我们发现已经成功的存储进去了:

 已经有一个元素了,不再是全空了。

我们继续进行下一步:

其中的thresho为他的临界值,为12. 到了临界值我们就要进行扩容。通过下面的resize进行扩容。

我们返回之后,我们table表里面已经存在java-10了。

我们继续开始debug下一行代码,如下所示:

我们开始继续进行自由装箱:

我们退出自由装箱后,进入put方法:

我们进入hash(key)计算出hash值。我们进入putVal方法:

我们也去判断现在table是否为空,但是之前已经存入了java-10,不等于空了。我们继续进行下一步:

我们索引完之后不是原先的位置,所以也是为空,与此前存的java索引完之后不再同一个位置,之后把php扔进去新的结点。

我们完成之后,我们发现table表又有了新的结点:

我们继续进行下一步之后,我们发现:

  此时的size为2,仍然不大于他的临界值,不进行扩容。

完了之后,我们进行返回。

我们debug第三行代码map,put("java",20);

我们进行如下所示:

我们进行自由装箱 :

我们退出去之后,我们进入put方法:

 我们之前就有一个java,所以我们通过算法算出的hash值与此前的相同,我们进入putVal源码进行查看:

 我们再此前我们算出的java的hash值为3,现在添加的值的key仍然是java,所以我们计算出来的hash值仍然是3,所以此时算出来的索引值所在位置不等于空,不等于空我们进入如下代码:

 我们将代码如下所示:

 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,else if,else三种情况,

第一种情况为我们先判断hash值是否相同,或者不是同一个对象但是内容相同,出现这种情况,我们不进行添加,即将e指向p.

第二种情况为如果是一个红黑树,按照红黑树的规则进行添加

第三种情况为我们数组后面是一个链表,需要与链表里面的内容进行一一比对。

我们添加的java与此前的相同,既符合第一种情况,直接跳出:

e不等于空。我们进行了如下所示:

我们此时将值进行了替换。 

我们发现此时进行了成功的替换。 

HashMap扩容树化扩容:

我们设计的的代码如下所示:

package com.rgf.map;

import java.util.HashMap;
import java.util.Objects;

@SuppressWarnings({"all"})
public class HashMapSources2 {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        for (int i = 0; i <= 12; i++) {
            hashMap.put(new A(i),"hello");

        }
        System.out.println("hashmap="+hashMap);//12个k-v
    }
}
class A{
    private  int num;

    public A(int num) {
        this.num = num;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        A a = (A) o;
        return num == a.num;
    }
//所有的A对象的hashCode都是100
    @Override
    public int hashCode() {
        return 100;
    }
}

运行界面如下所示:

 我们在 hashMap.put(new A(i),"hello");进行下断点:

因为我们设置得hashcode值相同,所以会放在table表的数组里面的同一个链表下面。

 我们发现此时的i为9,但是此时的数组没有满足64我们先不进行树化,而是先进行扩容,我们查看如下所示:

我们发现此时得结点类型仍然是HashMap$Node, 

我们扩容两次之后,我们发现如下所示: 

 

 此时的结点类型为HashMap$TreeNode.说明此时已经树化了。

我们查看树化的源码如下所示:

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        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);
        }
    }

我们下来进行表的扩容的代码展示:

我们设计的代码如下所示:

package com.rgf.map;

import java.util.HashMap;
import java.util.Objects;

@SuppressWarnings({"all"})
public class HashMapSources2 {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        for (int i = 1; i <= 12; i++) {
            hashMap.put(i,"hello");

        }
        hashMap.put("rgf","ypl");
        System.out.println("hashmap="+hashMap);//12个k-v
    }
}
class A{
    private  int num;

    public A(int num) {
        this.num = num;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        A a = (A) o;
        return num == a.num;
    }


    @Override
    public String toString() {
        return "\nA{" +
                "num=" + num +
                '}';
    }
}

我们运行界面如下所示:

 我们进行debug。如下所示:

我们发现此时的table表已经达到临界值,threshold=12,我们新增之后继续进行查看:

我们发现此时扩容为32,即扩容为两倍。 而且新的临界值变成了24:threshold=24

数组扩容为0-->16(12)-->32(24)-->64(64*0.75=48)-->128(96),依此类推,每次扩容两倍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一直再追梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值