关闭

HashMap工作原理

标签: hashmap数据
78人阅读 评论(0) 收藏 举报
分类:

HashMap工作原理

大家都知道,链表容易进行增删操作而查询效率低,数组易查询单增删效率低;这样就诞生了我们的开发过程中经常用的HashMap,它结合了数组和链表的优点,查询和增删操作上都有着不错的效率。在对HashMap的理解上,我们可以把它看成是数据和链表的结合体,实际上它也是哈,其中我们最常用的就是它对应的get和put方法,下面我们就来探讨一下这两个方法原理。

这里写图片描述

get()方法

我们都知道HashMap是通过K-V进行存取数据的,首先我们可以想象有一个大小已知的数组,这个table中存放是HashMapEntry对象,然后每个这样HashMapEntry对象e又可能存在e.next(构成链表)不空的情况,这样的我们就在某个具体位置上的链表上进行遍历去找key对应的value。(其实我们可以设想一下这种极端的情况,假如说hash后都对应着同一个位置,这样的话不就变成一个链表了么,可惜实际上HashMap并不会这样)OK,直接撸源码吧,看一下究竟是怎么写的:

    /**
     * Returns the value of the mapping with the specified key.
     *
     * @param key
     *            the key.
     * @return the value of the mapping with the specified key, or {@code null}
     *         if no mapping for the specified key is found.
     */
    public V get(Object key) {
        if (key == null) {
            HashMapEntry<K, V> e = entryForNullKey;
            return e == null ? null : e.value;
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                return e.value;
            }
        }
        return null;
    }

从源码中,可以看到首先是对传入的key进行判空,然后通过Collections.secondaryHash(key)来得到该key对应的hash值,然后将这个hash table放在一个HashMapEntry类型的数组tab中,然后可以看到进行一个for循环,首先,根据hash & (tab.length - 1)找到tab数组中tab[hash & (tab.length - 1)]这个位置,这个过程也就是我们上面说的根据key的hashCode找到相应的位置了;在for的条件中有e = e.next这样的处理,也就说,这个for循环真正遍历的是tab数组中某个位置上的链表,而链表中的节点貌似又是HashMapEntry对象;进入for循环中,可以看到将链表中的e的key赋给eKey,然后对拿这个eKey去和get()方法中传入的key进行比较,if条件中的意思是,要么eKey和key有相同的引用、要么他们的值是相同的,如果满足这两个条件的额任何一个就认为找到了key对应的value,就将HashMapEntry对象中的value返回,这时就找到了key对应的value。

如果当前的e中的key不符合if中的条件,这时就会继续执行for循环执行e = e.next,也就是遍历到下一个数组节点,继续去判断是否符合;OK,get()方法就酱紫。

put()方法

经过上面get()方法的讲解,估计put()方法在理解起来也比价简单,所以不啰嗦了,直接上源码:

    /**
     * Maps the specified key to the specified value.
     *
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the value of any previous mapping with the specified key or
     *         {@code null} if there was no such mapping.
     */
    @Override public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);
        return null;
    }

可以看到它在存放value时候,也会使用同样的方法去把value放到key的hash相对应的位置,先找到tab[]上相应的位置,然后在该位置上的链表进行遍历,找到合适e.key并更新e.value。OK,存放的过程是比较简单和清楚的,但是其中涉及上一个非常重要的问题,那就是HashMap扩充容量的问题,接下来就来说一下其扩容的相关问题。

从源码中可以看到,if (size++ > threshold)这句,如果说当前的size大于threshold时就执行tab = doubleCapacity()这句进行扩容。先来看一下这个threshold,源码中给出的注释是:

 /**
     * The table is rehashed when its size exceeds this threshold.
     * The value of this field is generally .75 * capacity, except when
     * the capacity is zero, as described in the EMPTY_TABLE declaration
     * above.
     */
    private transient int threshold;   

就是说如果table的size超过threshold这个值时,这个table就需要进行重新hash,threshold这个值时capacity * 0.75,而这个capacity是HashMap的初始大小,可以查到capacity是16(这里就不讨论为什么是16了),也就说上面的size大于12就需要对进行扩容了。

接下来就看一下扩容的操作doubleCapacity()这个函数:

/**
     * Doubles the capacity of the hash table. Existing entries are placed in
     * the correct bucket on the enlarged table. If the current capacity is,
     * MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which
     * will be new unless we were already at MAXIMUM_CAPACITY.
     */
    private HashMapEntry<K, V>[] doubleCapacity() {
        HashMapEntry<K, V>[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            return oldTable;
        }
        int newCapacity = oldCapacity * 2;
        HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
        if (size == 0) {
            return newTable;
        }

        for (int j = 0; j < oldCapacity; j++) {
            /*
             * Rehash the bucket using the minimum number of field writes.
             * This is the most subtle and delicate code in the class.
             */
            HashMapEntry<K, V> e = oldTable[j];
            if (e == null) {
                continue;
            }
            int highBit = e.hash & oldCapacity;
            HashMapEntry<K, V> broken = null;
            newTable[j | highBit] = e;
            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
                int nextHighBit = n.hash & oldCapacity;
                if (nextHighBit != highBit) {
                    if (broken == null)
                        newTable[j | nextHighBit] = n;
                    else
                        broken.next = n;
                    broken = e;
                    highBit = nextHighBit;
                }
            }
            if (broken != null)
                broken.next = null;
        }
        return newTable;
    }

可以重点看方法的中前一部分的代码,首先如果需要扩容的HashMap的capacity达到了MAXIMUM_CAPACITY,貌似就认为达到最大了不进行扩充了。然后int newCapacity = oldCapacity * 2来获得新的table的容量,然后在通过调用makeTable(newCapacity)方法来创建新的table,OK就酱紫。

HashMap在一些应用中为何需要重写hashCode和equals方法

在上面的get(Object key)方法中可以看到,这个key是Object类型的,也就是说们HashMap中的key类型不确定哦,但是我们需要保证一点,相同的key的hash值必须相同,不同的key的hash值必须要不同;为了保证这种需求,我们在有些自定义类作为key时,我们需要在这个类中去重写hashCode和equals方法,具体可以参考:http://blog.csdn.net/ranmudaofa/article/details/39483605中的例子。或许有人疑问,为啥String或其他的基本类型作为Key时不需要,因为在这些引用或则基本类型中java源码已经帮我们做了。

OK,HashMap的相关介绍已经结束,求轻喷~

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:2476次
    • 积分:90
    • 等级:
    • 排名:千里之外
    • 原创:7篇
    • 转载:0篇
    • 译文:0篇
    • 评论:0条
    文章分类