原文:https://blog.csdn.net/world6/article/details/70053356

版权声明:本文为博主原创文章,未经博主允许不得转载。    https://blog.csdn.net/world6/article/details/70053356
以前菜得不能看的时候看Java的招聘要求:Java基础扎实,熟悉常用集合类,多线程,IO,网络编程,经常会疑惑,集合类不就ArrayList,HashMap会用,熟悉下API不就好了么,知道得越多才会发觉不知道的还有好多!     一入Java深似海啊 的

本文使用的源码是jdk1.7的源码,hashmap每个版本都发生了改变,源码都不同,但原理都是一样的,可以参考一下;

读下文前可以先思考以下这几个问题,带着问题去阅读,收获更大;

1、平时我为什么要用hashmap?key和value是以什么样的形式存在的?

2、我了解hashmap的内部结构和实现原理吗?

3、hashmap构造方法的参数有哪些,有什么用?

4、用hashmap的时候需不需要给他一个初始化大小?如果要该怎么定义?

5、不起眼的hashcode和equals方法为什么在hashmap中至关重要?

6、什么是哈希冲突?发生哈希冲突好还是不好?不好该怎么解决?

7、hashmap有什么缺点?它跟hashtable有什么性能区别?

从下面的两张图可以看到,hashmap是由数组和链表组成的;

数组:数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;

链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易;

哈希表:这时候该引出哈希表了,哈希表寻址容易,插入删除也容易,同事还满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便!简直牛逼啊~

先盗两张图...因为我不会画...

hashmap是基于Map接口实现、允许null键/值、非同步这个大家应该都是知道的... 这个时候应该对hashmap有个感性的认识了

那我们从API里面来看看hashmap的构造方法,初始一下  initialCapacity 和 loadFactor 两个名词:

HashMap() 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空HashMap。

HashMap(int initialCapacity) 构造一个带指定初始容量和默认加载因子 (0.75) 的空HashMap。

HashMap(int initialCapacity, float loadFactor)  构造一个带指定初始容量和加载因子的空HashMap。

initialCapacity是哈希表的数组的初始大小,loadFactor是加载因子;

HashMap的默认大小是16,那么他的数组长度就是0-15;默认加载因子是0.75;

为什么是16呢不是其他的数字呢?为什么默认的加载因子是0.75呢? 留个悬念哈~后面会提到的!

我们可以看到hashmap里面存放的是一个一个的Entry对象,(下文中的entry/entry对象即为此处的Entry对象)

我们看看看看hashmap是如何定义的Entry对象;

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        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对象的部分代码;
final K key :表示key是不能改变的  final关键字!

value:这没啥好说的;

Entry<k,v> next 指向下一个节点;

int hash: 这个hash 是一个散列码 是这样得到的:int hash = hash(key.hashcode());下文会有详细讲解~

重点部分来了!

HashMap的put方法:

put方法大致的思路是:
1、对key的hashCode()做hash,然后再计算存到数组里的下标;
2、如果没发生冲突直接放到数组里;
3、如果冲突了,以链表的形式存在对应的数组[ i ]中 ;
4、如果冲突导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
5、如果节点已经存在就替换old value(保证key的唯一性)
6、如果数组满了(超过load factor*current capacity),就要resize(扩容)。

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        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;
    }
这是jdk1.7的源码,1.8只是多了一个新特性,当链表的长度>7的时候,链表转换为红黑树提高查询的效率;

代码有注释,我这里再分析一次;首先通过key.hashcode()出哈希码,哈希码拿去做hash运算算出一个散列值,散列值(hash)跟数组的长度做indexFor运算,就得到了一个entry对象要存到数组的下标,这里有一个要点! 就是这个hash运算的算法设计,因为就算你拿不同的key去调用hashcode方法得到不同的值拿去做hash运算都会得到一个相同的值,然后把相同的散列值拿去做indexFor运算就会得到相同的 i ,这就发生了哈希表的冲突~~ 发生冲突了怎么办呢?

这里解释源码里的 if 中的判断,因为hash(散列值)是会算出重复的(冲突嘛~),如果这个Entry对象的hash(散列值)和你拿进来的key算的散列值(hash=hash(key))是一样的并且key也相等(==),那么你放进来的这个entry对象是同一个对象,hashmap允许你的key为空,但是key不能相同,所以新进来的会覆盖旧的;或者key.equals(k),如果这两个对象通过hashmap重写的equals方法判断是一样的,那也说明是同一个entry对象嘛!怎么办? 老办法,新的覆盖旧的!其他发生冲突的情况就把新加入的entry对象放到对应数组下标位置的表头,早进来的entry对象后移一个位置,这就形成了一个链表~~

好的,上面说的挺啰嗦的,下面来个1.8版本的总结,1.8跟1.7差不多~,1.8只是多了一个新特性,当链表的长度>=8的时候,链表转换为红黑树提高查询的效率;

1.8 链表转红黑树的源码
static final int TREEIFY_THRESHOLD = 8;
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
         treeifyBin(tab, hash);
              break;
扩容的源码:
添加entry对象的时候如果会时刻判断需不需要扩容数组

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

createEntry(hash, key, value, bucketIndex);
    }
threshold是一个域值:是由现在的数组容量*加载因子;比如初始容量是16,加载因子是0.75;那么threshold=16*0.75=12; 当数组.size达到12的时候就会扩容了,
会把数组的大小从16扩容到32,那么这时的情况就是,哈希表的数组有第12个元素时,数组就会扩容到32;

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];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }
new Capacity 就是那个两倍的数组对象,(上面的2*table.length);

transf(newTable,rehash)这个方法是把原来那个未扩容前数组里的所有entry对象复制到现在这个新数组中来;
此时的域值  threshold 的大小就是新的数组大小 * 0.75 ;

说完put,说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;
}
分析完put,get就容易理解多了~
key==null,这个情况不用讲了吧~

还是先算出散列码的值,然后通过散列码定位到数组(table)下标,也就是常说的通过key就可以拿到value,hashmap底层都是通过key拿到entry对象,然后直接从entry对象里拿到value的;

接着说:如果通过key找到了entry对象,进入if判断,第一种情况:如果entry对象的散列码和 传进来key的散列码是相等(都是int嘛,直接判断是否相等)并且entry对象的key和你传进来的key如果也相等那么就认为是你的key跟你要找的key是同一个,那么就直接return你要的value; 哦对了,这个Object  k,是为了去接收entry对象的key(然后赋值给object的k)然后才好和你传进来的key做比较,因为人家不知道你传进来的是什么类型的嘛,用object比就不会有类型转换的错误;
第二种情况:如果e.hash == hash 然后你传进来的key和entry对象的key连重写后的equals后都一样,那肯定就是同一个key了嘛;

不知道我这个菜鸟分析得怎么样,不过可以通过以下几个问题来加深对HashMap的理解;

1. 什么时候会使用HashMap?他有什么特点?

是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的线程不安全,HashMap存储着Entry(hash, key, value, next)对象。

2. 你知道HashMap的工作原理吗?
通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将key传给put方法时,它调用hashCode计算hash从而得到储存的数组下标位置,进一步存储,HashMap会根据当前数组的占用情况自动调整容量(超过域值时则resize为原来的2倍)。获取对象时,我们将key传给get方法,它调用hashCode计算hash从而得到数组下标位置,并进一步调用equals()方法确定键值对。如果发生冲突的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个数组中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

3.为什么要重写hashcode和equals方法?

1、因为要得到散列码(hash)的时候要通过key.hashcode()去得到key的哈希码才可以做hash运算;不论是put和get方法,都要使用equals方法,equals方法是object的一个方法,直接调用是比较内存(object.equals源码是用==做比较);

2、Java约定在用集合类中要重写hashcode和equals方法;  Effective java 这本书第二章有详细的解释为什么要定义这样的公约,感兴趣的可以去看看;

4、 为什么HashMap的初始大小是16?为什么默认的加载因子是0.75?HashMap的大小超过了加载因子定义的容量,怎么办?

16是2的4次方,hashmap在put的时候会发生冲突,hashmap发生冲突是不可避免的,也是我们想要的,但是我们不想hashmap傻不拉几的发生冲突,因为最坏的情况,hashmap所有的冲突都发生在同一个数组下标的话,这个位置的链表过长,而其他数组位置确实空的,这样又hashmap还扩容不了,链表的查询效率可想而知,这样的话hashmap还有那么牛逼吗?把数组的长度设计成为2的n次方,加载因子设计为0.75,会极大的优化冲突,使每个数组的链表都差不多长(也不一定,概率问题);

至于为什么? 这是数据结构相关的东西,当时是数学家设计的啊,你问我怎么知道的? 不记得什么时候看了篇论文还是博客,上面是这样说的~我真的印象很深的记得我看到过,如果你不信也没办法~  反正是有这么回事~

如果超过了负载因子,则会重新resize一个原来长度两倍的HashMap。

5、如果两个键的hashcode相同,你如何获取对象的值?

获取对象的值,那么就是get方法咯,两个key的hashcode相同说明 散列码(hash)相同,

如果散列码都相同了,那么就会调用key.equals()去判断在该散列码得到的这个数组下标的链表里的entry对象是否有相同的对象;

如果有,那么好,你直接拿这个entry对象的值即可;

6、用hashmap的时候需不需要给他一个初始化大小?如果要该怎么定义?

此问题是博主自己想出来的,答案也是博主自己思考的一个结果,不一定对,提供一个思路!如果你有更好的回答,可以留言给我一起探讨,谢谢啦~

最好是需要的,因为我们知道hashmap的数组长度超过了他的域值会扩容,扩容的时候会把hashmap中所有的entry对象再计算一次他们在新数组中的下标;可以想象一下,如果一个hashmap里有10万个entry对象了,如果要扩容,这10万个entry对象的位置都要发生变化,这会有多影响系统性能?不优化一下吗?   如何定义这个我也回答不了...因为我们只能初始化数组的大小,并不会知道每个数组元素的链表会有多长,我看同事他们创建hashmap的时候好像都没有给参数,那么如果这10万条数据放到一个大小为16的hashmap里,如果不扩容的话10万条数据只放在数组的11个元素中,那平均每个链表长度有接近1W,肯定不合理吗,链表的查询速度那么慢,所以我们判断必定会扩容,好!我们知道会扩容,但不知道会扩容几次啊,这里就是这个问题难的地方了,因为我们无法知道hashmap会扩容多少次,我们能做的就是减少他扩容的次数,同时又不让hashmap占太多内存~ 那我们就大胆的预测吧,比如有10万条数据,我觉得至少hashmap数组长度应该给1W吧,这样我们就可以把hashmap的初始大小定义为2的14次方 16384,这样数组的长度我们就定义了1.6W,就算用了1W个,也不会扩容,也没浪费更多的资源;如果你觉得10万条数据可能用不到1W个数组这么长,那你就把hashmap的初始大小定义为2的13次方,或者2的12次方;我们开始就定义一个大小总比我们不定义好(毕竟默认只有16)对吧?

7、我们可以用自定义的对象作为hashmap的key吗?如果可以,你会自定义对象当做key吗?如果不行说明原因。

可以使用自定义的对象作为hashmap的key,只要重写hashcode和equals方法就可以了,原因见第三个问题的回答;

但我一般不会把自定义对象作为key,因为有Integer跟string给我用,没必要使用自定义对象了,复杂,麻烦~

8、那你平时使用hashmap的时候一般用什么当做key,为什么?

Integer跟string呀,因为用他们用起来简单啊,Entry对象的四个属性中key是被final关键字修饰的,而Integer和string都是不可变的,直接使用Integer和string岂不美哉

9、hashmap并不能保证线程安全,如果要保证线程安全怎么办?

hashtable能保证线程安全,但有一种更高效的方式就是使用CocurrentHashMap来保证线程安全,同时效率又高;

hashtable是锁住整个数组,一次只能有一个线程去hashtable里面拿,而CocurrentHashMap是只锁数组[ i ] ,所以CocurrentHashMap的效率会比hashtable快很多,又比hashmap安全性要高,实在是居家旅行之首选~

10、那你还知道什么类似hashmap的东西吗?

有个安卓的大牛告诉我有 SarseArray  , 安卓内部特有的一个API,因为hashmap用entry来保存key和value,这里entry涉及自动装箱和拆箱,其实挺占内存的;在key为Integer的场景可以使用SarseArray;(后半段来自搜索的解释)假设现在有这样的情况,我们定义了一个长度为100的数组,虚拟机为我们开辟了100个单位的内存空间,但是我们只使用了很少(假设是5个)的一些单元,这样就造成了内存空间的浪费。而 SparseArray 是一个优化的数组,它的 key 是 Integer 类型而不是其他类型,是因为这个数组是按照 key 值的大小来排序的。按照 key 值大小排序的好处是查找的时候,可以使用二分查找,而不是蛮力的遍历整个数组。这也是为什么 SparseArray 适合替换 HashMap<Integer,<E>>,而不是任何 HashMap 的原因了。在这种情况下,原本需要100个单位内存空间而 SparseArray 只占用了5个单位的内存(实际比5个单位要大一些,因为还有一些逻辑控制的内存消耗)。key 值被当成了数组下标的功能来使用了。 安卓用SarseArray 很大一部分原因就是因为 SarseArray 比较节约内存;

结语:写博客是个自我总结的过程,其实这些知识点网上都有,随便Google一下一把大,很多牛人都深入研究过hashmap,我写的这些东西也都是看了上百篇别人的博客总结下来的,因为自己在hashmap上以前花了很多时间去学习,但过一阵子遇到某个细节点的问题的时候总感觉不太记得了;这次为了写这个博客,花了6-7个小时,这个过程让我加深了记忆,可能这几年都不会忘掉这些了,下次用到hashmap的时候可以瞬间就想起这些;

首先感谢你看完了这么长的文章,如果觉得有收获的话,还请点一下下面的 " 顶 "按钮,你的支持就是我写作的动力,谢谢~
--------------------- 
作者:world6 
来源:CSDN 
原文:https://blog.csdn.net/world6/article/details/70053356 
版权声明:本文为博主原创文章,转载请附上博文链接!

高效编程之hashmap你不看就会忘记的知识点相关推荐

  1. 高效编程之HashMap的entryset和keyset比较

    最近看了一点spring的源码,甚是苦涩:对spring稍微有了点整体的认识,但对很多细节的地方还是懵逼啊...太多不懂了的,只能慢慢去读,先把简单的不懂的解决: 比如beanfactory通过扫描器 ...

  2. Python高效编程之88条军规(2):你真的会格式化字符串吗?

    目录 1.  C风格的字符串格式化方式 2. 内建format函数与str.format方法 3. f-字符串 总结: 在微信公众号「极客起源」中输入595586,可学习全部的<Python高效 ...

  3. 聊聊我的高效编程之路

    林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 摘要:在程序开发过程中,一些IDE的配置.常用代类的积累以及常用框架的使用.能帮助我们迅速写好 ...

  4. python itertools模块位置_Python高效编程之itertools模块详解

    0 前言 说到处理循环,我们习惯使用for, while等,比如依次打印每个列表中的字符:lis = [ I , love , python ] for i in lis: print(i) I lo ...

  5. Python高效编程之88条军规(1):编码规范、字节序列与字符串

    目录 军规1:遵循PEP 8样式指南 军规2:了解字节序列(bytes)和字符串(str)的差异 用编程语言写代码是自由的,编译器不会强制你使用特定的格式编写程序(只要符合语法,编译器才不管你呢!). ...

  6. 告别996,开启Java高效编程之门

    教程介绍 所谓"武以快为尊,天下武功唯快不破".本课程剑指Java高效编程,致力于从"技术"和"工具"两大 维度提高编程效率,帮助广大程序员 ...

  7. batch print pro_研之有效 | 高效编程之Batch批处理程序

    前言 常言道:"工欲善其事,必先利其器".要有效率地开展科研工作,不仅需要持续投入时间和热情,还有必要掌握好文献管理.数据处理与分析以及团队合作等各项技能.一款好的软件能大幅提高科 ...

  8. Java高效编程之Builder模式

    前言 在<Effective Java 第2版>中有提到,遇到多个构造器参数时要考虑使用构建器(Builder模式).相比于重叠构造器(telescoping constructor)模式 ...

  9. 【AWT编写图形用户界面】GUI编程之AWT讲解(看不懂你来打我)

    JDK中提供了AWT和Swing两个包,用于GUI(图形用户界面)程序的设计和开发.下面为大家介绍一下用AWT如何进行GUI编程. 文章目录 1 AWT 2 内部类和匿名类 3 Frame 4 布局管 ...

  10. Java 高效编程之 Builder 模式

    前言 在<Effective Java 第2版>中有提到,遇到多个构造器参数时要考虑使用构建器(Builder模式).相比于重叠构造器(telescoping constructor)模式 ...

最新文章

  1. AI帮你靠“想象”打字:手机电脑软键盘也能盲打了,准确率能达到95%
  2. 利用Word将连着一起的字符按照自己指定的”字符串或者字换行“自动换行。
  3. you aren‘t using a compiler supported by lombok, so lombok will not work and
  4. Error starting static Resources caused by project facet
  5. proe2001安装指南
  6. combobox的联动练习
  7. IIS出现The specified module could not be found解决方法
  8. 德芙网络营销策略ppt_德芙网络营销方案
  9. 由比特币想到的货币那些事儿
  10. android键盘事件
  11. Codeforces Round #296 (Div. 2) C. Glass Carving [ set+multiset ]
  12. golang 程序休眠_golang package time的用法具体详解
  13. 删除威金病毒残留的_desktop.ini文件(转贴)
  14. 转载:使用pythonr脚本实现微信公众号音频批量下载
  15. 张寓博当选山东省收藏者协会副主席兼美术评论委员会主任
  16. android 主流机型排行榜,安卓手机排行榜
  17. 新海诚画集[秒速5センチメートル:樱花抄·春]
  18. 中国残疾人福利基金会开展麻风救助行动
  19. pg数据库表存放在哪里_超详细的PG数据存储结构--逻辑结构和物理存储总结,值得收藏...
  20. 计算机的英语意思,计算机常用英语写意思.doc

热门文章

  1. Linux 常用软件分类
  2. okHttp3自用封装
  3. 注解(Annotation)自定义注解入门(转)
  4. 序列化Image到byte[]
  5. ARM中C和汇编混合编程及示例(转)
  6. jQuery新的事件绑定机制on()
  7. 浅谈数据中心白盒交换机开发
  8. 虚拟机里Ubuntu编译内核方法
  9. setup_per_cpu_areas 函数
  10. python试卷管理系统的设计与实现_《数据结构》考试系统的设计与实现.doc