zoukankan      html  css  js  c++  java
  • HashMap学习笔记

    hashmap的 扩容机制

    上一篇说了,hashmap的构造器里指明了两个对于理解HashMap比较重要的两个参数 int initialCapacity, float loadFactor,这两个参数会影响HashMap效率,HashMap底层采用的散列数组实现,利用initialCapacity这个参数我们可以设置这个数组的大小,也就是散列桶的数量,但是如果需要Map的数据过多,在不断的add之后,这些桶可能都会被占满,这是有两种策略,一种是不改变Capacity,因为即使桶占满了,我们还是可以利用每个桶附带的链表增加元素。但是这有个缺点,此时HaspMap就退化成为了LinkedList,使get和put方法的时间开销上升,这是就要采用另一种方法:增加Hash桶的数量,这样get和put的时间开销又回退到近于常数复杂度上。Hashmap就是采用的该方法。关于扩容,看hashmap的扩容方法,resize方法,它的源码如下:

    //扩容方法
    // 重新调整HashMap的大小,newCapacity是调整后的单位
        void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
    
            // 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,
            // 然后,将“新HashMap”赋值给“旧HashMap”。
            Entry[] newTable = new Entry[newCapacity];
            transfer(newTable);
            table = newTable;
            threshold = (int) (newCapacity * loadFactor);
        }

    很明显,是从新建了一个HashMap的底层数组,长度为原来的两倍,而后调用transfer方法,将旧HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。transfer方法的源码如下:

    // 将HashMap中的全部元素都添加到newTable中
        void transfer(Entry[] newTable) {
            Entry[] src = table;
            int newCapacity = newTable.length;
            for (int j = 0; j < src.length; j++) {
                Entry<K, V> e = src[j];
                if (e != null) {
                    src[j] = null;
                    do {
                        Entry<K, V> next = e.next;
                        int i = indexFor(e.hash, newCapacity);
                        e.next = newTable[i];
                        newTable[i] = e;
                        e = next;
                    } while (e != null);
                }
            }
        }

    很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。

    hashmap什么时候需要增加容量呢?

    因为效率问题,JDK采用预处理法,这时前面说的loadFactor就派上了用场,当size > initialCapacity * loadFactor,hashmap内部resize方法就被调用,使得重新扩充hash桶的数量,在目前的实现中,是增加一倍,这样就保证当你真正想put新的元素时效率不会明显下降。所以一般情况下HashMap并不存在键值放满的情况。当然并不排除极端情况,比如设置的JVM内存用完了,或者这个HashMap的Capacity已经达到了MAXIMUM_CAPACITY(目前的实现是2^30)。

    initialCapacity和loadFactor参数设什么样的值好呢?

    initialCapacity的默认值是16,有些人可能会想如果内存足够,是不是可以将initialCapacity设大一些,即使用不了这么大,就可避免扩容导致的效率的下降,反正无论initialCapacity大小,我们使用的get和put方法都是常数复杂度的。这么说没什么不对,但是可能会忽略一点,实际的程序可能不仅仅使用get和put方法,也有可能使用迭代器,如initialCapacity容量较大,那么会使迭代器效率降低。所以理想的情况还是在使用HashMap前估计一下数据量。

    加载因子默认值是0.75,是JDK权衡时间和空间效率之后得到的一个相对优良的数值。如果这个值过大,虽然空间利用率是高了,但是对于HashMap中的一些方法的效率就下降了,包括get和put方法,会导致每个hash桶所附加的链表增长,影响存取效率。如果比较小,除了导致空间利用率较低外没有什么坏处,只要有的是内存,毕竟现在大多数人把时间看的比空间重要。但是实际中还是很少有人会将这个值设置的低于0.5。

    HashMap的key和value都能为null么?如果k能为null,那么它是怎么样查找值的?

    如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。记住,key为null的键值对永远都放在以table[0]为头结点的链表中。

    HashMap中put值的时候如果发生了冲突,是怎么处理的?

    JDK使用了链地址法,hash表的每个元素又分别链接着一个单链表,元素为头结点,如果不同的key映射到了相同的下标,那么就使用头插法,插入到该元素对应的链表。

    HashMap的key是如何散列到hash表的?相比较HashTable有什么改进?

    我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除留余数法),HashTable就是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,且hashtable直接使用了hashcode值,没有重新计算。

    HashMap中则通过 h&(length-1) 的方法来代替取模,其中h是key的hash值,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。

    接下来,我们分析下为什么哈希表的容量一定要是2的整数次幂。

    首先,length为2的整数次幂的话,h&(length-1) 在数学上就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;

    其次,length为2的整数次幂的话,则一定为偶数,那么 length-1 一定为奇数,奇数的二进制的最后一位是1,这样便保证了 h&(length-1) 的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀,而如果length为奇数的话,很明显 length-1 为偶数,它的最后一位是0,这样 h&(length-1) 的最后一位肯定为0,即只能为偶数,这样导致了任何hash值都只会被散列到数组的偶数下标位置上,浪费了一半的空间,因此length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

    作为对比,再讨论一下Hashtable

    HashTable同样是基于哈希表实现的,其实类似HashMap,只不过有些区别,HashTable同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

    HashTable比较古老, 是JDK1.0就引入的类,而HashMap 是 1.2 引进的 Map 的一个实现。

    HashTable 是线程安全的,能用于多线程环境中。Hashtable同样也实现了Serializable接口,支持序列化,也实现了Cloneable接口,能被克隆。

    Hashtable继承于Dictionary类,实现了Map接口。Dictionary是声明了操作”键值对”函数接口的抽象类。 有一点注意,HashTable除了线程安全之外(其实是直接在方法上增加了synchronized关键字,比较古老,落后,低效的同步方式),还有就是它的key、value都不为null。另外Hashtable 也有 初始容量 和 加载因子

    public Hashtable() {
        this(11, 0.75f);
    }

    默认加载因子也是 0.75,HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。因为HashTable是直接使用除留余数法定位地址。且Hashtable计算hash值,直接用key的hashCode()。

    还要注意:前面说了Hashtable中key和value都不允许为null,而HashMap中key和value都允许为null(key只能有一个为null,而value则可以有多个为null)。但如在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。

    最后针对扩容:Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

    HashMap源码学习总结:参考http://blog.csdn.net/melod_bc/article/details/53864376

  • 相关阅读:
    第15周作业
    软件工程结课作业
    第13次作业--邮箱的正则表达式
    第12次作业
    第10次作业
    Java 8 新的时间日期库
    你还在用if-else吗?
    Java并发编程:4种线程池和缓冲队列BlockingQueue
    ZooKeeper学习第八期——ZooKeeper伸缩性(转)
    ZooKeeper学习第七期--ZooKeeper一致性原理(转)
  • 原文地址:https://www.cnblogs.com/lchzls/p/6716122.html
Copyright © 2011-2022 走看看