HashMap

  HashMap是一个实现了Map接口的Hash表。提供所有Map的操作,并且允许null
key和null value。HashMap几乎等同于HashTable,只不过HashMap不是线程安全的。

  如果hash函数将元素在buckets之间合适的散列的话,HashMap能提供常量时间的get和put操作性能。迭代操作所需时间与HashMap的capacity成正比,因此在需要快速迭代的场合,就不应该将初始capacity设置的很大。

  有两个参数会影响HashMap的性能:初始capacity,加载因子factor。capacity是hash表的buckets的数目,factor指定hash表达到怎样的容量时capacity会自动增加。当hash表的entries数目超过了hash表的factor和capacity,hash表将会被rehashed,内部数据结构会被重建,hash表会增加至将近buckets的两倍。

  一般情况下,基于时间和空间上的考虑,factor=0.75,值太大会减少空间但会增加查找时间。如果有大量的映射需要存储到HashMap中去,创建一个具有较大值的capacity的HashMap在性能上优于HashMap自己自动增长Hash表。

  当多个key具有相同的hashCode(),也会影响性能。当key是Comparable时,为了解决冲突,会对这些key使用比较方法。

哈希表

   数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。 哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。 

哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法—— 拉链法,我们可以理解为“链表的数组” ,如图:

  从上图我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。

  HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。

  首先HashMap里面实现一个静态内部类Entry,其重要的属性有
key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */

transient Entry[] table;

HashMap的存取实现

既然是线性数组,为什么能随机存取?这里HashMap用了一个小算法,大致是这样实现:

// 存储时:
int hash
= key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值
int index
= hash % Entry[].length;
Entry[index] = value;

// 取值时:
int hash
= key.hashCode();
int index
= hash % Entry[].length;
return Entry[index];

  

put

疑问:如果两个key通过hash%Entry[].length得到的index相同,会不会有覆盖的危险?

  这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方,
第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] =
A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现,我们应该已经清楚了。

 public V put(K key, V value) {

        if (key == null)

            return putForNullKey(value); //null总是放在数组的第一个链表中

        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;

    }

void int hash, K key, V value, int bucketIndex) {

    Entry<K,V> e = table[bucketIndex];

    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);     //如果size超过threshold,则扩充table大小。再散列

    if (size++ >= threshold)

            resize(2 * table.length);

}

当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个index的链就会很长,会不会影响性能?HashMap里面设置一个因子,随着map的size越来越大,Entry[]会以一定的规则加长长度。

get

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;

}

null key的存取

 null key总是存放在Entry[]数组的第一个元素。

private V putForNullKey(V value) {

        for (Entry<K,V> e = null; e = e.next) {

            if (e.key == null) {

                V oldValue = e.value;

                e.value = value;

                e.recordAccess(this);

                return oldValue;

            }

        }

        modCount++;

        addEntry(0, null, value, 0);

        return null;

    }

    private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}

确定数组index:hashcode % table.length取模

HashMap存取时,都需要计算当前key应该对应Entry[]数组哪个元素,即计算数组下标;算法如下:

 /**

     * Returns index for hash code h.

     */

    static int int h, int length) {

        return h & (length-1);

    }

按位取并,作用上相当于取模mod或者取余%。
这意味着数组下标相同,并不表示hashCode相同。

table初始大小

  public HashMap(int initialCapacity, float loadFactor) {

        .....         // Find a power of 2 >= initialCapacity

        int capacity = 1;

       while (capacity < initialCapacity)

            capacity <<= 1;

        this.loadFactor = loadFactor;

        threshold = (int)(capacity * loadFactor);

        new Entry[capacity];

        init();

    }

注意table初始大小并不是构造函数中的initialCapacity!!而是 >= initialCapacity的2的n次幂!!!!

  ————为什么这么设计呢?——

解决hash冲突的办法

    1. 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
    2. 再哈希法
    3. 链地址法
    4. 建立一个公共溢出区

java中hashmap的解决办法就是采用的链地址法。

再散列rehash过程

当哈希表的容量超过默认容量时,必须调整table的大小。当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一张新表,将原表的映射到新表中。

 /**

     * Rehashes the contents of this map into a new array with a

     * larger capacity.  This method is called automatically when the

     * number of keys in this map reaches its threshold.

     *

     * If current capacity is MAXIMUM_CAPACITY, this method does not

     * resize the map, but sets threshold to Integer.MAX_VALUE.

     * This has the effect of preventing future calls.

     *

     * @param newCapacity the new capacity, MUST be a power of two;

     *        must be greater than current capacity unless current

     *        capacity is MAXIMUM_CAPACITY (in which case value

     *        is irrelevant).

     */

    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);

    }

    /**

     * 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;

                    //重新计算index

                    int i = indexFor(e.hash, newCapacity);

                    e.next = newTable[i];

                    newTable[i] = e;

                    e = next;

                } while (e != null);

            }

        }

    }

Hashtable

  功能几乎等同于HashMap,主要有以下三个区别:

  1.Hashtable继承于Dictionary,HashMap继承于AbstractMap,两个类都实现了Map<K,V>, Cloneable, Serializable方法

  2.Hashtable是线程安全的,HashMap不是线程安全的

  3.HashMap允许null key和null value,Hashtable不允许

HashMap和HashTable源码分析的更多相关文章

  1. 并发-HashMap和HashTable源码分析

    HashMap和HashTable源码分析 参考: https://blog.csdn.net/luanlouis/article/details/41576373 http://www.cnblog ...

  2. Java入门系列之集合Hashtable源码分析(十一)

    前言 上一节我们实现了散列算法并对冲突解决我们使用了开放地址法和链地址法两种方式,本节我们来详细分析源码,看看源码中对于冲突是使用的哪一种方式以及对比我们所实现的,有哪些可以进行改造的地方. Hash ...

  3. 史上最简单的的HashTable源码分析

    HashTable源码分析 1.前言 Hashtable 一个元老级的集合类,早在 JDK 1.0 就诞生了 1.1.摘要 在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap.Link ...

  4. HashMap原理及源码分析

    HashMap 原理及源码分析 1. 存储结构 HashMap 内部是由 Node 类型的数组实现的.Node 包含着键值对,内部有四个字段,从 next 字段我们可以看出,Node 是一个链表.即数 ...

  5. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

  6. (转)HashMap和HashTable源码

    转自: http://www.cnblogs.com/ITtangtang/p/3948406.html http://frankfan915.iteye.com/blog/1152091 一.Has ...

  7. HashMap:从源码分析到面试题

    1 HashMap简介 HashMap是实现map接口的一个重要实现类,在我们无论是日常还是面试,以及工作中都是一个经常用到角色.它的结构如下: 它的底层是用我们的哈希表和红黑树组成的.所以我们在学习 ...

  8. JDK1.8 HashMap中put源码分析

    一.存储结构      在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构.它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置.Hash ...

  9. JAVA的HashTable源码分析

    Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长.Hashtable ...

随机推荐

  1. error:0906D064:PEM routines:PEM_read_bio:bad base64 decode

    今天在使用easywechat对接企业打款到银行卡时,遇到了两个错误 error:0906D064:PEM routines:PEM_read_bio:bad base64 decode 和 erro ...

  2. CAP通俗解释

    CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性). Availability(可用性).Partition tolerance(分区容错性),这三个基本需求,最多只 ...

  3. WOT干货大放送:大数据架构发展趋势及探索实践分享

      WOT大数据处理技术分会场,PingCAP CTO黄东旭.易观智库CTO郭炜.Mob开发者服务平台技术副总监林荣波.宜信技术研发中心高级架构师王东及商助科技(99Click)顾问总监郑泉五位讲师, ...

  4. Selenium_python自动化跨浏览器执行测试(简单多线程案例)

    发生背景: 跨浏览器测试是功能测试的一个分支,用以验证web应用在不同浏览器上的正常工作,通常情况下,我们都期望web类应用能够被我们的用户在任何浏览器上使用,例如有的人喜欢IE浏览器上使用,有的人喜 ...

  5. A1037

    给两个序列,一一对应相乘,求最大和. 0不算数,输入时按正负共分为4个数组. #include<cstdio> #include<algorithm> #include< ...

  6. Python3 urllib 爬取 花瓣网图片

    点我去我的github上看源码 **花瓣网是动态的,所以要抓包分析,,但我真的累的不行,不想写教程了,我源码里有注释

  7. uber司机 如何提高评分、接单率、成单率?

    接单率/成单率的解释 接单率计算方法为:成功接单的订单数 除以 系统派单的订单数. 成单率计算方法为:成功完成的订单数 除以 系统派单的订单数. 滴滴快车单单2.5倍,注册地址:http://www. ...

  8. 记boost在gcc的一个库链接问题generic_category()

    报错大致如下: main.cpp:(.text+0x49): undefined reference to `boost::system::generic_category()'main.cpp:(. ...

  9. 1009 产生数 2002年NOIP全国联赛普及组

    1009 产生数 2002年NOIP全国联赛普及组 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 黄金 Gold   题目描述 Description 给出一个整数 n(n< ...

  10. Spring学习(三)-----Spring自动装配Beans

    在Spring框架,可以用 auto-wiring 功能会自动装配Bean.要启用它,只需要在 <bean>定义“autowire”属性. <bean id="custom ...