先从最最基本的类型来说: String 和Integer。这两个类型基本上可以代表项目里的所有类型了。其他的什么枚举,或者对象什么的都是基于这两个类型的,至少目前来说是如此的。
String和Integer是有局限性的,为什么呢,他们只能代表一个东西,不能代表一堆东西。当我们要存一堆东西时,怎么办呢?很简单,我们知道有数组。数组估计就是这么出来的。说到数组,就得说一个典型的异常了。数组下标溢出异常ArrayIndexOutOfBoundsException。说到这个异常,我就想说数组实现的基本原理了。想创建一个数组,你得提前申请一块内存,注意是一块。而不是几块合起来叫做一块。所以呢,数组是一段连续的内存地址。数组名默认指向地址的起始地址。每个数据元素占用一定的长度。在使用数组和指针的时候,尤其能够体会到这一点。所以数组有一个局限性。必须是一块内存。而且是刚开始的时候已经固定大小的内存了。
总结:数组是用来存储东西的,而且初始化的时候就确定大小。换而言之,就是你想要一个数组可以,但必须告诉计算机你要一个多大的数组,然后计算机给你找一块这样大小的内存地址块。所以HashMap要想实现数据的存储,HashMap用到数组的可能性太大太大了。基于HashMap用到了数组,我们开始往下走。
用到了数组,我们很轻松的解决了存储一大堆东西的问题。那么新的问题来了,一般我们在设计数据库的时候,会有主键,而不是单纯的数据索引。所以是不是可以往数组里存入的时候,顺道存储一个索引呢,这个索引由我们自己来决定,而不是依据数组的索引。所以存入数据的元素将会是一个对象,而不是单纯的String、Integer对象了。Node这个对象就来了。key代表我们的索引,而value则代表我们真正想要存储的内容。
接着往下走,我们会遇到新的问题,就是虽然我们可以根据key获取自己的元素,但是在数组里,我们不得不依据循环遍历数组,对比key才能够获取我们的想要的内容。这样太麻烦了。有没有什么样的算法能够知道key就能够快速知道位置呢?
在Java中所有的引用类型都继承了Object类型,每个Object类型都有HashCode方法,它会返回一个int类型的hash值。有了这个hash值,我们每次存储时都能利用这个key的hash值确定位置直接存入,取的时候利用hash值确定索引,拿到值就OK了。
但是数组的大小是有限制的,一旦hash值超过了数组的大小,那就会出现数组下标越界异常。怎么办呢?我们可以利用求余算法来解决,即当hash值超出数组长度时,利用求余后的hash值来存入。但是又有新问题出现了,不同的hash值对容量求余后会出现重复的值。怎么办?
HashMap采用的是链式方案+equals方法解决了这个问题。当一个位置上hash遇到冲突时,会以链表形式存储,这样存储的问题就解决了。当取的时候,先根据hash值找到这个链的索引位置,然后用双等和equals方法来获取要取元素。
但是当某一链表特别特别长的时候,我们取的效率会特别低,所以在JDK1.8后HashMap采用数组+链表+红黑树(当链表长度达到8时,就会用红黑树,源码中TREEIFY_THRESHOLD默认为8)。简单地说,HashMap是数组存放一个个的桶,桶中存放链表或者红黑树,红黑树的引入是为了提高效率。
HashMap的实例有两个参数影响其性能。
初始容量:哈希表中桶的数量
加载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度
当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行rehash操作,将哈希表扩充至两倍的桶数。
Java中默认初始容量为16,加载因子为0.75。
1)loadFactor加载因子
定义:loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。
loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,
那么数组中存放的数据也就越稀,也就是可能数组中每个位置上就放一个元素。那有人说,就把loadFactor变为1最好吗,存的数据很多,但是这样会有一个问题,就是我们在通过key拿到我们的value时,
是先通过key的hashcode值,找到对应数组中的位置,如果该位置中有很多元素,则需要通过equals来依次比较链表中的元素,拿到我们的value值,这样花费的性能就很高,
如果能让数组上的每个位置尽量只有一个元素最好,我们就能直接得到value值了,所以有人又会说,那把loadFactor变得很小不就好了,但是如果变得太小,在数组中的位置就会太稀,也就是分散的太开,
浪费很多空间,这样也不好,所以在hashMap中loadFactor的初始值就是0.75,一般情况下不需要更改它。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
2)桶
根据前面画的HashMap存储的数据结构图,你这样想,数组中每一个位置上都放有一个桶,每个桶里就是装一个链表,链表中可以有很多个元素(entry),这就是桶的意思。也就相当于把元素都放在桶中。
3)capacity
capacity译为容量代表的数组的容量,也就是数组的长度,同时也是HashMap中桶的个数。默认值是16。
一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
4)size的含义
size就是在该HashMap的实例中实际存储的元素的个数
5)threshold的作用
threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。
注意这里说的是考虑,因为实际上要扩增数组,除了这个size>=threshold条件外,还需要另外一个条件。
什么时候会扩增数组的大小?在put一个元素时先size>=threshold并且还要在对应数组位置上有元素,这才能扩增数组。
注意:假设我们的容量为32,那么threshold 就是24,此时如果插入25个元素,并且这25个元素都在同一个桶中,结构为红黑树,则还有31个桶是空的,这样看来似乎不用扩充容量。但其实是需要扩容处理的,因为我们threshold 大小不适当才会导致25个元素集中在一个桶中,经过一次扩容后,元素会更加均匀的分布在各个桶中,提高了访问效率。可与此同时我们知道扩容处理会遍历所有元素,时间复杂度很高,也有坏处。所以我们说的尽量避免进行扩容处理,是遍历元素所带来的坏处是大于元素在桶中均匀分布所带来的好处。