JAVA源码解读---HashMap目录扩展的奥秘

摘要:为了探索JAVA1.7源码中HashMap类数据的组织方法与目录扩展方法,本文通过对JAVA1.7源码中HashMap类源码的阅读与分析,得出结论:hashmap中存储数据的数据结构采用的是链表数组,目录是个数组,数组的成员是链表。冲突解决方法:典型的链地址法,冲突后,在链表头部插入数据。目录扩展方法:已二倍的方式扩展,一直到目录的最大上限。目录扩展的触发条件:装载因子的方式触发。从java中hashmap的实现可以看出,桶数据的组织方式并不是一种非常高效的方式。对检索效率不利。同时,数据扩展简单的采用二倍的扩展方法,也只是使用了最为粗暴的扩展方式,扩展开销较大。

关键字:JAVA,HashMap,目录组织方式,目录扩展方法,目录触发条件

本文转自http://blog.csdn.net/daliaojie/article/details/26236979

散列是一种非常重要的数据结构,在JAVA与dotNet中都有相对应事先的类供调用。我们知道hashmap的容量是动态增长的,此篇博客分析了java中,hashmap中关于目录扩展的过程。

先看hashmap的成员变量:

 static final int DEFAULT_INITIAL_CAPACITY = 16;

    static final int MAXIMUM_CAPACITY = 1 << 30;

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    transient Entry[] table;

    transient int size;

    int threshold;

    final float loadFactor;

    transient int modCount;

其中

    transient Entry[] table;

这便是,hashmap中的散列的目录,可以看出,他是一个数组,里面存储了类。其他成员有,默认的目录长度

DEFAULT_INITIAL_CAPACITY。默认的目录最大长度:

static final int MAXIMUM_CAPACITY = 1 << 30;

默认的填充因子:

static final float DEFAULT_LOAD_FACTOR = 0.75f;

当然这些变量可以由构造函数初始化。

对应的成员变量为:

    transient int size;

    int threshold;

    final float loadFactor;

填充因子的意义为,达到这个比例后,目录需要扩张。为了探索,hash目录扩张的秘密:

我们从put操作看起,其实就是insert操作。

对了, 我们们的目录结构还没看完:

transient Entry[] table;

我们看Entry是如何定义的:

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

很明显, 目录中的桶采用的数据组织方法为链表。

也就是说。hash的冲突解决方法为链地址法。

关于散列中解决冲突的方法连地址法的详细介绍可以自己百度。

直接看insert过程吧。

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

put操作的过程:

将key进行散列并计算出目录中对应的位置。从而获取该位置处的链表。并对该链表进行遍历,遍历时,检查是否该key已经存在,如果存在就用将新的value替换掉旧的value。

如果不存在。就调用

 addEntry(hash, key, value, i);

进行插入。并且是在改目录位置处插入。

我们看插入操作做了些什么:

    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

方法的刚开始,获取了链表。

并头早了一个新的Entry,我们看对应的构造方法:

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

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

这个Entry的构造函数很明白了。

table[bucketIndex] = new Entry<>(hash, key, value, e);

实现了在bucktIndex处的连标的头部插入key-value的链表节点。明显这个插入过程还是比较高效的。

我们继续往后看。

addEntry(int hash, K key, V value, int bucketIndex)

该方法,在链表的头部插入新的节点后判断了当前size++与目录扩展阀值的关系。当达到分裂阀值后,执行

 resize(2 * table.length);

操作。我们看它对应的方法。

    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];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

方法传入了扩展后的目录长度。我们知道,目录是以2倍的方式,进行扩展的。并且最大有个限制,这里默认是1 <<30

resize方法里,先对新的目录长度进行检测,以防止,超过目录的最大长度。

然后new了一个新长度的目录。

然后执行

transfer(newTable);

源码如下:

    /**
     * Transfers all entries from current table to 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);
            }
        }
    }

方法的英文解释其实很明白了。他们将源目录中的所有元素进行遍历,经过对新目录长度的散列后,放进去了新的目录中。

解读一下吧。

for()是对原目录的遍历控制。

然后do while是对目录位置处的链表进行遍历时的控制。

目录扩展后,便将就目录更换为新的目录。并更新了目录扩展的阀值。

我们看一下map的查询操作:

 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

查询过程还是容易看的:首先计算key对应的目录位置,然后目录位置处对应的链表中,查询key是否存在。

做一下总结:

hashmap中存储数据的数据结构:链表数组。目录是个数组,数组的成员是链表。

冲突解决方法:典型的链地址法,冲突后,在链表头部插入数据。

目录扩展方法:已二倍的方式扩展,一直到目录的最大上限。

目录扩展的触发条件:装载因子的方式触发。

尾语:

从java中hashmap的实现可以看出,桶数据的组织方式并不是一种非常高效的方式。对检索效率不利。同时,数据扩展简单的采用二倍的扩展方法,也只是使用了最为粗暴的扩展方式。扩展开销较大。

JAVA源码解读---HashMap目录扩展的奥秘,布布扣,bubuko.com

时间: 2024-10-05 05:01:53

JAVA源码解读---HashMap目录扩展的奥秘的相关文章

dotNet源码解读--HashTable目录扩展的奥秘

摘要:为了探索dotnet中hashtable的目录结构及与目录扩展相关的算法,本文通过对相关源码的阅读与分析,得出如下结论,hashtable的目录是由数组组织,目录元素代表一个数据节点,不是数据桶.目录扩展是扩展当前目录长度2倍往1遍历过程中遇到的第一个素数.目录扩展触发条件:装载因子式的触发,同时考虑到"杂乱程度"需要进行重新散列.目录扩展时需要遍历原有目录中所有的元素.查询过程与探测再散列类似. 关键词:dotnet,hashmap,目录扩展方法,目录扩展触发条件 一.目录结构

jdk1.8.0_45源码解读——HashMap的实现

jdk1.8.0_45源码解读——HashMap的实现 一.HashMap概述 HashMap是基于哈希表的Map接口实现的,此实现提供所有可选的映射操作.存储的是<key,value>对的映射,允许多个null值和一个null键.但此类不保证映射的顺序,特别是它不保证该顺序恒久不变.  除了HashMap是非同步以及允许使用null外,HashMap 类与 Hashtable大致相同. 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能.迭代col

java源码解读--queue

queue接口特点:可以模拟队列行为,即"先进先出". 接口结构 queue接口继承了Collection接口,并增加了一些新方法 12345678910111213141516 public interface <E> extends Collection<E>{ boolean add(E e); //将元素插入队列,如果失败返回false boolean offer(E e); //移除并返回队列中的第一个元素,队列为空时,抛异常 E remove();

Java源码解析|HashMap的前世今生

HashMap的前世今生 Java8在Java7的基础上,做了一些改进和优化. 底层数据结构和实现方法上,HashMap几乎重写了一套 所有的集合都新增了函数式的方法,比如说forEach,也新增了很多好用的函数. 前世--Java 1.7 底层数据结构 数组 + 链表 在Java1.7中HashMap使用数组+链表来作为存储结构 数组就类似一个个桶构成的容器,链表用来解决冲突,当出现冲突时,就找到当前数据应该存储的桶的位置(数组下标),在当前桶中插入新链表结点. 如下图所示: 链表结点中存放(

java源码之HashMap和HashTable的异同

代码版本 JDK每一版本都在改进.本文讨论的HashMap和HashTable基于JDK 1.7.0_67 1. 时间 HashTable产生于JDK 1.1,而HashMap产生于JDK 1.2.从时间的维度上来看,HashMap要比HashTable出现得晚一些. 2. 作者 以下是HashTable的作者: 以下代码及注释来自java.util.HashTable * @author Arthur van Hoff * @author Josh Bloch * @author Neal G

Java源码解读系列(一):ArrayList

本文简单介绍了 ArrayList,并对扩容,添加,删除操作的源代码做分析.能力有限,欢迎指正. ArrayList是什么? ArrayList 就是数组列表,主要用来装载数据.底层实现是数组 Object[] elementData,当我们装载的是基本数据类型 int, long, boolean, shot...的时候我们只能存储他们对应的包装类型. 与它类似的是 LinkedList,和 LinkedList 相比,它的查找和访问元素的速度较快,但新增,删除的速度较慢. 线程安全吗? 线程

源码解读—HashTable

在上一篇学习过HashMap(源码解读—HashMap)之后对hashTable也产生了兴趣,随即便把hashTable的源码看了一下.和hashMap类似,但是也有不同之处. public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable  实现接口:Map,Cloneable,Serializable 继承自Diction

从Java源码的角度来分析HashMap与HashTable的区别

由于HashMap与HashTable都是用来存储Key-Value的键值对,所以经常拿来对比二者的区别,下面就从源码的角度来分析一下HashMap与HashTable的区别, 首先介绍一下两者的区别,然后再从源码分析. HahMap与HahTable两者主要区别: 1.继承的父类不同 <span style="font-size:18px;">public class HashMap<K, V> extends AbstractMap<K, V>

Java之ArrayList源码解读(JDK 1.8)

java.util.ArrayList 详细注释了ArrayList的实现,基于JDK 1.8 . 迭代器SubList部分未详细解释,会放到其他源码解读里面.此处重点关注ArrayList本身实现. 没有采用标准的注释,并适当调整了代码的缩进以方便介绍 import java.util.AbstractList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.