Java 中 HashMap 的实现解析

转载请注明出处:http://blog.csdn.net/hjf_huangjinfu/article/details/63684337


        HashMap 作为一个散列表,基于 散列 的方式,实现一个 Map。下面看一下它在具体实现方面的一些点。

        备注:a^b 为 a的b次方。

 

1、基本实现方法

        HashMap 内部把每一对键值对封装成Node,然后以Node数组为主,Node链表为辅 来存放数据。根据每个元素的 hashcode 来计算出元素在数组中相应的存放位置(索引)。所以理论上可以使插入和查找的时间复杂度降低为 O(1)。

 

2、容量分配策略

        HashMap 内部的数组容量大小设定有一定的特点,那就是,都是 2^n (n >= 4 && n<= 30),数组容量的最小值,是 2^4,也就是 16。

/**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

 

3、Hash 策略(索引计算策略)

        Java期望每个对象都可以合理的重写自己的 hashCode 方法,然后用一个 int 类型的值来表示 hash 值。hash策略就是用来根据每个元素的 hashcode 来计算出元素在数组中相应的存放位置(索引),我们可以知道,int 的值范围是 -2^31 ~ 2^31-1,而 HashMap 内部数组的索引范围是 0 ~ size-1(size 是数组大小)。

转为2进制看起来会比较方便:

int 的范围:0x10000000 00000000 00000000 00000000 ~ 0x01111111 11111111 11111111 11111111

索引的范围:0 ~ 0x0...01...1,0和1的个数取决于数组的 size 大小。

 

HashMap 重新处理了每个对象的 hashCode,把高位2个字节保持不变,低位2个字节替换为高位两字节与低位2字节的异或后的值:

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

 

HashMap 计算元素在数组中的索引的方法是 直接取 处理后的 hash 的低 n 位(数组大小为 2^n)。

p = tab[i = (n - 1) & hash])

 

为什么要对原始的 hash 做处理?

        如果不这样做处理的话,那么假如有一个 hash 集合,他们的值低位2个字节不变,只变化高位2个字节的话(比如 0x1234ffff 和 0x5678ffff),并且集合大小 size < 2^16 ,这个集合中所有 hash 计算出来的地址的值就会一样,hash 冲突概率 100%,这样就等于没有使用 hash 的优点。处理是为了让高位2个字节也参与元素索引的计算,降低hash冲突的概率。

 

 

4、Hash冲突解决策略

        只要是使用 hash,不可避免的就会导致 hash 冲突(不同的值经过hash函数计算后,输出的值相同),HashMap 中采用了 拉链法(具有相同 hash 的元素会链接到上一个元素的结尾) 来解决 hash 冲突,。

        但是 HashMap 内部对此作了优化,当某一个 hash 冲突的元素数量达到 8 个 后,HashMap 内部会把这 8 个元素 的普通单向链表转化为一颗红黑树(插入、查找、删除的时间复杂度为 log2N )。这样在局部可以提升查找插入的性能。

/**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

当冲突元素数量逐渐降低,降为 6 的时候,HashMap 内部就会把 红黑树 转化为普通的单向链表。

/**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;


 

5、扩容时候的元素位置调整

        当容量不够用的时候,HashMap 内部会自动进行扩容操作,扩容规模为当前容量的 2 倍,也就是 size = size << 1; ,那么问题来了,并不是简单的创建一个新数组,然后把旧数据复制到新数组的相应位置就行了。因为这个位置是经过计算出来的,而数组的容量也是参与运算的元素之一,设想这样两个元素:

        元素1的 hash = 0b01101010 = 106;

        元素2的 hash = 0b00101010 = 42;

        数组容量   size= 0b01000000 = 64;

        元素1的 index = 0b01101010 &  0b00111111 = 42;

        元素2的  index = 0b00101010 &  0b00111111 = 42;

        此时,元素1 和 元素 2 的索引,计算出来都是 index = 0b00101010 = 42;

但是当数组扩容后:

        元素1的hash = 0b01101010 = 106;

        元素2的hash = 0b00101010 = 42;

        数组容量 size= 0b01000000 = 64;

        元素1的 index = 0b01101010 &  0b01111111 = 106;

        元素2的  index = 0b00101010 &  0b01111111 = 42;

所以就需要重组数据,重新调整数据的存储位置以及方式。但是每个数据只有2种情况,要么索引不变,要么索引就会向后偏移旧容量的大小(106 - 42 = 64)。

 

 

6、如何正确的使用HashMap

        一个正确的使用方式,可以最大限度的提升 HashMap 的性能。既然是基于 hash机制,那么 hash算法的性能就尤为重要,这里内部的索引计算方式不谈,我们把目光转移到元素的 hashCode 方法上面,毕竟大多数情况,对象的 hashCode 都是由开发者来覆写,那么如何高质量的覆写呢。

        先来看一下 hashCode 的方法说明:当两个对象 equals 的时候(逻辑上相等),hashCode 方法必须返回相同的值。但是,两个不 equals 的对象的 hashCode方法,并不是一定要返回两个不相同的值。但是我们要知道,这种情况下,返回不同的值,会提升基于 hash 机制的算法的效率。

        假如某个开发者贪图方便,hashCode 随笔一写,或者所有对象都返回相同的 hash 值,那么在使用 HashMap 的时候,你可能会发现,效率很低。效率已经从 O(1) 变为 O(log2n)。

 

因为,基于上述的分析结果,一个工作中的 HashMap,它的数据存储大概是这样的:

 

 

 

7、官方推荐的 hashCode 覆写方式

  @Override 
  public int hashCode() {
     // Start with a non-zero constant.
     int result = 17;

     // Include a hash for each field.
     result = 31 * result + (booleanField ? 1 : 0);

     result = 31 * result + byteField;
     result = 31 * result + charField;
     result = 31 * result + shortField;
     result = 31 * result + intField;

     result = 31 * result + (int) (longField ^ (longField >>> 32));

     result = 31 * result + Float.floatToIntBits(floatField);

     long doubleFieldBits = Double.doubleToLongBits(doubleField);
     result = 31 * result + (int) (doubleFieldBits ^ (doubleFieldBits >>> 32));

     result = 31 * result + Arrays.hashCode(arrayField);

     result = 31 * result + referenceField.hashCode();
     result = 31 * result +
         (nullableReferenceField == null ? 0
                                         : nullableReferenceField.hashCode());

     return result;
   }


 

 8、Iterator 遍历顺序

        HashMap 中 Iterator 的遍历顺序是乱序的,也就是不能保证按 插入顺序 输出集合中的元素。

        示例图如下:

        这里有一个 局部乱序 要提一下:就是说,在整体上遍历顺序是从元素数组的 0 索引开始,一直到数组结束。遇到单链表的情况时,也是顺着此单链表顺序遍历的,但是当 hash 冲突比较严重,导致有红黑树的时候,此时遍历顺序,并不是按照树的结构遍历(前序、中序、后序、层序),而是乱序的,因为在把单链转换为红黑树的时候,除了把元素组织成了树结构,他还用单链表组织了数据的关系(图中没有画出)。其实是按照那个单链表的顺序来遍历的。 

 

 单链转红黑树:
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
					//使用单链表来组织元素
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

遍历:
    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null){
		    throw new NoSuchElementException();	
		}   
		//如果在tab中遇到空元素,跳过,找到下一个非空元素,否则,按单链表顺序遍历
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }



 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值