基础准备知识。
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) & hash)n 必须是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的讲解很不错。