当hash表中的负载因子达到负载极限的时候,hash表会自动成倍的增加容量(桶的数量),并将原有对象重新分配并加入新的桶内,这个过程称为rehash,这个过程是十分耗性能的,一般不这样子弄,一般建议设置比较大的初始化容量,防止rehash,但是也不能太大。
这是自己粗糙的实现的hashMap的源码:测试过的
package org.llyf.hashmap;
public class MyHashMap<K,V>{
private Entry[] table;
private static Integer CAPACITY =8;
//
private int size=0;
public MyHashMap() {
this.table = new Entry[CAPACITY];
}
//这个size()方法其实就是统计你hashMap有多少个元素,所以说你返回table.length显然是不对的 没新增一个元素的时候就size++
public int size() {
return size;
}
public Object get(Object key) {
int hash = key.hashCode();
int i=hash %8;
//get值的话也是循环该链表,找到下标,
for(Entry<K,V> e=table[i]; e !=null;e=e.next){
//判断循环到该节点的key与我们传进来的key是否相等
if(e.k.equals(key)){
return e.getV();
}
}
return null;
}
public Object put(K key, V value) {
//把这个节点添加到数组里面 首先要计算该节点的下标用hashcode然后再取模
int hash = key.hashCode();
int i=hash %8;
//这里hashmap所对应的返回值,如果是(曹操 1) 再put一个(曹操 2)这个时候就是key相同,值不相同的情况,hashmap对于这样的情况,
//就会把老的值给更新掉 1变成2,然后把老的给返回出来,这时候的操作就是遍历该链表,找到key 把value值更新 把老的value返回出去
//这个for循环从头节点开始循环,条件是e.next !=null 等于空的话就代表结束了
for(Entry<K,V> e=table[i];e !=null;e=e.next){
//只要判断循环到的这个节点的key和我们传进来的key
if(e.k.equals(key)){
//如果相等我们得存一下老值
V oldValue=e.v;
e.v=value;
return oldValue;
}
}
//计算addEntry(key, value, i);下标以后jdk1.7是放在数组节点的头部,jdk1.8是放在数组节点的后边 这时候该节点可以用table[i]来表示
addEntry(key, value, i);
return null;
}
//增加一个节点
private void addEntry(K key, V value, int i) {
Entry<K,V> entry = new Entry(key,value,table[i]);
table[i]=entry;
size++;
}
//源码中多存了一个hash 这个hash就是key多对应的hash值
class Entry<K,V>{
private K k;
private V v;
private Entry<K,V> next;
public Entry(K k, V v, Entry<K, V> next) {
this.k = k;
this.v = v;
this.next = next;
}
public K getK() {
return k;
}
public V getV() {
return v;
}
public Entry<K, V> getNext() {
return next;
}
}
public static void main(String[] args){
MyHashMap<String,String> myHashMap = new MyHashMap<>();
for(int i=0;i<10;i++){
myHashMap.put("周瑜"+i,"周瑜"+i);
}
System.out.println(myHashMap.get("周瑜"));
}
}
这是随便给的图,可以参考这张图分析更直观:
这个是hashMap初始化的源码
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity 这里的意思是比方说我传的11则大于11的2的次方数是16 ,则下面是new出来16大 //小的数组
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
//这里算出来的阈值是16*0.75=12
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
static int indexFor(int h, int length) {
return h & (length-1);
}
这个是求数组下标的操作,下面解释下为什么要这么做 length=16这个大家都知道
0000 1000 8
0001 0000 16
0000 1111 16-1 上面就是15来&hash(因为hashcode是比较乱的随便举一个1010 0101) 所以这个操作就变成了
0000 1111
& 1010 0101 从这里可以看出来这结果被控制在后四位了 因为前四位都是0000 最终的取值范围也就是 后四位的取值大小,那么这后四位的取值范围就是0-15 最终i的取值范围肯定是0-15毋庸置疑了 ,这也就说明了为什么会是2的次方数,因为拿到2^n会去减1,减1之后的效果就是它的低位全部都是1,低位全是1的效果就能保证它最后算出来的值就是这低位的取值范围,这样就能保证数组下标不会越界,
h ^= k.hashCode(); 那么它拿到这个hashcode为什么要进行这样的操作(异或和右移)呢? 因为一个好的hash算法,它要求的散列性是很高的,正如上面刚才说的真正能够控制hash值对应数组下标的就是这后四位,前面的高四位并没有什么用,你再来很多hashcode的时候会是什么情况呢,出来的结果很多都是一样的概率很大,这就导致对应的数组下标都是一样的,这样的散列性其实就是很差的
0000 1111
1010 0101
1010 0101 这是右移之后的效果,最终的效果就是你所有的位数,包括你高的几位都参与了运算,最终你算出来的这个hashcode,所有的高位和低位都能影响这个hash值,这样达到的最终散列性就会比较高了
它这种做法就可以节省你的内存使用率,
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
这是另外一段源码 增加节点的
void addEntry(int hash, K key, V value, int bucketIndex) {
//数组size>阈值的时候就进行扩容,但是&&后边的条件是什么意思呢??结合图
容量是8 假如说达到6个就开始扩容,但是那个项羽如果放在那个空的地方的时候是不会进行扩容的,只有落在1-4这中间才会触发扩容,所以想要扩容,还得加一个条件就是
table[bucketIndex]不能为空。但是在jdk8中就没有这个条件了
这里讲下jdk7与jdk8的区别
1 jdk8会将链表变成红黑树
左边是完全平衡二叉树,右边是红黑树,为什么不偏偏用红黑树呢?
插入效率:链表>红黑树>完全平衡二叉树
查询效率:链表<红黑树<完全平衡二叉树 红黑树的插入和查找是处于两者之间的,那为什么要用红黑树呢,因为我们在用hashMap的时候不仅仅时需要查询的,而且余插入等等,这些操作需要平衡,所以就选择红黑树来达到这些平衡,jdk7用的是链表,如果数据量比较大的时候,你的链表还是比较长,你在查这个链表的时候性能还是比较低的,jdk8就是想优化一些查询
static final int TREEIFY_THRESHOLD = 8; 这里是jdk8多出来的属性,当打到这个属性值8的时候,这个链表就会变成红黑树
static final int UNTREEIFY_THRESHOLD = 6;当删除节点时候达到这个值的时候就会从红黑树变成链表,
2 新节点插入链表的顺序不同,jdk7是插入链表头节点,jdk8因为要遍历链表把链表变成红黑树所以采用插入尾节点
3 hash算法简化
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
4 resize的逻辑修改,(jdk7 会出现死循环,jdk8不会,)(死锁场景http://www.importnew.com/22011.html)
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//这里建议你new hashMap的时候指定容量,指定的话就不会扩容了,在并发高德情况下你扩容很可能会引发死锁
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
4 resize的逻辑修改,(jdk7 会出现死循环,jdk8不会,)(死锁场景http://www.importnew.com/22011.html)
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
死锁的场景,因为它是从一个老的数组一到一个新的地方去之后,因为你那个元素并没有去复制,而是把引用给不同的数组,所以会出现死锁的场景
关于二进制跟&运算,扩展一道编程题
编程题:输入一个整数,输出该数二进制表示中1的个数,负数用补码表示:
public class Solution {
public int NumberOf1(int n) {
int count = 0;
while(n!= 0){
count++;
n = n & (n - 1);
}
return count;
}
}
分析一下代码: 这段小小的代码,很是巧妙。
如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。
举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。
每次 n & (n-1) 都会使得二进制数据的最右边的一个1变为0,所以有多少个1就会执行循环多少次,直至没有1(也就是0)