深入理解Java HashMap

基础准备知识。

1. Hashcode。

2. == 和equals方法

3. ^异或, &与

4. 数组,链表,红黑树

 

Hashcode。

hashcode是系统用来快速检索对象而使用

简单的说,对象的hashCode是将该对象的内存地址转换成的一个整数。

但是在HashMap里面使用,还要达到另外一个目的, 也就是必须散列化, 以减少撞值。所以还要再散列一次:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

以上这个方法的目的就是让每一位值的变化都会导致hash值的变化。最终达到下表index的散列化, 以减少撞值。

下面这张图是java sdk7 int hash(Object key)的处理方式(摘自https://www.iteye.com/topic/709945, 里面有更详细的讲解):

== 和equals()方法

==

基本数据类型(byte,short,char,int,float,double,long,boolean), 以及String是作为常量在方法区中的常量池里面以HashSet策略存储起来的。在常量池中,一个常量对应一个地址,因此相同的字符串只会存储一个地址,由于他们的引用都是指向的同一块地址,因此基本数据类型和String是可以直接通过==来直接比较的。

equals()方法

public boolean equals(Object obj) {
    return (this == obj);
}

在Object类中,equals方法本质上就是== 。所以并没有什么区别。但是我们可以在子类重写 equals()方法。至于重写后是什么逻辑,是比较内存地址也好, 比较内容也好,完全有我们自己决定。所以问==和equals()方法有什么区别。说==是比较内存,而equals()方法比较内容,这种说法很片面。equals()方法也可以只比较内存地址, 只看业务需求。

但是如果重写equals()方法,通常遵守以下规则顺序:

1. 先比较内存地址是否相同。再比较对象类型是否相同, if( a instanceof aClass)。最后比较内容是否相同。

2. 满足以下效果原则:

自反性: x.equals(x)必须返回是“true” ;

类推性: 如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true” ;

一致性: 如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true” ;

对称性: 如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。

 

3. 与 hashCode()的关系满足以下原则:

原则 1 : 如果 x.equals(y) 返回 “true”,那么 x 和 y 的 hashCode() 必须相等 ;所以重写equals, 通常也要重写hashCode()
原则 2 : 如果 x.equals(y) 返回 “false”,那么 x 和 y 的 hashCode() 有可能相等,也有可能不等 ;
原则 3 : 如果 x 和 y 的 hashCode() 不相等,那么 x.equals(y) 一定返回 “false” ;
原则 4 : 一般来讲,equals 这个方法是给用户调用的,而 hashcode 方法一般用户不会去调用 ;
原则 5 : 当一个对象类型作为集合对象的元素时,那么这个对象应该拥有自己的equals()和hashCode()设计,而且要遵守前面所说的几个原则。

在HashMap中,如果hash值相同,导致撞值。再比较的时候就是用equals()方法。

 

^异或, &与

异或的使用和应用可以参考 Java异或XOR的应用。HaspMap中用杂对hashcode的再hash。

&与的作用应该很清楚,但是就应用不一定了。在HashMap中对再hash后的值取模,就是对象存储的数组下标。

但是用%取模和用&取模效率较差很多,所以用&取模。但是要满足一个条件((n - 1) & hashn 必须是2的n次方才能取模。这也是为什么HaspMap的长度必须是2的n次方。

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

额外的发现就是这些二进制操作符的作用,尽然可以如此的灵活多样。

数组,链表,红黑树

数组的特点就是根据数组下标查询速度快。所以HaspMap利用了这一点。利用每个对象的hashCode多数情况下不同, 来生成数组下标。 但是依旧会有数组下标相同的情况。 就是撞值。撞值了以后怎么解决,这就引出了链表和红黑树。

在Java SDK8以前,遇到撞值就存到table[i]的next里面。也就是说数组的item是每一个链表的头。就是一个链表数组。撞值后,循环这个链表,根据equals()方法来比较是否相同, 如果相同就替换,不相同, 就以头插法插入链表。Java SDK8以后改为尾插法,因为头插法,在多线程时会死循环。虽然HashMap不支持线程安全。但是死循环过于恐怖,所以Java SDK8以后改了。

但是如果撞值多了,链表也就长了, 链表查询时间是O(n),所以效率就差了。但是完美的平衡二叉树的查询时间是logN. 所以就又在Java SDK8的时候引入了红黑树。红黑树可以尽量让平衡二叉树保持完美。没有红黑树那一颗树就很可能和链表差不多了。

这是java7中的数据结构,撞值区当始终是链表结构。

 

这是java8中的数据结构,撞值区的链表长度超过8时,自动转为红黑树。

 

 

HashMap继承实现了AbstractMap抽象类,AbstractMap尽力实现了Map接口。使得HashMap类得以简化。

HashMap默认的构造方法:

//默认初始容量 (16) 和默认加载因子 (0.75)   

HashMap()  
// 指定初始容量和默认加载因子 (0.75) 。  最终真实容量是initialCapacity<=2^n. 因为长度必须是2的n

//次方。原因上面有讲。
HashMap(int initialCapacity)  
   // 指定初始容量和加载因子 。 java建议加载因子是0.75. 不需要去改动。
HashMap(int initialCapacity, float loadFactor)  

 

HashMap的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
加载因子,当容量达到总容量的0.75, 就扩容HashMap的容量, 每次扩大一倍。以减少撞值情况的发生。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
当撞值链表长度超过8时,就转成红黑树的结构。
static final int TREEIFY_THRESHOLD = 8;
当原有的红黑树内数量 < 6时,则将 红黑树转换成链表
static final int UNTREEIFY_THRESHOLD = 6;
最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)
否则,若桶内元素太多时,则直接扩容,而不是树形化。
为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

HashMap树化要同时满足:TREEIFY_THRESHOLD>8 && MIN_TREEIFY_CAPACITY>=64

默认桶数量是16个,也就是上面图中的数组结构,数组长度是16。加载因子是0.75,当HashMap容量达到16*0.75=12时,HashMap就扩容为了32个桶, 也就是数组长度变成了32.
相对准确的估算数据量,将极大的影响HashMap的性能,因为resize是一个重新分配的过程,耗时应该是里面最大的。
加载因子越大,填满的元素越多,空间利用率高了,则查找的成本越高.
加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了,但查找变快了

最后一个问题:为什么每次扩容是2的n次幂?

1. hashmap是通过取余数确定放在那个数据里, 计算公式: hash % n,这个是十进制计算。在计算机中, (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,计算更加高效。

2 只有是2的幂数的数字经过n-1之后,二进制肯定是 ...11111111 这样的格式。2的幂次方*2,在二进制中比如4和8,代表2的2次方和3次方,他们的2进制结构相 似,比如 4和8 00000100 0000 1000 只是高位向前移了一位,这样扩容的时候,只需要判断高位hash,移动到之前位置的倍数就可以了,免去了重新计算位置的运算。

 

下面两个链接对HashMap的讲解很不错。

https://study.163.com/course/courseLearn.htm?courseId=1209173825#/learn/live?lessonId=1278756575&courseId=1209173825

https://blog.csdn.net/justloveyou_/article/details/62893086

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值