在对HashMap源码进行解析之前,我们先来探讨下到底阅读源码应该采用个什么法?
以我自身经验来讲:
我阅读源码首先会分三步走:
第一步:先对该对象做一个宏观的了解:了解这个类所涉及的相关知识,先了解
这些知识,然后就对这个类做个大概的了解。
以HashMap为例:
一、宏观了解:
1.HashMap不同步的,也就是非线程安全
多线程下进行结构修改需要在外部进行同步操作,改变已经关联的键值不是结构修改,
可以在自然封装映射的对象上同步。
如果不存在此类对象,则应使用Map m = Collections.synchronizedMap
(new HashMap(…));
来常见该映射对象最好在创建时完成,以防止意外对映射的非同步访问。
2.允许存在null键null值,只能允许一个,因为key是不能重复的
3.HashMap存入的顺序和遍历的顺序有可能是不一致的
4.影响HashMap性能的来个只要因素:初试容量、负载因数
5.返回的迭代器是fail-fast机制的
6.快速失败机制
7.不能保证迭代器的快速故障行为,迭代器的快速故障行为应该只用于检测bug
8.保存数据的时候通过计算key的hash值来去决定存储的位置。
9.hash桶结构(数组和链表(或树形结构)组成)-拉链式散列算法
10.红黑树
11.序列化机制:通过实现readObject/writeObject两个方法自定义序列化内容
12.进制转换
13.位运算
第二步:对HashMap有个大概的了解之后,接着就从HashMap内部开始进行了解:
二、进一步的了解:
1.HashMap是Map接口的一个实现,属于映射型的哈希表(散列表),存储key-value键
值对,允许存储null值的键值对,非同步的(非线程安全)。
2.HashMap是将Key做Hash算法,然后将 Hash值映射到内存地址,直接取得 Key所对应
的数据,hash算法是不可逆的,且具唯一性。
3.key不能重复,且HashMap是按照哈希值存储,HashMap的遍历输出是随机无序的
也就是数据存放进去和取出来的顺序是不一样的。
4.HashMap存值的时候会根据key的hashCode()来计算存储的位置(位置是散列的,所
以说其无序)。
5.对于存进去Map的一组数据,在没有外界影响的情况下(没有元素的取用,查找不算),
不会调用Hash算法,所以数据的顺序也是不会变的。
6.HashMap的存储结构是由数组+链表+红黑树的形式来组成散列桶,桶的每一个节点
都为链表的头节点或者树的根节点,采用链地址法来进行对象的存储,
先根据key的hashCode()方法在散列桶(数组)中进行寻址找到具体位置(bucket),
7.该具体位置就是一个链表结构,调用keys.equals()方法找到key对象的value节点,
如果该位置链表结构的节点超过8,就将该位置的链表转化为红黑树结构来进行存储,
时间复杂度:O(logN)。
8.HashMap根据key的hashCode值生成数组下标,通过内存地址直接查找,没有任何
的判断,时间复杂度和数组相同,都是需要更多的空间,以空间换时间。
第三步:对HashMap有了进一步的了解之后,可能会有还意犹未尽的感觉,继续
摸索:
三、再进一步的了解:
1.HashMap的底层基本性能操作:get与put。
2.哈希函数将元素适当地分散到桶中,迭代所需时间与HashMap实例(桶的数量)和本身
大小(键值映射对的数量)有关,因此,不设置初始值是非常重要的,如果迭代性能过高,
则容量过高(或负载因子过低)重要。
3.load factor(负载因子)是一个衡量哈希表允许有多满的指标,该指标在容量自动增加
之前获取
4.初试容量为创建哈希表时定义的容量,当哈希表的大小超过了当前的容量和负载因子
,就会利用rehash进行扩容操作。
5.碰撞冲突:两个相同的Key被分配到相同的桶bucket里面,哈希冲突并不会导致
HashMap覆盖一个已经存在于集合中的元素,这种情况只会在使用者试图向集合中放
入两个元素,并且它们的键对于 equal()方法是相等的时候才会发生。
6.默认的负载因子(0.75)提供了一个很好的选择,权衡时间和空间成本。值越高,空间
开销,但增加查找成本(反映在大多数HashMap类的操作,包括get, put)。
7.键不相等但又会产生哈希冲突的不同元素最终会以某种数据结构存储在 HashMap的
同一个桶中肯定会降低HashMap的性能,可以使用Comparable接口进行比较来打破
该僵局。
8.迭代器的快速故障行为应该只用于检测bug。
9.capacity: 源码中没有将它作为属性,但是为了方便,引进了这个概念,是指
HashMap中桶的数量。默认值为16。
文字有点多,划个线。。。(做完这三步,如果有哪些疑惑,我就会从源码中去分析了。。。)
停停停。。。我们阅读源码究竟是为了什么而来的,为了兴趣?
知秋大佬的话直击我的痛点:我以前阅读源码的时候,有个问题,非得把源码给全部解析注释完,你说我这不是找虐么?
大佬建议:你有没有想过为什么要设计这个类呢。我自己撸源码从来不为面试为出发点,而以生活为出发点,以需求为出发点,和写文章首先要确定中心思想,然后大纲走起,确立上下文关系,然后丰富血肉是一个道理
1、这个就很有问题了,我读源码更多是把握脉络,抓主线,以自己的思维去抓作者的思路,注释很少,因为英文类名字和方法名字已经表达的很清楚了,不需要我来做这些事情
2、我了好多大学生了,很多接触的时候都是为了读源码而读源码,纯粹为了应付面试,而不是从里面吸收思想。以一个过来人的经验,读源码这个事情确实是需要一直坚持下去的,慢慢就会发现,你是在玩源码,接近作者的思想,更多是一种思想的交流。就好比我下图这里的一个小细节,对于outboundHttpMessage()是不是要派生实现,其实就是基于netty两端对于消息的认定不同:
3、当你对于netty真的玩的很6的时候,自然就想到这些而不需要刻意去思考它为什么要这么做,包括我上面源码截图里的那一句注释,其实就是在强调做事要前后照应
好了,学习方法,就说到这里,有更好的建议欢迎和我交流!!!
阅读HashMap源码,首先要带着问题去寻求答案:
1.HashMap的工作原理,其中get()方法的工作原理?
2.我们能否让HashMap同步?
3.关于HashMap中的哈希冲突(哈希碰撞)以及冲突解决办法?
4.如果HashMap的大小超过负载因子定义的容量会怎么办?
5.你了解重新调整HashMap大小存在什么问题吗?
6.为什么String, Interger这样的wrapper类适合作为键?
7.我们可以使用自定义的对象作为键吗?
8.我们可以使用CocurrentHashMap来代替Hashtable吗?
9.HashMap扩容问题?
10.为什么HashMap是线程不安全的?如何体现出不安全的?
11.能否让HashMap实现线程安全,如何做?
12.HashMap中hash函数是怎么实现的?
13.HashMap什么时候需要重写hashcode和equals方法?
1.HashMap用什么数据结构实现的?
2.HashMap初始化传入的容量参数的值就是HashMap实际分配的空间么?
3.HashMap扩容机制是什么,什么时候扩,每次扩多少?
1.当两个对象的hashcode相同怎么办
2.如果两个键的hashcode相同,你如何获取值对象
3.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办
1.什么是HashMap?你为什么会用到它?
2.什么是hash?什么是碰撞?
3.jdk1.8的HashMap中的链表达到多少个时会生成红黑树?
4.HashMap 的遍历方式及其性能对比
5.HashMap,LinkedHashMap,TreeMap 有什么区别?
6.HashMap,LinkedHashMap,TreeMap 有什么区别?
7.HashMap 和 HashTable 有什么区别?
8.HashMap & ConcurrentHashMap 的区别?
9.为什么 ConcurrentHashMap 比 HashTable 效率要高?
本文不对这些问题进行逐一分析。
如果你对HashMap还有什么值得探讨的问题,欢迎与我交流!!!
源码解析:JDK版本11
/**
* 阅读源码前,需要对如下有熟悉
* 关键字:static、final、transient、instanceof
* 运算符:<< 、 ^ 、 >>> 、 |=
*
* 回顾基础:
*
* static:
* 1.被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享,而非静态变量是对象所拥有的。
* 2.在类被加载的时候,就会去加载被static修饰的部分。
* 3.被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
* 4.static方法就是没有this的方法
* 5.在static方法内部不能调用非静态方法
* 6.在静态方法中不能访问非静态成员方法和非静态成员变量,因为非静态成员方法/变量都是须依赖具体的对象才能够被调用。但是在非静态成员方法中是可以访问静态成员方法/变量的。
* 7.static关键字并不会改变变量和方法的访问权限,静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。
* 8.static是不允许用来修饰局部变量。
* 9.static修饰的属性和方法,使用类名.xx的形式访问。
* 10.使用的位置:那些不需要通过创建对象就可以访问方法的情况。
*
*
* final:
* 1.修饰类当用final去修饰一个类的时候,表示这个类不能被继承。
* 2.被final修饰的类,final类中的成员变量可以根据自己的实际需要设计为fianl。
* 3.final类中的成员方法都会被隐式的指定为final方法。
* 4.被final修饰的方法不能被重写。
* 5.一个类的private方法会隐式的被指定为final方法。
* 6.如果父类中有final修饰的方法,那么子类不能去重写。
* 7.修饰成员变量必须要赋初始值,而且是只能初始化一次。
* 8.修饰成员变量必须初始化值。被fianl修饰的成员变量赋值,有两种方式:1、直接赋值 2、全部在构造方法中赋初值。
* 9.如果修饰的成员变量是基本类型,则表示这个变量的值不能改变。
* 10.如果修饰的成员变量是一个引用类型,则是说这个引用的地址的值不能修改,但是这个引用所指向的对象里面的内容还是可以改变的。
*
*
* transient:
* 1.在已序列化的类中使变量不序列化——在已实现序列化的类中,有的变量不需要保存在磁盘中,就要transient关键字修饰.
* 2.transient只能修饰成员变量,不能修饰方法。
*
* instanceof:
* 在运行时指出的对象是否是特定类的一个实例
*
* <<(左移位运算) :(>>:则反过来)
* 1<<2 = 2 、 1 << 3 = 8 、1 << 4 = 16 ......
* 20的二进制补码:0001 010