jdk1.7 HashMap的实现原理

本文暂时只讨论HashMap中put/get方法原理实现,后续更新扩容机制

本着为读者着想(//不写大白话自己也看不懂)的态度完成

我们用一段简单的代码做测试涵盖

  • map处理put/set区分是否为null键两种形式
    public static void main(String[] args) {
        HashMap<Object, Object> map = new HashMap<>();
        map.put(null,null);
        //map只关心是不是null键,不关心值这种方式和上面的null键null值方式走一条路
        //map.put(null,"xxs");  

        map.put("key","value");

        map.get("key");
        map.get(null);
    }

前提

在看下面的内容之前,得先知道,map是用数组+链表实现的,我们先不管链表,map怎么判断往数组的哪个位置放键值对呢?

以下是map对于数组下标怎么获取(key!=null的情形),也就是往数组的哪个位置放键值对的代码

//var1 = key
int var3 = this.hash(var1); //获取key的hash

//this.table.length : 该map集合的长度,默认为16
indexFor(var3, this.table.length);

//这里返回的int 就是对应的数组的下标
static int indexFor(int var0, int var1) {
    //&运算符 : 先将数字转换为二进制,如果相对应位都是1,则结果为1,否则为0
    //为什么要让var1(map长度)减一,因为数组下标是length-1,防越界
    return var0 & var1 - 1; 
}

通过以上代码可以知道往map里啪啪啪put的时候数据在哪,相反get的时候只要通过同样的方式算出对应的下标就能取出对应的键值对

不知道hash值是啥?
这里写图片描述

put

一点点介绍,首先是最简单的情况:

  • null键< map.put(null,null)>

    key = var1 / value = var2

//以下为源码部分(只提取出关于null键的)
        public V put(K var1, V var2) {//var1=null,var2=null
            if (var1 == null) {
                return this.putForNullKey(var2);
        }

        private V putForNullKey(V var1) {
        for(HashMap.Entry var2 = this.table[0]; var2 != null; var2 = var2.next) {
            if (var2.key == null) {
                Object var3 = var2.value;
                var2.value = var1;
                var2.recordAccess(this);
                return var3;
            }
        }

        ++this.modCount;
        this.addEntry(0, (Object)null, var1, 0);
        return null;
        }

解释一下,主要是putForNullKey方法
从代码HashMap.Entry var2 this.table[0]; var2 != null;我们可以看出来,map直接对数组的0号位取值判断是否为空,也就是只要put了null键,就往数组0号位放

var2 = var2.next这一部分我们放一放下面讲

this.addEntry(0, (Object)null, var1, 0);储存到数组中的方法

//以下源码

void addEntry(int var1, K var2, V var3, int var4) {
//这一部分是扩容
        if (this.size >= this.threshold && null != this.table[var4]) {
            this.resize(2 * this.table.length);
            var1 = null != var2 ? this.hash(var2) : 0;
            var4 = indexFor(var1, this.table.length);
        }

//正式添加数据到数组
        this.createEntry(var1, var2, var3, var4);
    }

----------

 void createEntry(int var1, K var2, V var3, int var4) {
     //table : map中真正存数据的数组
     //Entry 对象 : map中键值对对象,我们存的每对key/value都会被放到这个对象中统一管理
      HashMap.Entry var5 = this.table[var4];

    //统计大小,调用的HashMap.Size()就是取它,在每次添加元素时++
       ++this.size;
   }



----------


Entry(int var1, K var2, V var3, HashMap.Entry<K, V> var4) {
      this.value = var3;  //值
      this.next = var4;   //链表指向
      this.key = var2;    //键
      this.hash = var1;   //key的hash值
 }
  • 非null键< map.put(“key”,“value”)>
//以下源码
var1 = "key" / var2="value"
public V put(K var1, V var2) {
        if (var1 == null) {
            //null键 上文解析
            return this.putForNullKey(var2);
        } else {
            //非null键
            //获取key的hash值
            int var3 = this.hash(var1);
            //根据hash获取应该放到数组的第几位(上文解析该方法)
            int var4 = indexFor(var3, this.table.length);   //var4 角标

            //【关键】 下文详解
            for(HashMap.Entry var5 = this.table[var4]; var5 != null; var5 = var5.next) {
                if (var5.hash == var3) {
                    Object var6 = var5.key;
                    if (var5.key == var1 || var1.equals(var6)) {
                        Object var7 = var5.value;
                        var5.value = var2;
                        var5.recordAccess(this);
                        return var7;
                    }
                }
            }

            ++this.modCount;
            this.addEntry(var3, var1, var2, var4);
            return null;
        }
    }

在了解map储存非null键之前,要先解决一个问题:

数组初始化大小为16,如果储存16个key在通过indexFor方法获取的索引下标都相同怎么办?

这里写图片描述

for(HashMap.Entry var5 = this.table[var4]; var5 != null; var5 = var5.next) {
                if (var5.hash == var3) {
                    Object var6 = var5.key;
                    if (var5.key == var1 || var1.equals(var6)) {
                        Object var7 = var5.value;
                        var5.value = var2;
                        var5.recordAccess(this);
                        return var7;
                    }
                }
            }
this.addEntry(var3, var1, var2, var4);

上面这段循环解决了角标相同时的链表储存形式
循环首先判断需要放置数据的坑是否有值(HashMap.Entry var5 = this.table[var4]; var5 != null;
如果为空不进循环,按照正常插入数据
如果不为空,即为有值,判断该值的key与新插入数据的key的hash是否一致(从hash判断是否相同值,理论可能出现hash相同值不同情况)if (var5.hash == var3),如果true则继续判断其地址/值是否相同if (var5.key == var1 || var1.equals(var6))如果相同覆盖该值。
若第一次循环的值不相同,找下一个(Entry.next(上图箭头))直到下一个为空(没有指向)
循环结束,执行添加数据的方法


//var1=key,var2=value,var3=key.hash(),var4=数组角标
void addEntry(int var1, K var2, V var3, int var4) {
        ...
        this.createEntry(var1, var2, var3, var4);
}

void createEntry(int var1, K var2, V var3, int var4) {
        //这里将在这个坑的原有数据取出
        //var5=原数据
        HashMap.Entry var5 = this.table[var4];
        this.table[var4] = new HashMap.Entry(var1, var2, var3, var5);
        ++this.size;
    }



----------
//将新键值加入到原有坑,旧的值连同它后面的链表都防止新键值的next
Entry(int var1, K var2, V var3, HashMap.Entry<K, V> var4) {
      this.value = var3;  //值
      this.next = var4;   //链表指向
      this.key = var2;    //键
      this.hash = var1;   //key的hash值
        }

加入新的键值对是从前进入,可以理解为类似栈结构。

get

同样分为是否取null的值
<map.get(null);>

//取值源码
  private V getForNullKey() {
        for(HashMap.Entry var1 = this.table[0]; var1 != null; var1 = var1.next) {
            if (var1.key == null) {
                return var1.value;
            }
        }

找到数组0号元素,迭代后面的链表,找到key=null,取值即可

<map.get("key");>

 public V get(Object var1) {
        if (var1 == null) {
            return this.getForNullKey();
        } else {
            HashMap.Entry var2 = this.getEntry(var1);
            return null == var2 ? null : var2.getValue();
        }
    }

final HashMap.Entry<K, V> getEntry(Object var1) {
        //再次判断key是否为null
        int var2 = var1 == null ? 0 : this.hash(var1);

        for(HashMap.Entry var3 = this.table[indexFor(var2, this.table.length)]; var3 != null; var3 = var3.next) {
            if (var3.hash == var2) {
                Object var4 = var3.key;
                if (var3.key == var1 || var1 != null && var1.equals(var4)) {
                    return var3;
                }
            }
        }

        return null;
    }

整体和put相似,先根据key算出hash,根据indexFor方法计算角标,迭代链表,如果hash值相同进一步判断是否是同一对象,如果也相同认为是相同对象,返回该键值对

吃饭去了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值