HashMap

1.8JDK
几个重要的属性:

//默认装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表化为树的阈值
static final int TREEIFY_THRESHOLD = 8;

内部Node节点中重要属性:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

    }

一些公共属性:

 //Node类型的数组
 transient Node<K,V>[] table;
 //阈值
 int threshold;
 //装载因子
 final float loadFactor;

公共的操作:
两个构造方法:

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

若我们再创建HashMap对象时没有指定容量,则默认容量为0,默认装载因子为0.75
否则容量就按我们指定的容量去赋值

计算key的hash值的方法:

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

此方法为取key的hash值,若key为null则hash值为0;否则先调用Object的hashCode方法计算出key的hashCode记为h,然后将h右移16位,并将两次得到的结果通过异或计算得出最终结果

计算HashMap表长/初始阈值:

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

这个方法传入的参数cap为HashMap的容量,这里分三种情况:
1、 若cap为1,返回值为1;
2、 若cap为不为2的n次方返回比它大的最小的2的n次方;
3、 若cap本身就是2的n次方返回的是它本身
首先将cap-1,这一步主要是为了解决cap本身就是2的n次方这个问题而存在的,然后将所得的值右移1位并与它本身按位或,然后右移两位再与所得值按位或…最后将所得值加1得到比cap大的最小2的n次方

下面是基本的增删查改:
HashMap说白了就是一个数组+链表+树的数据结构,数组用来存放键值对,链表用来解决冲突问题,红黑树用来解决效率问题。

增:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

具体实现方法就不贴出来了
1、首先会通过键值对中的key计算出hash值
2、若数组为空或数组长度为0,则先进行扩容操作
3、通过除留余数法算出键值对在数组中的位置i = (n - 1) & hash
4、若此位置没有元素则先将此键值对包装成Node节点,并直接将其插入到此位置;若此位置有元素并且此元素的键key和要插入元素的键key相等,则用新值覆盖旧值,并将旧值返回;否则一直遍历此位置的链表,直到最后,然后将要插入的元素插入到链表的最后
5、树的情况暂不讨论

删:

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

1、需要我们给出要删除元素的键key,通过此键计算出hash值
2、若数组长度为0或HashMap为空,或者位置index = (n - 1) & hash上的元素为空,返回null
3、定位到hash值和key都相等的元素,若此元素所在链表没有其它元素,则删除它,否则删除它之后,用它的后继节点来占据它的位置

查:

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

1、需要我们给出要删除元素的键key,通过此键计算出hash值
2、若数组长度为0或HashMap为空,或者位置index = (n - 1) & hash上的元素为空,返回null
3、定位到hash值和key都相等的元素,返回

一些讨论性的话题:
HashMap中的树:
当链表长度大于8的时候将链表转换为红黑树

HashMap对 “null” 的处理:
先说结论:
键值都可以为null
当插入元素的键key为空时,

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

可以看到,key为空时计算出的hash值为0。因此计算此元素在数组中位置的表达式i = (n - 1) & hash得出来的结果也为0。当我们将一个键key为null的元素插入到HashMap中时,实际上时将此元素插入到了数组中下标为0的位置上
而对值value没有什么特别的处理

计算hash值的方法:

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

取key的hash值,若key为null则hash值为0;否则先调用Object的hashCode方法计算出key的hashCode记为h,然后将h右移16位,并将两次得到的结果通过异或计算得出最终结果
为什么要让y右移16位在异或呢?
定位元素在HashMap中位置时用的是除留余数法,因为表长n的二进制位数有限,所以就算hash值的二进制位数再长,我们用到的也只是它的低位,用不到它的高位;而将hash值的二进制右移16位后,它的低16位没了,留下来的是它的17位开始往上的高位数,这样用hash值的高位与低位相异或,就能良好的利用了hash值所有位置上的数,并且增加了hash值的随机性,从而达到一定的减小冲突作用

负载因子:
负载因子为0.75。如果是0.5, 那么每次达到容量的一半就进行扩容,空间利用率低下,并且执行resize操作的频率会增加,但是冲突的几率减少了; 如果是1,那意味着每次数组空间使用完毕才扩容,空间利用率增加了,但是冲突的几率也相应增加。因此,取0.75也算是一种折中,这个数字是对空间和时间相对都比较友好的一个值。(服从泊松分布,只不过不是我们讨论的话题)

扩容/容量/定位:
当hashMap的数组长度到了一个临界值就会扩容,把所有元素rehash再放到新的HashMap中,容量为之前的2倍
为什么容量要是之前容量的2倍呢?
在构造HashMap后,我们会将其初始阈值用tableSizeFor这个方法调整为2的n次方,并且再第一次扩容时,会将此阈值赋给新容量newCap让HashMap的容量也为2的n次方。计算键值对在数组中位置的时候,我们会使用的是除留余数法,即通过表达式i = (n - 1) & hash来计算。n代指数组容量,hash为键key的hash值。因为初始容量为2的n次方,所以初始n-1得到的二进制数一定是这样的0111111…,即n-1的二进制中除第一位为0后面位置上的数都为1,这样n-1和hash值相与出来的结果即为hash%n的结果,即取余数;若n不为2的n次方,得出来的结果就不是两者的余数了,除留余数法也就不成立了,并且倘若容量为11,n-1取二进制得到1010,来看一下不同的hash值与其相与的结果:
1010 & 101010100101001001000 结果:1000 = 8
1010 & 101000101101001001001 结果:1000 = 8
1010 & 101010101101101001111 结果: 1010 = 8
1010 & 101100100111001101100 结果: 1000 = 8
可以看到,因为由0的存在,hash值相应位置上不管是0还是1和它相与出来的结果都为0,这增加了hash冲突,tab[8]这个位置上的链表长度也相应增加,对于相应位置键值对的增删查速度也会有所影响

扩容方法就不贴出来了,看具体流程:
具体的扩容流程:
1、 若我们一开始创建HashMap对象时没有给定初始容量,即这样构造:

HashMap map = new HashMap();

HashMap内部首先会加载一个装载因子值为0.75f,并且暂时不为容量Cap和阈值Thr赋值,让它们各自拥有默认初值0
接下来我们使用put方法插入一条数据。跟踪进去,HashMap首先会进行一个判断:若Hash表长为0或Hash表为空则会走到resize方法,并将Cap设置为16,Thr设置为16*0.75 = 12;否则直接插入数据。显然,这里首先走到resize方法进行了扩容
接着我们连续插入11个数据,都不会扩容(假定无冲突)
再插入一条数据,现在数组中容量达到了阈值。首先会将数据插入到当前HashMap中,插入完毕,在结束put方法前会判断当前表长size是否超过了Thr,若超过则进行扩容。新Cap为之前的两倍32,Thr也为之前两倍24.

2、 开始创建HashMap对象时给定初始容量,即这样构造:

HashMap map = new HashMap(1);
这里传入的初始容量为1

首先调用HashMap的有参构造方法将默认装载因子0.75f和指定容量Cap = 1传入其中,并算出阈值Thr = 1
接下来插入一个元素,因为HashMap表长为0并且HashMap也为空,所以先进行resize操作,算出newCap = 1,newThr = newCap * loadFactor =0.75,然后插入元素。在put方法结束前判断当前表长size是否超过了newThr,显然size为1大于newThr,所以再次resize,将newCap赋值为2,newThr赋值为1.5
… …

3、 初始容量不为2的n次方

HashMap map = new HashMap(3);

首先调用HashMap的有参构造方法将默认装载因子0.75f和指定容量Cap = 3传入其中,然后调用tableSizeFor方法算出阈值Thr=4
接下来插入一个元素,因为HashMap表长为0并且HashMap也为空,所以先进行resize操作。在这里HashMap会将旧阈值Thr=4赋给新的容量Cap,即newCap = oldThr = 1,newThr = newCap * loadFactor =3,然后插入元素。在put方法结束前判断当前表长size是否超过了newThr,显然不超过
接下来再插入3条.再第3条数据插入完毕后进行容量判断时发现表中数据有4条,而阈值为3,所以进行扩容,结果newCap = 8,newThr = 6

画一幅图
假设我们将初始容量赋值为1
在这里插入图片描述
每次都要等待表中数组个数超出阈值以后才扩容

总结:
不管有没有为它赋初值,HashMap在第一次使用之前(即第一次插入元素之前)会扩容,
1、 倘若没有为它赋初始容量,HashMap默认容量为0,在第一次使用之前它会被赋予newCap=16,newThr=12
2、 倘若为它赋初始容量1,使用tableSizeFor方法算出来的值任然是1,阈值为0.75;插入第一个元素后会进行二次扩容,newCap=2,newThr=1.5
3、 若初始容量是其它2的n次方以外的数,第一次插入元素之前会将表的容量增加到大于初始容量的最小的2的n次方
4、 若初始容量为2的n次方,则它就会使用这个容量值
每次插入元素后,HashMap会判断表中元素总数是否超过阈值,若超过,则进行扩容,并且HashMap总会等到表中元素总数超过了阈值以后才会扩容
扩容是将容量和阈值都变为之前的两倍

重写hashCode/equals的意义:
equals:
Object的equals方法判断的是两者内存地址是否一致(即源码是用“==”来实现的),我们可以直接用 “ = = ”来代替。很多情况下,我们都只需要判断两者的字面量是否相等,所以我们需要重写equals方法去判断两者的字面量大小
例如String的equals方法

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

String的equals方法是通过逐步比较两个字符串中每个字符是否相等来得出结果的。
我们来做个测试:

public class Test{
	    
	    public int hash(Object key) {
	    	int h;
	    	return (h = key.hashCode()) ^ (h >>> 16);
	    }
	    
	    public static void main(String[] args){
	    	Testtest = new Test();
	    	String a = "abc";
	    	String b = new String("abc");
	    	System.out.println(test.hash(a));
	    	System.out.println(test.hash(b));
	    	System.out.println(a == b);
	    	System.out.println(a.equals(b));
	    }
}

结果:

96355
96355
false
true

假设我们将上述两个字符串a和b作为key插入HashMap,首先它们用hash方法算出来的值是相等的,即两者在同一个槽位上,然后我们肯定是想用b的值来覆盖a的值,因为a、b两者字符串的字面量是相等的,都为“abc”,我们期望使用equals方法来判断,得出结果为true以方便b来覆盖a。倘若我们不重写equals方法,默认调用的就是Object的equals方法,比较的是两个对象在内存中的地址,返回的是false,很明显与我们的与其不符
所以我们需要重写equals方法,让其来判断两个String对象的字面量是否相等

重写equals的意义:
HashMap中比较两个key是否相等用的是两个key所属类的equals方法,如String、Integer等

意义就是处理重复键key(HashMap的put方法中比较两个key是否相等使用的就是equals和hashCode,并且key相同就会用新value覆盖旧value,否则会再次往后定位)

hashCode:
首先,重写hashCode的第一个意义当然是减少冲突
使用hashCode的高16位和低16位相异或,增加了hashCode的随机性,在一定程度上可以起到减少冲突的作用,上面有介绍
这里,我们的重点暂时不放在有关冲突的话题上。

数组中元素的hashCode值是通过key来计算的。我们在向HashMap中插入元素的时候,首先需要通过key计算出它的hashCode值并且通过此值来定位到这个元素在数组中的位置i。
所以hashCode的第一个作用是定位
接下来若此位置无元素,直接插入;若此位置有元素,则使用equals比较此位置中元素的key和要插入元素的key是否相等(如上述equals的使用),若相等,则用新value覆盖旧value;不等,往链表后面搜索合适位置再插入。
所以重写hashCode的意义:
1、减少冲突;
2、定位

HashMap中put方法通过hashCode和equals方法达到了减少冲突、定位和处理重复键key的功能

多线程环境下,HashMap1.7扩容会产生的问题:
1.7resize关键代码:

1、   void transfer(Entry[] newTable, boolean rehash) {
2、      int newCapacity = newTable.length;
3、       for (Entry<K,V> e : table) { //这里才是问题出现的关键..
4、            while(null != e) {
5、                Entry<K,V> next = e.next;  //寻找到下一个节点..
6、                if (rehash) {
7、                    e.hash = null == e.key ? 0 : hash(e.key);
8、                }
9、                int i = indexFor(e.hash, newCapacity);  //重新获取hashcode
10、                e.next = newTable[i];  
11、                newTable[i] = e;
12、                e = next;
13、            }
14、        }
15、    }

1.7的resize方法为头插
首先我们构造一个HashMap并赋给它初始容量Cap = 2,阈值Thr = 1.5
我们有两个线程T1和T2,两个线程都欲往里面插入数据
下图是初始状态
在这里插入图片描述假设线程T1执行到上述代码第5行后被挂起
下图是挂起后的标记状态
在这里插入图片描述接下来线程T2向里面插入一条数据后扩容
扩容完毕后所有元素都被放入新的集合里,下图左图为容量Cap = 4的线程T2的新数组
因为T1被挂起,所以标记e和next的指向都没变
为便于观察第三条数据就不画出来了
在这里插入图片描述下一步T1开始执行
元素A,1被标记为e的next
下图右图为容量为Cap = 4的线程T1的新数组
在这里插入图片描述接下来上图左边T2的新数组将它的元素全部插入右边T1新数组中
在这里插入图片描述接下来执行第10行代码e.next = newTable[i]
在这里插入图片描述

遍历:
以下代码公用部分:

HashMap<String, Integer> map = new HashMap<String, Integer>();
		map.put("A", 1);
		map.put("B", 2);
		map.put("C", 3);
		map.put("D", 4);

一、

for (String key : map.keySet()) {
     System.out.println(map.get(key));
}

较下列三种遍历方式,性能较差
二、

for (Entry<String, Integer> entry : map.entrySet()) {
		System.out.print(entry.getKey() + "  ");
		System.out.print(entry.getValue());
		System.out.println();
}

最好用这种方法
三、

Set<Entry<String, Integer>> entrySet = map.entrySet();
		for (Entry<String, Integer> entry : entrySet) {
		System.out.print(entry.getKey() + "  ");
		System.out.print(entry.getValue());
		System.out.println();
		}

四、

Iterator<HashMap.Entry<String, Integer>> iterator = map.entrySet().iterator();
		while (iterator.hasNext()) {
			HashMap.Entry<String, Integer> entry = iterator.next();
			System.out.print(entry.getKey() + "  ");
			System.out.print(entry.getValue());
			System.out.println();
		}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值