我理解的hashmap

平时里我们总说HashMap的初始化长度是16,就是说一开始加载就是16,其实这个是不对的,一开始在进行new的时候其实是0,然后再进行put的时候才变为16

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

//然后就是put操作
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
//点进去的这个
 if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
 //在进入resize()
 Node<K,V>[] oldTab = table;
 table = newTab;//就是这里进行了扩容

其实对于扩容的是DEFAULT_INITIAL_CAPACITY = 1 << 4 ,这里的<<也可以留一下,我记得好像是因为如果是2* 2 * 2 *2 在编译的时候也会被翻译成1<<4,因此这样写会提高性能,并且有关于为什么是16,这个是取决是hash()方法的,hash方法再进行计算的时候是取length-1,那么这里取的就是15 ,翻译成2进制就是1111,然后因为1&任何数字就是那个数字,这就保证了hash值的多样性,如果是0,那么任何数字& 它都是0;

其实就是对于扩容的时候,这里其实有几个注意点,大家的普遍说法是在达到扩容因子(16*0.75=12)的时候就进行扩容,我记得好像是在1.7的时候还会进行判断,就是判断是否是>12,并且判断那个hash值对应的数组的索引上是否有值,如果有值就扩容,没有就存放,所以理论上在扩容之前能存放的最大数字应该是(11(hash碰撞)+10)!!!!!(因为我没有安装jdk,所以记得不清楚)

在1.8以后进行了更改,

//但是这里因为判断的是直接的map的size,我一开始以为判断的是map那个数组的长度,结果发现是size,我感觉这样就扩容了是不是挺耗费内存的 
if (++size > threshold)
            resize();

对于1.7和1.8还发生了红黑树的改变,就是超过8改变,小于6了又变回来


  if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
    tab[index] = first.untreeify(map);  // too small
    

其实关于红黑树方面,我看过过一篇文章,关于红黑树的作用的,但我是数学渣看了很懵,
https://msd.misuland.com/pd/3223833238703184856
里面的意思就是说其实要想达到链表长度为8的概率是极地极地的,

其实对于1.7,1.8的改变还有个小细节,那就是1.7之前用的是头插法,1.8会变成尾插法

public V put(K key, V value) {
 	 addEntry(hash, key, value, i);   
}
void addEntry(int hash, K key, V value, int bucketIndex) {
	table[bucketIndex] = new Entry<K,V>(hash, key, value, e);    
}

这里平时是不会出现问题的,但是如果多线程进行扩容的时候就会出现循环的问题
//1.8
    public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

等以后想到啥就更新啥把,,,,并且里面可能会有错误,希望大家提醒下我

//哎,其实我很懒的,不喜欢写文章,但就是最近在面试,说是博客是个加分项。emm

9.30更新

在博客上看到一篇文章

https://blog.csdn.net/weixin_34346099/article/details/91940085

这里提到了HashMap有个read和writeobject方法进行了自己的实现并且是private修饰,首先是private修饰是可以防止被子类所重写,但是这两个方法再类里面没有任何地方进行了调用

private void writeObject(java.io.ObjectOutputStream s) throws IOException
 
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException

首先因为是hashmap实现了Serializable接口,因此该类能够被实例化,然后对于操作序列化的类是ObjectOutputStream,反序列化是ObjectInputStream

public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }
/** if true, invoke writeObjectOverride() instead of writeObject() */
    private final boolean enableOverride;

这个代码的意思就是判断有没有重写writeObject方法,,如果有就调用重写的方法

以下直接是原话摘抄了:

为什么HashMap要自己实现writeObject和readObject方法,而不是使用JDK统一的默认序列化和反序列化操作呢?

首先要明确序列化的目的,将java对象序列化,一定是为了在某个时刻能够将该对象反序列化,而且一般来讲序列化和反序列化所在的机器是不同的,因为序列化最常用的场景就是跨机器的调用(把对象转化为字节流,才能进行网络传输),而序列化和反序列化的一个最基本的要求就是,反序列化之后的对象与序列化之前的对象是一致的。

HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。

Hash值不同导致的结果就是:有可能一个HashMap对象的反序列化结果与序列化之前的结果不一致。即有可能序列化之前,Key=’AAA’的元素放在数组的第0个位置,而反序列化值后,根据Key获取元素的时候,可能需要从数组为2的位置来获取,而此时获取到的数据与序列化之前肯定是不同的。 在《Effective Java》中,Joshua大神对此有所解释:

For example, consider the case of a hash table. The physical representation is a sequence of hash buckets containing key-value entries. The bucket that an entry resides in is a function of the hash code of its key, which is not, in general, guaranteed to be the same from JVM implementation to JVM implementation. In fact, it isn’t even guaranteed to be the same from run to run. Therefore, accepting the default serialized form for a hash table would constitute a serious bug. Serializing and deserializing the hash table could yield an object whose invariants were seriously corrupt.

所以为了避免这个问题,HashMap采用了下面的方式来解决:

  1. 将可能会造成数据不一致的元素使用transient关键字修饰,从而避免JDK中默认序列化方法对该对象的序列化操作。不序列化的包括:Entry[ ] table,size,modCount。
  2. 自己实现writeObject方法,从而保证序列化和反序列化结果的一致性。
  1. 那么,HashMap又是通过什么手段来保证序列化和反序列化数据的一致性的呢?
  1. 首先,HashMap序列化的时候不会将保存数据的数组序列化,而是将元素个数以及每个元素的Key和Value都进行序列化。
  2. 在反序列化的时候,重新计算Key和Value的位置,重新填充一个数组。

想想看,是不是能够解决序列化和反序列化不一致的情况呢? 由于不序列化存放元素的Entry数组,而是反序列化的时候重新生成,这样就避免了反序列化之后根据Key获取到的元素与序列化之前获取到的元素不同。

不知道你们有没有看过Object类中对于hashcode的描述里面有这样一句话

​ *** This integer need not remain consistent from one execution of an**

翻译过来就是此整数无需在每次执行一次时保持一致

我的理解时hashcode是对于内存地址值的映射,因此每次启动jvm可能内存地址值会发生变化,所以不一定会保持一致!!!!!!!!!!!

但是我试过一次,new一个对象使用默认的hashcode计算了,然后重启了电脑后又计算了一次发现2个数字是一样的,,,,????等我技术更高了再来解决这个问题

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值