HashMap源码分析

自定义哈希表

* 哈希表又称为散列表 是一种数据结构 最典型的可以是数组+链表,数组+二叉树的结构 java中HashMap就是对散列表的一种实现
* 以数据+链表举例
* 具体需要有三个主要部分 分为1. 数组  2. 链表  3. 散列表需要放置的元素
* 数组的作用:用来存放链表
* 链表的作用:用来存放元素
* 元素的作用:存放需要记录的信息(对象)
* 通过计算散列值来确认数组的下标,以此表示需要将元素加入到那个链表
* java中HashMap通过key的hashcode值来计算散列值,以此确认数组的下标

代码实现如下:

package test.hash;

import java.util.HashMap;

/**
 *@ClassName HashTabTest
 *@Description 哈希表实现
 * 哈希表又称为散列表 是一种数据结构 最典型的可以是数组+链表,数组+二叉树的结构 java中HashMap就是对散列表的一种实现
 * 以数据+链表举例
 * 具体需要有三个主要部分 分为1. 数组  2. 链表  3. 散列表需要放置的元素
 * 数组的作用:用来存放链表
 * 链表的作用:用来存放元素
 * 元素的作用:存放需要记录的信息(对象)
 * 通过计算散列值来确认数组的下标,以此表示需要将元素加入到那个链表
 * java中HashMap通过key的hashcode值来计算散列值,以此确认数组的下标
 **/
public class HashTabTest {

    public static void main(String[] args) {
        //HashMap<String, String> hashMap = new HashMap<>();
        //测试自定义的散列表
        HashTab hashTab = new HashTab(4);
        hashTab.put(new Stu(1,"学生1"));
        hashTab.put(new Stu(2,"学生2"));
        hashTab.put(new Stu(3,"学生3"));
        hashTab.put(new Stu(4,"学生4"));
        hashTab.put(new Stu(5,"学生5"));
        hashTab.put(new Stu(9,"学生9"));
        hashTab.print();
        System.out.println(hashTab.get(10));
        System.out.println(hashTab.get(1));
    }
}
class HashTab {
    //定义数组 数组中元素类型为链表
    private StuList[] table;

    //初始化数组的大小
    private int size;

    public HashTab(int size) {
        this.size = size;
        table = new StuList[size];
        //给数组中每个stuList列表进行初始化
        for (int i = 0; i < size; i++) {
            table[i] = new StuList();
        }
    }

    //添加数据
    public void put(Stu stu) {
        //先获取散列值
        int hashNo = getHashNo(stu.id);
        table[hashNo].add(stu);
    }

    //查找通过id查找stu信息
    public Stu get(int id) {
        //先计算散列值
        int hashNo = getHashNo(id);
        return table[hashNo].get(id);
    }

    //遍历散列表
    public void print() {
        for (int i = 0; i < size; i++) {
            table[i].print();
        }
    }

    //获取数据的散列值 暂时不适用hashCode
    public int getHashNo (int id) {
        return id % size;
    }
}
class Stu { //类似于hashMap中的node
    int id;
    String name;
    Stu next;

    public Stu(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Stu{" +
                "id=" + id +
                ", name=" + name +
                '}';
    }
}

class StuList { //实现数组+链表结构中的学生链表 ,只存header 头结点
     private Stu head;

    public void add(Stu stu) { //向链表添加数据
        if (head == null) {
            head = stu;
            return;
        }
        //查找到最后元素,将最后元素的next指向stu
        Stu temp = head;
        while (true) {
            if (temp.next == null) { //找到了最后元素
                break;
            }
            temp = temp.next;
        }
        temp.next = stu;
    }

    //遍历链表数据
    public void print() {
        Stu temp = head;
        while (true) {
            if (temp == null) break;
            System.out.println(temp);
            temp = temp.next;
        }
    }

    //通过id查找链表中数据 找到就打印出来
    public Stu get(int id) {
        Stu temp = head;
        while (true) {
            if (temp == null) {
                //System.out.println("未找到该元素");
                return null;
            }
            if (temp.id == id) {
                //System.out.println(temp);
                return temp;
            }
            temp = temp.next;
        }
    }
}

打印如下:

Stu{id=4, name=学生4}
Stu{id=1, name=学生1}
Stu{id=5, name=学生5}
Stu{id=9, name=学生9}
Stu{id=2, name=学生2}
Stu{id=3, name=学生3}
null
Stu{id=1, name=学生1}

分析HashMap

实现哈希表结构需要数组和链表形成散列,新元素加入哈希表关键是完成取余操作,通过上述自定义哈希表的编写基础后,我们看HashMap源码中是如何完成的取余操作,然后分析分析HashMap中的数组,链表结构,最后分析put方法的源码;

1.理解HashMap中的位运算

通过分析HashMap源码,会看到源码中有如下两处位运算操作:
请添加图片描述
请添加图片描述

i = (n - 1) & hash     // n 是 hashmap中数组的长度,此句代码是一句取余操作
(h = key.hashCode()) ^ (h >>> 16)   //用于减少hash冲突

这两个位运算的基本知识是:

按位与 &,只有两个操作数都是1,结果为1,否则为0。1 & 1 = 1,0 & 1 = 0,1 & 0 = 0,0 & 0 = 0

异或 ^,当两个操作数相同结果为0,否则为1。1 ^ 1 = 0,0 ^ 1 = 1,1 ^ 0 = 1,0 ^ 0 = 0

问题1:首先,(n - 1) & hash 为什么是一步取余操作呢?

假如有 hash = 12345678,转换成二进制为 101111000110000101001110

此时如果数组长度为16,那个(n-1)= (16 -1)= 15的二进制为 1111

那么此时进行 (n - 1) & hash 操作,如下图:
请添加图片描述

12345678 % 16 = 14

最后得到的余数为 1110 = 14,此时会发现蓝色部分和结果的绿色部分拼接起来刚好等于原本的hash值

所以,源码此处的 (n - 1) & hash 和 hash % table.length 的结果一样都是取余操作

问题2:HashMap中数组长度为什么要是2的n次方?

根据上面的分析可以得知,只有2的n次方的高位只有一处为1,其余低位皆为0,例如:

16 = 10000,那么在进行 -1 操作以后,低位全变为1,此时 15 = 01111,此时进行 按位与操作,高位数据全部变为0,只为取到hash值的低位数据即是余数,如果不是(2^n -1),那么低位数据结果就不是余数了。

问题3:如何理解 (h = key.hashCode()) ^ (h >>> 16) 这一句位运算呢?

分析:为了尽可能减少hash冲突
请添加图片描述
请添加图片描述

int占有32位,如果不进行这一步运算,那么下一个key值高位发生变化而1110不发生变化的概率很大,因为在取余操作中,高位对取余结果不会产生影响,那么如果我们将hashcode的值无符号右移16位,然后再与hashcode按位异或,那么就会尽可能的让32的每一位就尽可能的参与取余操作,减少哈希冲突。

2.HashMap的结构分析

通过以上实现哈希表的思路,java中HashMap中肯定也是数组+链表的结构,再分析下其源码:
请添加图片描述
Entry是Map接口的一个内部接口,它提供了实现一个Key,Value数据结构需要的方法

再看HashMap中的Node
请添加图片描述

Node是继承了Entry接口,实现了getKey和getValue方法,另外还有key,value,hash,next(指向下一个元素)

HashMap中 数组是什么,链表是什么 ,元素是什么?
请添加图片描述数组是一个Node类型的数组,

链表是一个Node类型结点(自带next属性形成链表)

元素是一个K,V类型的映射类型对象

3.put方法源码分析

以下为源码的分析注释:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);  //调用hash方法进行异或位运算
}


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)  //将table赋给tab,n为数组的长度,如果数组为空,进行初始化扩容
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)  //先进行取模运算,然后判断数组当前位置是否为null,如果为空直接将新元素放入作为head,将所在数组的元素赋值给p
        tab[i] = newNode(hash, key, value, null);
    else {  //如果取模运算的当前数组位置不为null
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k)))) //p此时代表数组当前元素,即是链表的head,如果发生了hash冲突,并且key相等,则将p复制给e
            e = p;
        else if (p instanceof TreeNode)  //链表超过8就会自动转换成红黑树,这里是在判断该链表是否已经是红黑树,红黑树添加的方法和链表不同
            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 此处判断当前链表的元素是否超过7,超过7将链表转换成红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&    
                    ((k = e.key) == key || (key != null && key.equals(k)))) //如果不等于null,且key的hash和equals都相等,返回当前e
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key 如果e不为null表示出现了相同的key,hashcode和equals都相等
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)   //将改节点的value替换成最新的,且把原value返回
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;   //modCount 用来记录map结构被修改的次数 
    if (++size > threshold)  //如果size大于 threshold(即将发生扩容的阈值) 此时执行扩容
        resize(); //扩容方法 每次扩容一倍 保持2的n次方
    afterNodeInsertion(evict);
    return null;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

@晴天_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值