HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供全部可选的映射操做,并容许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。html
此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操做(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。因此,若是迭代性能很重要,则不要将初始容量设置得过高或将加载因子设置得过低。也许你们开始对这段话有一点不太懂,不过不用担忧,当你读完这篇文章后,就能深切理解这其中的含义了。java
须要注意的是:Hashmap 不是同步的,若是多个线程同时访问一个 HashMap,而其中至少一个线程从结构上(指添加或者删除一个或多个映射关系的任何操做)修改了,则必须保持外部同步,以防止对映射进行意外的非同步访问。node
在 Java 编程语言中,最基本的结构就是两种,一个是数组,另一个是指针(引用),HashMap 就是经过这两个数据结构进行实现。HashMap其实是一个“链表散列”的数据结构,即数组和链表的结合体。git
从上图中能够看出,HashMap 底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个 HashMap 的时候,就会初始化一个数组。github
咱们经过 JDK 中的 HashMap 源码进行一些学习,首先看一下构造函数:面试
咱们着重看一下第 18 行代码table = new Entry[capacity];
。这不就是 Java 中数组的建立方式吗?也就是说在构造函数中,其建立了一个 Entry 的数组,其大小为 capacity(目前咱们还不须要太了解该变量含义),那么 Entry 又是什么结构呢?看一下源码:算法
咱们目前仍是只着重核心的部分,Entry 是一个 static class,其中包含了 key 和 value,也就是键值对,另外还包含了一个 next 的 Entry 指针。咱们能够总结出:Entry 就是数组中的元素,每一个 Entry 其实就是一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了链表。编程
咱们看一下方法的标准注释:在注释中首先提到了,当咱们 put 的时候,若是 key 存在了,那么新的 value 会代替旧的 value,而且若是 key 存在的状况下,该方法返回的是旧的 value,若是 key 不存在,那么返回 null。数组
从上面的源代码中能够看出:当咱们往 HashMap 中 put 元素的时候,先根据 key 的 hashCode 从新计算 hash 值,根据 hash 值获得这个元素在数组中的位置(即下标),若是数组该位置上已经存放有其余元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最早加入的放在链尾。若是数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。缓存
addEntry(hash, key, value, i)方法根据计算出的 hash 值,将 key-value 对放在数组 table 的 i 索引处。addEntry 是 HashMap 提供的一个包访问权限的方法,代码以下:
当系统决定存储 HashMap 中的 key-value 对时,彻底没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每一个 Entry 的存储位置。咱们彻底能够把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置以后,value 随之保存在那里便可。
hash(int h)方法根据 key 的 hashCode 从新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,形成的 hash 冲突。
咱们能够看到在 HashMap 中要找到某个元素,须要根据 key 的 hash 值来求得对应数组中的位置。如何计算这个位置就是 hash 算法。前面说过 HashMap 的数据结构是数组和链表的结合,因此咱们固然但愿这个 HashMap 里面的 元素位置尽可能的分布均匀些,尽可能使得每一个位置上的元素数量只有一个,那么当咱们用 hash 算法求得这个位置的时候,立刻就能够知道对应位置的元素就是咱们要的,而不用再去遍历链表,这样就大大优化了查询的效率。
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算获得的 hash 码值老是相同的。咱们首先想到的就是把 hash 值对数组长度取模运算,这样一来,元素的分布相对来讲是比较均匀的。可是,“模”运算的消耗仍是比较大的,在 HashMap 中是这样作的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪一个索引处。indexFor(int h, int length) 方法的代码以下:
这个方法很是巧妙,它经过 h & (table.length -1) 来获得该对象的保存位,而 HashMap 底层数组的长度老是 2 的 n 次方,这是 HashMap 在速度上的优化。在 HashMap 构造器中有以下代码:
这段代码保证初始化时 HashMap 的容量老是 2 的 n 次方,即底层数组的长度老是为 2 的 n 次方。
当 length 老是 2 的 n 次方时,h& (length-1)运算等价于对 length 取模,也就是 h%length,可是 & 比 % 具备更高的效率。这看上去很简单,其实比较有玄机的,咱们举个例子来讲明:
假设数组长度分别为 15 和 16,优化后的 hash 码分别为 8 和 9,那么 & 运算后的结果以下:
h & (table.length-1) | hash | table.length-1 | ||
---|---|---|---|---|
8 & (15-1): | 0100 | & | 1110 | = 0100 |
9 & (15-1): | 0101 | & | 1110 | = 0100 |
8 & (16-1): | 0100 | & | 1111 | = 0100 |
9 & (16-1): | 0101 | & | 1111 | = 0101 |
从上面的例子中能够看出:当它们和 15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8 和 9 会被放到数组中的同一个位置上造成链表,那么查询的时候就须要遍历这个链 表,获得8或者9,这样就下降了查询的效率。同时,咱们也能够发现,当数组长度为 15 的时候,hash 值会与 15-1(1110)进行“与”,那么最后一位永远是 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费至关大,更糟的是这种状况中,数组可使用的位置比数组长度小了不少,这意味着进一步增长了碰撞的概率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1 获得的二进制数的每一个位上的值都为 1,这使得在低位上&时,获得的和原 hash 的低位相同,加之 hash(int h)方法对 key 的 hashCode 的进一步优化,加入了高位计算,就使得只有相同的 hash 值的两个值才会被放到数组中的同一个位置上造成链表。
因此说,当数组长度为 2 的 n 次幂的时候,不一样的 key 算得得 index 相同的概率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的概率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
根据上面 put 方法的源代码能够看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:若是两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。若是这两个 Entry 的 key 经过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。若是这两个 Entry 的 key 经过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 造成 Entry 链,并且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。
有了上面存储时的 hash 算法做为基础,理解起来这段代码就很容易了。从上面的源代码中能够看出:从 HashMap 中 get 元素时,首先计算 key 的 hashCode,找到数组中对应位置的某一元素,而后经过 key 的 equals 方法在对应位置的链表中找到须要的元素。
简单地说,HashMap 在底层将 key-value 当成一个总体进行处理,这个总体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存全部的 key-value 对,当须要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,在根据 equals 方法决定其在该数组位置上的链表中的存储位置;当须要取出一个Entry 时,也会根据 hash 算法找到其在数组中的存储位置,再根据 equals 方法从该位置上的链表中取出该Entry。
当 HashMap 中的元素愈来愈多的时候,hash 冲突的概率也就愈来愈高,由于数组的长度是固定的。因此为了提升查询的效率,就要对 HashMap 的数组进行扩容,数组扩容这个操做也会出如今 ArrayList 中,这是一个经常使用的操做,而在 HashMap 数组扩容以后,最消耗性能的点就出现了:原数组中的数据必须从新计算其在新数组中的位置,并放进去,这就是 resize。
那么 HashMap 何时进行扩容呢?当 HashMap 中的元素个数超过数组大小 *loadFactor
时,就会进行数组扩容,loadFactor的默认值为 0.75,这是一个折中的取值。也就是说,默认状况下,数组大小为 16,那么当 HashMap 中元素个数超过 16*0.75=12
的时候,就把数组的大小扩展为 2*16=32
,即扩大一倍,而后从新计算每一个元素在数组中的位置,而这是一个很是消耗性能的操做,因此若是咱们已经预知 HashMap 中元素的个数,那么预设元素的个数可以有效的提升 HashMap 的性能。
HashMap 包含以下几个构造器:
HashMap 的基础构造器 HashMap(int initialCapacity, float loadFactor) 带有两个参数,它们是初始容量 initialCapacity 和负载因子 loadFactor。
负载因子 loadFactor 衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来讲,查找一个元素的平均时间是 O(1+a),所以若是负载因子越大,对空间的利用更充分,然然后果是查找效率的下降;若是负载因子过小,那么散列表的数据将过于稀疏,对空间形成严重浪费。
HashMap 的实现中,经过 threshold 字段来判断 HashMap 的最大容量:
threshold = (int)(capacity * loadFactor);
结合负载因子的定义公式可知,threshold 就是在此 loadFactor 和 capacity 对应下容许的最大元素数目,超过这个数目就从新 resize,以下降实际的负载因子。默认的的负载因子 0.75 是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize 后的 HashMap 容量是容量的两倍:
咱们知道 java.util.HashMap 不是线程安全的,所以若是在使用迭代器的过程当中有其余线程修改了 map,那么将抛出 ConcurrentModificationException,这就是所谓 fail-fast 策略。
ail-fast 机制是 java 集合(Collection)中的一种错误机制。 当多个线程对同一个集合的内容进行操做时,就可能会产生 fail-fast 事件。
例如:当某一个线程 A 经过 iterator去遍历某集合的过程当中,若该集合的内容被其余线程所改变了;那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。
这一策略在源码中的实现是经过 modCount 域,modCount 顾名思义就是修改次数,对 HashMap 内容(固然不只仅是 HashMap 才会有,其余例如 ArrayList 也会)的修改都将增长这个值(你们能够再回头看一下其源码,在不少操做中都有 modCount++ 这句),那么在迭代器初始化过程当中会将这个值赋给迭代器的 expectedModCount。
在迭代过程当中,判断 modCount 跟 expectedModCount 是否相等,若是不相等就表示已经有其余线程修改了 Map:
注意到 modCount 声明为 volatile,保证线程之间修改的可见性。
在 HashMap 的 API 中指出:
由全部 HashMap 类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器建立以后,若是从结构上对映射进行修改,除非经过迭代器自己的 remove 方法,其余任什么时候间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。所以,面对并发的修改,迭代器很快就会彻底失败,而不冒在未来不肯定的时间发生任意不肯定行为的风险。
注意,迭代器的快速失败行为不能获得保证,通常来讲,存在非同步的并发修改时,不可能做出任何坚定的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。所以,编写依赖于此异常的程序的作法是错误的,正确作法是:迭代器的快速失败行为应该仅用于检测程序错误。
在上文中也提到,fail-fast 机制,是一种错误检测机制。它只能被用来检测错误,由于 JDK 并不保证 fail-fast 机制必定会发生。若在多线程环境下使用 fail-fast 机制的集合,建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。
效率高,之后必定要使用此种方式!
效率低,之后尽可能少使用!
对于 HashSet 而言,它是基于 HashMap 实现的,底层采用 HashMap 来保存元素,因此若是对 HashMap 比较熟悉了,那么学习 HashSet 也是很轻松的。
咱们先经过 HashSet 最简单的构造函数和几个成员变量来看一下,证实我们上边说的,其底层是 HashMap:
其实在英文注释中已经说的比较明确了。首先有一个HashMap的成员变量,咱们在 HashSet 的构造函数中将其初始化,默认状况下采用的是 initial capacity为16,load factor 为 0.75。
对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存全部元素,所以 HashSet 的实现比较简单,相关 HashSet 的操做,基本上都是直接调用底层 HashMap 的相关方法来完成,咱们应该为保存到 HashSet 中的对象覆盖 hashCode() 和 equals()
若是此 set 中还没有包含指定元素,则添加指定元素。更确切地讲,若是此 set 没有包含知足(e==null ? e2==null : e.equals(e2)) 的元素 e2,则向此 set 添加指定的元素 e。若是此 set 已包含该元素,则该调用不更改 set 并返回 false。但底层实际将将该元素做为 key 放入 HashMap。思考一下为何?
因为 HashMap 的 put() 方法添加 key-value 对时,当新放入 HashMap 的 Entry 中 key 与集合中原有 Entry 的 key 相同(hashCode()返回值相等,经过 equals 比较也返回 true),新添加的 Entry 的 value 会将覆盖原来 Entry 的 value(HashSet 中的 value 都是PRESENT
),但 key 不会有任何改变,所以若是向 HashSet 中添加一个已经存在的元素时,新添加的集合元素将不会被放入 HashMap中,原来的元素也不会有任何改变,这也就知足了 Set 中元素不重复的特性。
该方法若是添加的是在 HashSet 中不存在的,则返回 true;若是添加的元素已经存在,返回 false。其缘由在于咱们以前提到的关于 HashMap 的 put 方法。该方法在添加 key 不重复的键值对的时候,会返回 null。
和 HashMap 同样,Hashtable 也是一个散列表,它存储的内容是键值对。
Hashtable 在 Java 中的定义为:
从源码中,咱们能够看出,Hashtable 继承于 Dictionary 类,实现了 Map, Cloneable, java.io.Serializable接口。其中Dictionary类是任何可将键映射到相应值的类(如 Hashtable)的抽象父类,每一个键和值都是对象(源码注释为:The Dictionary
class is the abstract parent of any class, such as Hashtable
, which maps keys to values. Every key and every value is an object.)。但在这一点我开始有点怀疑,由于我查看了HashMap以及TreeMap的源码,都没有继承于这个类。不过当我看到注释中的解释也就明白了,其 Dictionary 源码注释是这样的:NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class. 该话指出 Dictionary 这个类过期了,新的实现类应该实现Map接口。
Hashtable是经过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, count, threshold, loadFactor, modCount。
关于变量的解释在源码注释中都有,最好仍是应该看英文注释。
Hashtable 一共提供了 4 个构造方法:
public Hashtable(int initialCapacity, float loadFactor)
: 用指定初始容量和指定加载因子构造一个新的空哈希表。useAltHashing 为 boolean,其若是为真,则执行另外一散列的字符串键,以减小因为弱哈希计算致使的哈希冲突的发生。public Hashtable(int initialCapacity)
:用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。public Hashtable()
:默认构造函数,容量为 11,加载因子为 0.75。public Hashtable(Map<? extends K, ? extends V> t)
:构造一个与给定的 Map 具备相同映射关系的新哈希表。put 方法的整个流程为:
我在下面的代码中也进行了一些注释:
经过一个实际的例子来演示一下这个过程:
假设咱们如今Hashtable的容量为5,已经存在了(5,5),(13,13),(16,16),(17,17),(21,21)这 5 个键值对,目前他们在Hashtable中的位置以下:
如今,咱们插入一个新的键值对,put(16,22),假设key=16的索引为1.但如今索引1的位置有两个Entry了,因此程序会对链表进行迭代。迭代的过程当中,发现其中有一个Entry的key和咱们要插入的键值对的key相同,因此如今会作的工做就是将newValue=22替换oldValue=16,而后返回oldValue=16.
而后咱们如今再插入一个,put(33,33),key=33的索引为3,而且在链表中也不存在key=33的Entry,因此将该节点插入链表的第一个位置。
相比较于 put 方法,get 方法则简单不少。其过程就是首先经过 hash()方法求得 key 的哈希值,而后根据 hash 值获得 index 索引(上述两步所用的算法与 put 方法都相同)。而后迭代链表,返回匹配的 key 的对应的 value;找不到则返回 null。
Hashtable 有多种遍历方式:
HashMap 是无序的,HashMap 在 put 的时候是根据 key 的 hashcode 进行 hash 而后放入对应的地方。因此在按照必定顺序 put 进 HashMap 中,而后遍历出 HashMap 的顺序跟 put 的顺序不一样(除非在 put 的时候 key 已经按照 hashcode 排序号了,这种概率很是小)
JAVA 在 JDK1.4 之后提供了 LinkedHashMap 来帮助咱们实现了有序的 HashMap!
LinkedHashMap 是 HashMap 的一个子类,它保留插入的顺序,若是须要输出的顺序和输入时的相同,那么就选用 LinkedHashMap。
LinkedHashMap 是 Map 接口的哈希表和连接列表实现,具备可预知的迭代顺序。此实现提供全部可选的映射操做,并容许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
LinkedHashMap 实现与 HashMap 的不一样之处在于,LinkedHashMap 维护着一个运行于全部条目的双重连接列表。此连接列表定义了迭代顺序,该迭代顺序能够是插入顺序或者是访问顺序。
注意,此实现不是同步的。若是多个线程同时访问连接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
根据链表中元素的顺序能够分为:按插入顺序的链表,和按访问顺序(调用 get 方法)的链表。默认是按插入顺序排序,若是指定按访问顺序排序,那么调用get方法后,会将此次访问的元素移至链表尾部,不断访问能够造成按访问顺序排序的链表。
我在最开始学习 LinkedHashMap 的时候,看到访问顺序、插入顺序等等,有点晕了,随着后续的学习才慢慢懂得其中原理,因此我会先在进行作几个 demo 来演示一下 LinkedHashMap 的使用。看懂了其效果,而后再来研究其原理。
看下面这个代码:
一个比较简单的测试 HashMap 的代码,经过控制台的输出,咱们能够看到 HashMap 是没有顺序的。
咱们如今将 map 的实现换成 LinkedHashMap,其余代码不变:Map<String, String> map = new LinkedHashMap<String, String>();
看一下控制台的输出:
咱们能够看到,其输出顺序是完成按照插入顺序的!也就是咱们上面所说的保留了插入的顺序。咱们不是在上面还提到过其能够按照访问顺序进行排序么?好的,咱们仍是经过一个例子来验证一下:
代码与以前的都差很少,但咱们多了两行代码,而且初始化 LinkedHashMap 的时候,用的构造函数也不相同,看一下控制台的输出结果:
这也就是咱们以前提到过的,LinkedHashMap 能够选择按照访问顺序进行排序。
对于 LinkedHashMap 而言,它继承与 HashMap(public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
)、底层使用哈希表与双向链表来保存全部元素。其基本操做与父类 HashMap 类似,它经过重写父类相关的方法,来实现本身的连接列表特性。下面咱们来分析 LinkedHashMap 的源代码:
LinkedHashMap 采用的 hash 算法和 HashMap 相同,可是它从新定义了数组中保存的元素 Entry,该 Entry 除了保存当前对象的引用外,还保存了其上一个元素 before 和下一个元素 after 的引用,从而在哈希表的基础上又构成了双向连接列表。看源代码:
LinkedHashMap 中的 Entry 集成与 HashMap 的 Entry,可是其增长了 before 和 after 的引用,指的是上一个元素和下一个元素的引用。
经过源代码能够看出,在 LinkedHashMap 的构造方法中,实际调用了父类 HashMap 的相关构造方法来构造一个底层存放的 table 数组,但额外能够增长 accessOrder 这个参数,若是不设置,默认为 false,表明按照插入顺序进行迭代;固然能够显式设置为 true,表明以访问顺序进行迭代。如:
咱们已经知道 LinkedHashMap 的 Entry 元素继承 HashMap 的 Entry,提供了双向链表的功能。在上述 HashMap 的构造器中,最后会调用 init() 方法,进行相关的初始化,这个方法在 HashMap 的实现中并没有意义,只是提供给子类实现相关的初始化调用。
但在 LinkedHashMap 重写了 init() 方法,在调用父类的构造方法完成构造后,进一步实现了对其元素 Entry 的初始化操做。
LinkedHashMap 并未重写父类 HashMap 的 put 方法,而是重写了父类 HashMap 的 put 方法调用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了本身特有的双向连接列表的实现。咱们在以前的文章中已经讲解了HashMap的put方法,咱们在这里从新贴一下 HashMap 的 put 方法的源代码:
HashMap.put:
重写方法:
LinkedHashMap 重写了父类 HashMap 的 get 方法,实际在调用父类 getEntry() 方法取得查找的元素后,再判断当排序模式 accessOrder 为 true 时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。因为的链表的增长、删除操做是常量级的,故并不会带来性能的损失。
LinkedHashMap 定义了排序模式 accessOrder,该属性为 boolean 型变量,对于访问顺序,为 true;对于插入顺序,则为 false。通常状况下,没必要指定排序模式,其迭代顺序即为默认为插入顺序。
这些构造方法都会默认指定排序模式为插入顺序。若是你想构造一个 LinkedHashMap,并打算按从近期访问最少到近期访问最多的顺序(即访问顺序)来保存元素,那么请使用下面的构造方法构造 LinkedHashMap:public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
该哈希映射的迭代顺序就是最后访问其条目的顺序,这种映射很适合构建 LRU 缓存。LinkedHashMap 提供了 removeEldestEntry(Map.Entry<K,V> eldest) 方法。该方法能够提供在每次添加新条目时移除最旧条目的实现程序,默认返回 false,这样,此映射的行为将相似于正常映射,即永远不能移除最旧的元素。
咱们会在后面的文章中详细介绍关于如何用 LinkedHashMap 构建 LRU 缓存。
其实 LinkedHashMap 几乎和 HashMap 同样:从技术上来讲,不一样的是它定义了一个 Entry<K,V> header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 经过继承 hashMap 中的 Entry<K,V>,并添加两个属性 Entry<K,V> before,after,和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。
在写关于 LinkedHashMap 的过程当中,记起来以前面试的过程当中遇到的一个问题,也是问我 Map 的哪一种实现能够作到按照插入顺序进行迭代?当时脑子是忽然短路的,但如今想一想,也只能怪本身对这个知识点仍是掌握的不够扎实,因此又从头认真的把代码看了一遍。
不过,个人建议是,你们首先首先须要记住的是:LinkedHashMap 可以作到按照插入顺序或者访问顺序进行迭代,这样在咱们之后的开发中遇到类似的问题,才能想到用 LinkedHashMap 来解决,不然就算对其内部结构很是了解,不去使用也是没有什么用的。
思考了很久,到底要不要总结 LinkedHashSet 的内容 = = 我在以前的博文中,分别写了 HashMap 和 HashSet,而后咱们能够看到 HashSet 的方法基本上都是基于 HashMap 来实现的,说白了,HashSet内部的数据结构就是一个 HashMap,其方法的内部几乎就是在调用 HashMap 的方法。
LinkedHashSet 首先咱们须要知道的是它是一个 Set 的实现,因此它其中存的确定不是键值对,而是值。此实现与 HashSet 的不一样之处在于,LinkedHashSet 维护着一个运行于全部条目的双重连接列表。此连接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
看到上面的介绍,是否是感受其与 HashMap 和 LinkedHashMap 的关系很像?
注意,此实现不是同步的。若是多个线程同时访问连接的哈希Set,而其中至少一个线程修改了该 Set,则它必须保持外部同步。
在LinkedHashMap的实现原理中,经过例子演示了 HashMap 和 LinkedHashMap 的区别。触类旁通,咱们如今学习的LinkedHashSet与以前的很相同,只不过以前存的是键值对,而如今存的只有值。
因此我就再也不具体的贴代码在这边了,但咱们能够确定的是,LinkedHashSet 是能够按照插入顺序或者访问顺序进行迭代。
对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。
LinkedHashSet 底层使用 LinkedHashMap 来保存全部元素,它继承与 HashSet,其全部的方法操做上又与 HashSet 相同,所以 LinkedHashSet 的实现上很是简单,只提供了四个构造方法,并经过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操做上与父类 HashSet 的操做相同,直接调用父类 HashSet 的方法便可。LinkedHashSet 的源代码以下:
以上几乎就是 LinkedHashSet 的所有代码了,那么读者可能就会怀疑了,不是说 LinkedHashSet 是基于 LinkedHashMap 实现的吗?那我为何在源码中甚至都没有看到出现过 LinkedHashMap。不要着急,咱们能够看到在 LinkedHashSet 的构造方法中,其调用了父类的构造方法。咱们能够进去看一下:
在父类 HashSet 中,专为 LinkedHashSet 提供的构造方法以下,该方法为包访问权限,并未对外公开。
由上述源代码可见,LinkedHashSet 经过继承 HashSet,底层使用 LinkedHashMap,以很简单明了的方式来实现了其自身的全部功能。
以上就是关于 LinkedHashSet 的内容,咱们只是从概述上以及构造方法这几个方面介绍了,并非咱们不想去深刻其读取或者写入方法,而是其自己没有实现,只是继承于父类 HashSet 的方法。
因此咱们须要注意的点是:
ArrayList 能够理解为动态数组,用 MSDN 中的说法,就是 Array 的复杂版本。与 Java 中的数组相比,它的容量能动态增加。ArrayList 是 List 接口的可变数组的实现。实现了全部可选列表操做,并容许包括 null 在内的全部元素。除了实现 List 接口外,此类还提供一些方法来操做内部用来存储列表的数组的大小。(此类大体上等同于 Vector 类,除了此类是不一样步的。)
每一个 ArrayList 实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它老是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增加。自动增加会带来数据向新数组的从新拷贝,所以,若是可预知数据量的多少,可在构造 ArrayList 时指定其容量。在添加大量元素前,应用程序也可使用 ensureCapacity 操做来增长 ArrayList 实例的容量,这能够减小递增式再分配的数量。
注意,此实现不是同步的。若是多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操做,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)
咱们先学习了解其内部的实现原理,才能更好的理解其应用。
对于 ArrayList 而言,它实现 List 接口、底层使用数组保存全部元素。其操做基本上是对数组的操做。下面咱们来分析 ArrayList 的源代码:
ArrayList 继承了 AbstractList,实现了 List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
ArrayList 实现了 RandmoAccess 接口,即提供了随机访问功能。RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在 ArrayList 中,咱们便可以经过元素的序号快速获取元素对象;这就是快速随机访问。
ArrayList 实现了 Cloneable 接口,即覆盖了函数 clone(),能被克隆。 ArrayList 实现 java.io.Serializable 接口,这意味着 ArrayList 支持序列化,能经过序列化去传输。
ArrayList 提供了三种方式的构造器:
public ArrayList()
能够构造一个默认初始容量为10的空列表;public ArrayList(int initialCapacity)
构造一个指定初始容量的空列表;public ArrayList(Collection<? extends E> c)
构造一个包含指定 collection 的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。ArrayList 中提供了多种添加元素的方法,下面将一一进行讲解:
1.set(int index, E element):该方法首先调用rangeCheck(index)
来校验 index 变量是否超出数组范围,超出则抛出异常。然后,取出原 index 位置的值,而且将新的 element 放入 Index 位置,返回 oldValue。
2.add(E e):该方法是将指定的元素添加到列表的尾部。当容量不足时,会调用 grow 方法增加容量。
3.add(int index, E element):在 index 位置插入 element。
4.addAll(Collection<? extends E> c)
和 addAll(int index, Collection<? extends E> c)
:将特定 Collection 中的元素添加到 Arraylist 末尾。
在 ArrayList 的存储方法,其核心本质是在数组的某个位置将元素添加进入。但其中又会涉及到关于数组容量不够而增加等因素。
这个方法就比较简单了,ArrayList 可以支持随机访问的缘由也是很显然的,由于它内部的数据结构是数组,而数组自己就是支持随机访问。该方法首先会判断输入的index值是否越界,而后将数组的 index 位置的元素返回便可。
ArrayList 提供了根据下标或者指定对象两种方式的删除功能。须要注意的是该方法的返回值并不相同,以下:
注意:从数组中移除元素的操做,也会致使被移除的元素之后的全部元素的向左移动一个位置。
从上面介绍的向 ArrayList 中存储元素的代码中,咱们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,若是超出,数组将会进行扩容,以知足添加数据的需求。数组扩容有两个方法,其中开发者能够经过一个 public 的方法ensureCapacity(int minCapacity)
来增长 ArrayList 的容量,而在存储元素等操做过程当中,若是遇到容量不足,会调用priavte方法private void ensureCapacityInternal(int minCapacity)
实现。
从上述代码中能够看出,数组进行扩容时,会将老数组中的元素从新拷贝一份到新的数组中,每次数组容量的增加大约是其原容量的 1.5 倍(从int newCapacity = oldCapacity + (oldCapacity >> 1)
这行代码得出)。这种操做的代价是很高的,所以在实际使用时,咱们应该尽可能避免数组容量的扩张。当咱们可预知要保存的元素的多少时,要在构造 ArrayList 实例时,就指定其容量,以免数组扩容的发生。或者根据实际需求,经过调用ensureCapacity 方法来手动增长 ArrayList 实例的容量。
ArrayList 也采用了快速失败的机制,经过记录 modCount 参数来实现。在面对并发的修改时,迭代器很快就会彻底失败,而不是冒着在未来某个不肯定时间发生任意不肯定行为的风险。 关于 Fail-Fast 的更详细的介绍,我在以前将 HashMap 中已经提到。
LinkedList 和 ArrayList 同样,都实现了 List 接口,但其内部的数据结构有本质的不一样。LinkedList 是基于链表实现的(经过名字也能区分开来),因此它的插入和删除操做比 ArrayList 更加高效。但也是因为其为基于链表的,因此随机访问的效率要比 ArrayList 差。
看一下 LinkedList 的类的定义:
LinkedList 继承自 AbstractSequenceList,实现了 List、Deque、Cloneable、java.io.Serializable 接口。AbstractSequenceList 提供了List接口骨干性的实现以减小实现 List 接口的复杂度,Deque 接口定义了双端队列的操做。
在 LinkedList 中除了自己本身的方法外,还提供了一些可使其做为栈、队列或者双端队列的方法。这些方法可能彼此之间只是名字不一样,以使得这些名字在特定的环境中显得更加合适。
LinkedList 也是 fail-fast 的(前边提过不少次了)。
LinkedList 是基于链表结构实现,因此在类中包含了 first 和 last 两个指针(Node)。Node 中包含了上一个节点和下一个节点的引用,这样就构成了双向的链表。每一个 Node 只能知道本身的前一个节点和后一个节点,但对于链表来讲,这已经足够了。
该方法是在链表的 end 添加元素,其调用了本身的方法 linkLast(E e)。
该方法首先将 last 的 Node 引用指向了一个新的 Node(l),而后根据l新建了一个 newNode,其中的元素就为要添加的 e;然后,咱们让 last 指向了 newNode。接下来是自身进行维护该链表。
该方法是在指定 index 位置插入元素。若是 index 位置正好等于 size,则调用 linkLast(element) 将其插入末尾;不然调用 linkBefore(element, node(index))方法进行插入。该方法的实如今下面,你们能够本身仔细的分析一下。(分析链表的时候最好可以边画图边分析)
LinkedList 的方法实在是太多,在这无法一一举例分析。但不少方法其实都只是在调用别的方法而已,因此建议你们将其几个最核心的添加的方法搞懂就能够了,好比 linkBefore、linkLast。其本质也就是链表之间的删除添加等。
咱们在以前的博文中了解到关于 HashMap 和 Hashtable 这两种集合。其中 HashMap 是非线程安全的,当咱们只有一个线程在使用 HashMap 的时候,天然不会有问题,但若是涉及到多个线程,而且有读有写的过程当中,HashMap 就不能知足咱们的须要了(fail-fast)。在不考虑性能问题的时候,咱们的解决方案有 Hashtable 或者Collections.synchronizedMap(hashMap),这两种方式基本都是对整个 hash 表结构作锁定操做的,这样在锁表的期间,别的线程就须要等待了,无疑性能不高。
因此咱们在本文中学习一个 util.concurrent 包的重要成员,ConcurrentHashMap。
ConcurrentHashMap 的实现是依赖于 Java 内存模型,因此咱们在了解 ConcurrentHashMap 的前提是必须了解Java 内存模型。但 Java 内存模型并非本文的重点,因此我假设读者已经对 Java 内存模型有所了解。
ConcurrentHashMap 的结构是比较复杂的,都深究去本质,其实也就是数组和链表而已。咱们由浅入深慢慢的分析其结构。
先简单分析一下,ConcurrentHashMap 的成员变量中,包含了一个 Segment 的数组(final Segment<K,V>[] segments;
),而 Segment 是 ConcurrentHashMap 的内部类,而后在 Segment 这个类中,包含了一个 HashEntry 的数组(transient volatile HashEntry<K,V>[] table;
)。而 HashEntry 也是 ConcurrentHashMap 的内部类。HashEntry 中,包含了 key 和 value 以及 next 指针(相似于 HashMap 中 Entry),因此 HashEntry 能够构成一个链表。
因此通俗的讲,ConcurrentHashMap 数据结构为一个 Segment 数组,Segment 的数据结构为 HashEntry 的数组,而 HashEntry 存的是咱们的键值对,能够构成链表。
首先,咱们看一下 HashEntry 类。
HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。其类的定义为:
HashEntry 的学习能够类比着 HashMap 中的 Entry。咱们的存储键值对的过程当中,散列的时候若是发生“碰撞”,将采用“分离链表法”来处理碰撞:把碰撞的 HashEntry 对象连接成一个链表。
以下图,咱们在一个空桶中插入 A、B、C 两个 HashEntry 对象后的结构图(其实应该为键值对,在这进行了简化以方便更容易理解):
Segment 的类定义为static final class Segment<K,V> extends ReentrantLock implements Serializable
。其继承于 ReentrantLock 类,从而使得 Segment 对象能够充当锁的角色。Segment 中包含HashEntry 的数组,其能够守护其包含的若干个桶(HashEntry的数组)。Segment 在某些意义上有点相似于 HashMap了,都是包含了一个数组,而数组中的元素能够是一个链表。
table:table 是由 HashEntry 对象组成的数组若是散列时发生碰撞,碰撞的 HashEntry 对象就以链表的形式连接成一个链表table数组的数组成员表明散列映射表的一个桶每一个 table 守护整个 ConcurrentHashMap 包含桶总数的一部分若是并发级别为 16,table 则守护 ConcurrentHashMap 包含的桶总数的 1/16。
count 变量是计算器,表示每一个 Segment 对象管理的 table 数组(若干个 HashEntry 的链表)包含的HashEntry 对象的个数。之因此在每一个Segment对象中包含一个 count 计数器,而不在 ConcurrentHashMap 中使用全局的计数器,是为了不出现“热点域”而影响并发性。
咱们经过下图来展现一下插入 ABC 三个节点后,Segment 的示意图:
其实从我我的角度来讲,Segment结构是与HashMap很像的。
ConcurrentHashMap 的结构中包含的 Segment 的数组,在默认的并发级别会建立包含 16 个 Segment 对象的数组。经过咱们上面的知识,咱们知道每一个 Segment 又包含若干个散列表的桶,每一个桶是由 HashEntry 连接起来的一个链表。若是 key 可以均匀散列,每一个 Segment 大约守护整个散列表桶总数的 1/16。
下面咱们还有经过一个图来演示一下 ConcurrentHashMap 的结构:
在 ConcurrentHashMap 中,当执行 put 方法的时候,会须要加锁来完成。咱们经过代码来解释一下具体过程: 当咱们 new 一个 ConcurrentHashMap 对象,而且执行put操做的时候,首先会执行 ConcurrentHashMap 类中的 put 方法,该方法源码为:
咱们经过注释能够了解到,ConcurrentHashMap 不容许空值。该方法首先有一个 Segment 的引用 s,而后会经过 hash() 方法对 key 进行计算,获得哈希值;继而经过调用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法进行存储操做。该方法源码为:
关于该方法的某些关键步骤,在源码上加上了注释。
须要注意的是:加锁操做是针对的 hash 值对应的某个 Segment,而不是整个 ConcurrentHashMap。由于 put 操做只是在这个 Segment 中完成,因此并不须要对整个 ConcurrentHashMap 加锁。因此,此时,其余的线程也能够对另外的 Segment 进行 put 操做,由于虽然该 Segment 被锁住了,但其余的 Segment 并无加锁。同时,读线程并不会由于本线程的加锁而阻塞。
正是由于其内部的结构以及机制,因此 ConcurrentHashMap 在并发访问的性能上要比Hashtable和同步包装以后的HashMap的性能提升不少。在理想状态下,ConcurrentHashMap 能够支持 16 个线程执行并发写操做(若是并发级别设置为 16),及任意数量线程的读操做。
在实际的应用中,散列表通常的应用场景是:除了少数插入操做和删除操做外,绝大多数都是读取操做,并且读操做在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap 针对读操做作了大量的优化。经过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,使得 大多数时候,读操做不须要加锁就能够正确得到值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提升。
ConcurrentHashMap 是一个并发散列映射表的实现,它容许彻底并发的读取,而且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不一样线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也致使对容器的访问变成串行化的了。
ConcurrentHashMap 的高并发性主要来自于三个方面:
使用分离锁,减少了请求 同一个锁的频率。
经过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操做大多数时候不须要加锁就能成功获取到须要的值。因为散列映射表在实际应用中大多数操做都是成功的 读操做,因此 2 和 3 既能够减小请求同一个锁的频率,也能够有效减小持有锁的时间。经过减少请求同一个锁的频率和尽可能减小持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提升。
咱们平时总会有一个电话本记录全部朋友的电话,可是,若是有朋友常常联系,那些朋友的电话号码不用翻电话本咱们也能记住,可是,若是长时间没有联系了,要再次联系那位朋友的时候,咱们又不得不求助电话本,可是,经过电话本查找仍是很费时间的。可是,咱们大脑可以记住的东西是必定的,咱们只能记住本身最熟悉的,而长时间不熟悉的天然就忘记了。
其实,计算机也用到了一样的一个概念,咱们用缓存来存放之前读取的数据,而不是直接丢掉,这样,再次读取的时候,能够直接在缓存里面取,而不用再从新查找一遍,这样系统的反应能力会有很大提升。可是,当咱们读取的个数特别大的时候,咱们不可能把全部已经读取的数据都放在缓存里,毕竟内存大小是必定的,咱们通常把最近常读取的放在缓存里(至关于咱们把最近联系的朋友的姓名和电话放在大脑里同样)。
LRU 缓存利用了这样的一种思想。LRU 是 Least Recently Used 的缩写,翻译过来就是“最近最少使用”,也就是说,LRU 缓存把最近最少使用的数据移除,让给最新读取的数据。而每每最常读取的,也是读取次数最多的,因此,利用 LRU 缓存,咱们可以提升系统的 performance。
要实现 LRU 缓存,咱们首先要用到一个类 LinkedHashMap。
用这个类有两大好处:一是它自己已经实现了按照访问顺序的存储,也就是说,最近读取的会放在最前面,最最不常读取的会放在最后(固然,它也能够实现按照插入顺序存储)。第二,LinkedHashMap 自己有一个方法用于判断是否须要移除最不常读取的数,可是,原始方法默认不须要移除(这是,LinkedHashMap 至关于一个linkedlist),因此,咱们须要 override 这样一个方法,使得当缓存里存放的数据个数超过规定个数后,就把最不经常使用的移除掉。关于 LinkedHashMap 中已经有详细的介绍。
代码以下:(可直接复制,也能够经过LRUcache-Java下载)
HashMap 和 HashSet 都是 collection 框架的一部分,它们让咱们可以使用对象的集合。collection 框架有本身的接口和实现,主要分为 Set 接口,List 接口和 Queue 接口。它们有各自的特色,Set 的集合里不容许对象有重复的值,List 容许有重复,它对集合中的对象进行索引,Queue 的工做原理是 FCFS 算法(First Come, First Serve)。
首先让咱们来看看什么是 HashMap 和 HashSet,而后再来比较它们之间的分别。
HashSet 实现了 Set 接口,它不容许集合中有重复的值,当咱们提到 HashSet 时,第一件事情就是在将对象存储在 HashSet 以前,要先确保对象重写 equals()和 hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。若是咱们没有重写这两个方法,将会使用这个方法的默认实现。
public boolean add(Obje
HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供全部可选的映射操做,并容许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操做(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。因此,若是迭代性能很重要,则不要将初始容量设置得过高或将加载因子设置得过低。也许你们开始对这段话有一点不太懂,不过不用担忧,当你读完这篇文章后,就能深切理解这其中的含义了。
须要注意的是:Hashmap 不是同步的,若是多个线程同时访问一个 HashMap,而其中至少一个线程从结构上(指添加或者删除一个或多个映射关系的任何操做)修改了,则必须保持外部同步,以防止对映射进行意外的非同步访问。
在 Java 编程语言中,最基本的结构就是两种,一个是数组,另一个是指针(引用),HashMap 就是经过这两个数据结构进行实现。HashMap其实是一个“链表散列”的数据结构,即数组和链表的结合体。
从上图中能够看出,HashMap 底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个 HashMap 的时候,就会初始化一个数组。
咱们经过 JDK 中的 HashMap 源码进行一些学习,首先看一下构造函数:
咱们着重看一下第 18 行代码table = new Entry[capacity];
。这不就是 Java 中数组的建立方式吗?也就是说在构造函数中,其建立了一个 Entry 的数组,其大小为 capacity(目前咱们还不须要太了解该变量含义),那么 Entry 又是什么结构呢?看一下源码:
咱们目前仍是只着重核心的部分,Entry 是一个 static class,其中包含了 key 和 value,也就是键值对,另外还包含了一个 next 的 Entry 指针。咱们能够总结出:Entry 就是数组中的元素,每一个 Entry 其实就是一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了链表。
咱们看一下方法的标准注释:在注释中首先提到了,当咱们 put 的时候,若是 key 存在了,那么新的 value 会代替旧的 value,而且若是 key 存在的状况下,该方法返回的是旧的 value,若是 key 不存在,那么返回 null。
从上面的源代码中能够看出:当咱们往 HashMap 中 put 元素的时候,先根据 key 的 hashCode 从新计算 hash 值,根据 hash 值获得这个元素在数组中的位置(即下标),若是数组该位置上已经存放有其余元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最早加入的放在链尾。若是数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
addEntry(hash, key, value, i)方法根据计算出的 hash 值,将 key-value 对放在数组 table 的 i 索引处。addEntry 是 HashMap 提供的一个包访问权限的方法,代码以下:
当系统决定存储 HashMap 中的 key-value 对时,彻底没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每一个 Entry 的存储位置。咱们彻底能够把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置以后,value 随之保存在那里便可。
hash(int h)方法根据 key 的 hashCode 从新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,形成的 hash 冲突。
咱们能够看到在 HashMap 中要找到某个元素,须要根据 key 的 hash 值来求得对应数组中的位置。如何计算这个位置就是 hash 算法。前面说过 HashMap 的数据结构是数组和链表的结合,因此咱们固然但愿这个 HashMap 里面的 元素位置尽可能的分布均匀些,尽可能使得每一个位置上的元素数量只有一个,那么当咱们用 hash 算法求得这个位置的时候,立刻就能够知道对应位置的元素就是咱们要的,而不用再去遍历链表,这样就大大优化了查询的效率。
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算获得的 hash 码值老是相同的。咱们首先想到的就是把 hash 值对数组长度取模运算,这样一来,元素的分布相对来讲是比较均匀的。可是,“模”运算的消耗仍是比较大的,在 HashMap 中是这样作的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪一个索引处。indexFor(int h, int length) 方法的代码以下:
这个方法很是巧妙,它经过 h & (table.length -1) 来获得该对象的保存位,而 HashMap 底层数组的长度老是 2 的 n 次方,这是 HashMap 在速度上的优化。在 HashMap 构造器中有以下代码:
这段代码保证初始化时 HashMap 的容量老是 2 的 n 次方,即底层数组的长度老是为 2 的 n 次方。
当 length 老是 2 的 n 次方时,h& (length-1)运算等价于对 length 取模,也就是 h%length,可是 & 比 % 具备更高的效率。这看上去很简单,其实比较有玄机的,咱们举个例子来讲明:
假设数组长度分别为 15 和 16,优化后的 hash 码分别为 8 和 9,那么 & 运算后的结果以下:
h & (table.length-1) | hash | table.length-1 | ||
---|---|---|---|---|
8 & (15-1): | 0100 | & | 1110 | = 0100 |
9 & (15-1): | 0101 | & | 1110 | = 0100 |
8 & (16-1): | 0100 | & | 1111 | = 0100 |
9 & (16-1): | 0101 | & | 1111 | = 0101 |
从上面的例子中能够看出:当它们和 15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8 和 9 会被放到数组中的同一个位置上造成链表,那么查询的时候就须要遍历这个链 表,获得8或者9,这样就下降了查询的效率。同时,咱们也能够发现,当数组长度为 15 的时候,hash 值会与 15-1(1110)进行“与”,那么最后一位永远是 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费至关大,更糟的是这种状况中,数组可使用的位置比数组长度小了不少,这意味着进一步增长了碰撞的概率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1 获得的二进制数的每一个位上的值都为 1,这使得在低位上&时,获得的和原 hash 的低位相同,加之 hash(int h)方法对 key 的 hashCode 的进一步优化,加入了高位计算,就使得只有相同的 hash 值的两个值才会被放到数组中的同一个位置上造成链表。
因此说,当数组长度为 2 的 n 次幂的时候,不一样的 key 算得得 index 相同的概率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的概率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
根据上面 put 方法的源代码能够看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:若是两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。若是这两个 Entry 的 key 经过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。若是这两个 Entry 的 key 经过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 造成 Entry 链,并且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。
有了上面存储时的 hash 算法做为基础,理解起来这段代码就很容易了。从上面的源代码中能够看出:从 HashMap 中 get 元素时,首先计算 key 的 hashCode,找到数组中对应位置的某一元素,而后经过 key 的 equals 方法在对应位置的链表中找到须要的元素。
简单地说,HashMap 在底层将 key-value 当成一个总体进行处理,这个总体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存全部的 key-value 对,当须要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,在根据 equals 方法决定其在该数组位置上的链表中的存储位置;当须要取出一个Entry 时,也会根据 hash 算法找到其在数组中的存储位置,再根据 equals 方法从该位置上的链表中取出该Entry。
当 HashMap 中的元素愈来愈多的时候,hash 冲突的概率也就愈来愈高,由于数组的长度是固定的。因此为了提升查询的效率,就要对 HashMap 的数组进行扩容,数组扩容这个操做也会出如今 ArrayList 中,这是一个经常使用的操做,而在 HashMap 数组扩容以后,最消耗性能的点就出现了:原数组中的数据必须从新计算其在新数组中的位置,并放进去,这就是 resize。
那么 HashMap 何时进行扩容呢?当 HashMap 中的元素个数超过数组大小 *loadFactor
时,就会进行数组扩容,loadFactor的默认值为 0.75,这是一个折中的取值。也就是说,默认状况下,数组大小为 16,那么当 HashMap 中元素个数超过 16*0.75=12
的时候,就把数组的大小扩展为 2*16=32
,即扩大一倍,而后从新计算每一个元素在数组中的位置,而这是一个很是消耗性能的操做,因此若是咱们已经预知 HashMap 中元素的个数,那么预设元素的个数可以有效的提升 HashMap 的性能。
HashMap 包含以下几个构造器:
HashMap 的基础构造器 HashMap(int initialCapacity, float loadFactor) 带有两个参数,它们是初始容量 initialCapacity 和负载因子 loadFactor。
负载因子 loadFactor 衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来讲,查找一个元素的平均时间是 O(1+a),所以若是负载因子越大,对空间的利用更充分,然然后果是查找效率的下降;若是负载因子过小,那么散列表的数据将过于稀疏,对空间形成严重浪费。
HashMap 的实现中,经过 threshold 字段来判断 HashMap 的最大容量:
threshold = (int)(capacity * loadFactor);
结合负载因子的定义公式可知,threshold 就是在此 loadFactor 和 capacity 对应下容许的最大元素数目,超过这个数目就从新 resize,以下降实际的负载因子。默认的的负载因子 0.75 是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize 后的 HashMap 容量是容量的两倍:
咱们知道 java.util.HashMap 不是线程安全的,所以若是在使用迭代器的过程当中有其余线程修改了 map,那么将抛出 ConcurrentModificationException,这就是所谓 fail-fast 策略。
ail-fast 机制是 java 集合(Collection)中的一种错误机制。 当多个线程对同一个集合的内容进行操做时,就可能会产生 fail-fast 事件。
例如:当某一个线程 A 经过 iterator去遍历某集合的过程当中,若该集合的内容被其余线程所改变了;那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。
这一策略在源码中的实现是经过 modCount 域,modCount 顾名思义就是修改次数,对 HashMap 内容(固然不只仅是 HashMap 才会有,其余例如 ArrayList 也会)的修改都将增长这个值(你们能够再回头看一下其源码,在不少操做中都有 modCount++ 这句),那么在迭代器初始化过程当中会将这个值赋给迭代器的 expectedModCount。
在迭代过程当中,判断 modCount 跟 expectedModCount 是否相等,若是不相等就表示已经有其余线程修改了 Map:
注意到 modCount 声明为 volatile,保证线程之间修改的可见性。
在 HashMap 的 API 中指出:
由全部 HashMap 类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器建立以后,若是从结构上对映射进行修改,除非经过迭代器自己的 remove 方法,其余任什么时候间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。所以,面对并发的修改,迭代器很快就会彻底失败,而不冒在未来不肯定的时间发生任意不肯定行为的风险。
注意,迭代器的快速失败行为不能获得保证,通常来讲,存在非同步的并发修改时,不可能做出任何坚定的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。所以,编写依赖于此异常的程序的作法是错误的,正确作法是:迭代器的快速失败行为应该仅用于检测程序错误。
在上文中也提到,fail-fast 机制,是一种错误检测机制。它只能被用来检测错误,由于 JDK 并不保证 fail-fast 机制必定会发生。若在多线程环境下使用 fail-fast 机制的集合,建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。
效率高,之后必定要使用此种方式!
效率低,之后尽可能少使用!
对于 HashSet 而言,它是基于 HashMap 实现的,底层采用 HashMap 来保存元素,因此若是对 HashMap 比较熟悉了,那么学习 HashSet 也是很轻松的。
咱们先经过 HashSet 最简单的构造函数和几个成员变量来看一下,证实我们上边说的,其底层是 HashMap:
其实在英文注释中已经说的比较明确了。首先有一个HashMap的成员变量,咱们在 HashSet 的构造函数中将其初始化,默认状况下采用的是 initial capacity为16,load factor 为 0.75。
对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存全部元素,所以 HashSet 的实现比较简单,相关 HashSet 的操做,基本上都是直接调用底层 HashMap 的相关方法来完成,咱们应该为保存到 HashSet 中的对象覆盖 hashCode() 和 equals()
若是此 set 中还没有包含指定元素,则添加指定元素。更确切地讲,若是此 set 没有包含知足(e==null ? e2==null : e.equals(e2)) 的元素 e2,则向此 set 添加指定的元素 e。若是此 set 已包含该元素,则该调用不更改 set 并返回 false。但底层实际将将该元素做为 key 放入 HashMap。思考一下为何?
因为 HashMap 的 put() 方法添加 key-value 对时,当新放入 HashMap 的 Entry 中 key 与集合中原有 Entry 的 key 相同(hashCode()返回值相等,经过 equals 比较也返回 true),新添加的 Entry 的 value 会将覆盖原来 Entry 的 value(HashSet 中的 value 都是PRESENT
),但 key 不会有任何改变,所以若是向 HashSet 中添加一个已经存在的元素时,新添加的集合元素将不会被放入 HashMap中,原来的元素也不会有任何改变,这也就知足了 Set 中元素不重复的特性。
该方法若是添加的是在 HashSet 中不存在的,则返回 true;若是添加的元素已经存在,返回 false。其缘由在于咱们以前提到的关于 HashMap 的 put 方法。该方法在添加 key 不重复的键值对的时候,会返回 null。
和 HashMap 同样,Hashtable 也是一个散列表,它存储的内容是键值对。
Hashtable 在 Java 中的定义为:
从源码中,咱们能够看出,Hashtable 继承于 Dictionary 类,实现了 Map, Cloneable, java.io.Serializable接口。其中Dictionary类是任何可将键映射到相应值的类(如 Hashtable)的抽象父类,每一个键和值都是对象(源码注释为:The Dictionary
class is the abstract parent of any class, such as Hashtable
, which maps keys to values. Every key and every value is an object.)。但在这一点我开始有点怀疑,由于我查看了HashMap以及TreeMap的源码,都没有继承于这个类。不过当我看到注释中的解释也就明白了,其 Dictionary 源码注释是这样的:NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class. 该话指出 Dictionary 这个类过期了,新的实现类应该实现Map接口。
Hashtable是经过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, count, threshold, loadFactor, modCount。
关于变量的解释在源码注释中都有,最好仍是应该看英文注释。
Hashtable 一共提供了 4 个构造方法:
public Hashtable(int initialCapacity, float loadFactor)
: 用指定初始容量和指定加载因子构造一个新的空哈希表。useAltHashing 为 boolean,其若是为真,则执行另外一散列的字符串键,以减小因为弱哈希计算致使的哈希冲突的发生。public Hashtable(int initialCapacity)
:用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。public Hashtable()
:默认构造函数,容量为 11,加载因子为 0.75。public Hashtable(Map<? extends K, ? extends V> t)
:构造一个与给定的 Map 具备相同映射关系的新哈希表。put 方法的整个流程为:
我在下面的代码中也进行了一些注释:
经过一个实际的例子来演示一下这个过程:
假设咱们如今Hashtable的容量为5,已经存在了(5,5),(13,13),(16,16),(17,17),(21,21)这 5 个键值对,目前他们在Hashtable中的位置以下:
如今,咱们插入一个新的键值对,put(16,22),假设key=16的索引为1.但如今索引1的位置有两个Entry了,因此程序会对链表进行迭代。迭代的过程当中,发现其中有一个Entry的key和咱们要插入的键值对的key相同,因此如今会作的工做就是将newValue=22替换oldValue=16,而后返回oldValue=16.
而后咱们如今再插入一个,put(33,33),key=33的索引为3,而且在链表中也不存在key=33的Entry,因此将该节点插入链表的第一个位置。
相比较于 put 方法,get 方法则简单不少。其过程就是首先经过 hash()方法求得 key 的哈希值,而后根据 hash 值获得 index 索引(上述两步所用的算法与 put 方法都相同)。而后迭代链表,返回匹配的 key 的对应的 value;找不到则返回 null。
Hashtable 有多种遍历方式:
HashMap 是无序的,HashMap 在 put 的时候是根据 key 的 hashcode 进行 hash 而后放入对应的地方。因此在按照必定顺序 put 进 HashMap 中,而后遍历出 HashMap 的顺序跟 put 的顺序不一样(除非在 put 的时候 key 已经按照 hashcode 排序号了,这种概率很是小)
JAVA 在 JDK1.4 之后提供了 LinkedHashMap 来帮助咱们实现了有序的 HashMap!
LinkedHashMap 是 HashMap 的一个子类,它保留插入的顺序,若是须要输出的顺序和输入时的相同,那么就选用 LinkedHashMap。
LinkedHashMap 是 Map 接口的哈希表和连接列表实现,具备可预知的迭代顺序。此实现提供全部可选的映射操做,并容许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
LinkedHashMap 实现与 HashMap 的不一样之处在于,LinkedHashMap 维护着一个运行于全部条目的双重连接列表。此连接列表定义了迭代顺序,该迭代顺序能够是插入顺序或者是访问顺序。
注意,此实现不是同步的。若是多个线程同时访问连接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
根据链表中元素的顺序能够分为:按插入顺序的链表,和按访问顺序(调用 get 方法)的链表。默认是按插入顺序排序,若是指定按访问顺序排序,那么调用get方法后,会将此次访问的元素移至链表尾部,不断访问能够造成按访问顺序排序的链表。
我在最开始学习 LinkedHashMap 的时候,看到访问顺序、插入顺序等等,有点晕了,随着后续的学习才慢慢懂得其中原理,因此我会先在进行作几个 demo 来演示一下 LinkedHashMap 的使用。看懂了其效果,而后再来研究其原理。
看下面这个代码:
一个比较简单的测试 HashMap 的代码,经过控制台的输出,咱们能够看到 HashMap 是没有顺序的。
咱们如今将 map 的实现换成 LinkedHashMap,其余代码不变:Map<String, String> map = new LinkedHashMap<String, String>();
看一下控制台的输出:
咱们能够看到,其输出顺序是完成按照插入顺序的!也就是咱们上面所说的保留了插入的顺序。咱们不是在上面还提到过其能够按照访问顺序进行排序么?好的,咱们仍是经过一个例子来验证一下:
代码与以前的都差很少,但咱们多了两行代码,而且初始化 LinkedHashMap 的时候,用的构造函数也不相同,看一下控制台的输出结果:
这也就是咱们以前提到过的,LinkedHashMap 能够选择按照访问顺序进行排序。
对于 LinkedHashMap 而言,它继承与 HashMap(public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
)、底层使用哈希表与双向链表来保存全部元素。其基本操做与父类 HashMap 类似,它经过重写父类相关的方法,来实现本身的连接列表特性。下面咱们来分析 LinkedHashMap 的源代码:
LinkedHashMap 采用的 hash 算法和 HashMap 相同,可是它从新定义了数组中保存的元素 Entry,该 Entry 除了保存当前对象的引用外,还保存了其上一个元素 before 和下一个元素 after 的引用,从而在哈希表的基础上又构成了双向连接列表。看源代码:
LinkedHashMap 中的 Entry 集成与 HashMap 的 Entry,可是其增长了 before 和 after 的引用,指的是上一个元素和下一个元素的引用。
经过源代码能够看出,在 LinkedHashMap 的构造方法中,实际调用了父类 HashMap 的相关构造方法来构造一个底层存放的 table 数组,但额外能够增长 accessOrder 这个参数,若是不设置,默认为 false,表明按照插入顺序进行迭代;固然能够显式设置为 true,表明以访问顺序进行迭代。如:
咱们已经知道 LinkedHashMap 的 Entry 元素继承 HashMap 的 Entry,提供了双向链表的功能。在上述 HashMap 的构造器中,最后会调用 init() 方法,进行相关的初始化,这个方法在 HashMap 的实现中并没有意义,只是提供给子类实现相关的初始化调用。
但在 LinkedHashMap 重写了 init() 方法,在调用父类的构造方法完成构造后,进一步实现了对其元素 Entry 的初始化操做。
LinkedHashMap 并未重写父类 HashMap 的 put 方法,而是重写了父类 HashMap 的 put 方法调用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了本身特有的双向连接列表的实现。咱们在以前的文章中已经讲解了HashMap的put方法,咱们在这里从新贴一下 HashMap 的 put 方法的源代码:
HashMap.put:
重写方法:
LinkedHashMap 重写了父类 HashMap 的 get 方法,实际在调用父类 getEntry() 方法取得查找的元素后,再判断当排序模式 accessOrder 为 true 时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。因为的链表的增长、删除操做是常量级的,故并不会带来性能的损失。
LinkedHashMap 定义了排序模式 accessOrder,该属性为 boolean 型变量,对于访问顺序,为 true;对于插入顺序,则为 false。通常状况下,没必要指定排序模式,其迭代顺序即为默认为插入顺序。
这些构造方法都会默认指定排序模式为插入顺序。若是你想构造一个 LinkedHashMap,并打算按从近期访问最少到近期访问最多的顺序(即访问顺序)来保存元素,那么请使用下面的构造方法构造 LinkedHashMap:public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
该哈希映射的迭代顺序就是最后访问其条目的顺序,这种映射很适合构建 LRU 缓存。LinkedHashMap 提供了 removeEldestEntry(Map.Entry<K,V> eldest) 方法。该方法能够提供在每次添加新条目时移除最旧条目的实现程序,默认返回 false,这样,此映射的行为将相似于正常映射,即永远不能移除最旧的元素。
咱们会在后面的文章中详细介绍关于如何用 LinkedHashMap 构建 LRU 缓存。
其实 LinkedHashMap 几乎和 HashMap 同样:从技术上来讲,不一样的是它定义了一个 Entry<K,V> header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 经过继承 hashMap 中的 Entry<K,V>,并添加两个属性 Entry<K,V> before,after,和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。
在写关于 LinkedHashMap 的过程当中,记起来以前面试的过程当中遇到的一个问题,也是问我 Map 的哪一种实现能够作到按照插入顺序进行迭代?当时脑子是忽然短路的,但如今想一想,也只能怪本身对这个知识点仍是掌握的不够扎实,因此又从头认真的把代码看了一遍。
不过,个人建议是,你们首先首先须要记住的是:LinkedHashMap 可以作到按照插入顺序或者访问顺序进行迭代,这样在咱们之后的开发中遇到类似的问题,才能想到用 LinkedHashMap 来解决,不然就算对其内部结构很是了解,不去使用也是没有什么用的。
思考了很久,到底要不要总结 LinkedHashSet 的内容 = = 我在以前的博文中,分别写了 HashMap 和 HashSet,而后咱们能够看到 HashSet 的方法基本上都是基于 HashMap 来实现的,说白了,HashSet内部的数据结构就是一个 HashMap,其方法的内部几乎就是在调用 HashMap 的方法。
LinkedHashSet 首先咱们须要知道的是它是一个 Set 的实现,因此它其中存的确定不是键值对,而是值。此实现与 HashSet 的不一样之处在于,LinkedHashSet 维护着一个运行于全部条目的双重连接列表。此连接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
看到上面的介绍,是否是感受其与 HashMap 和 LinkedHashMap 的关系很像?
注意,此实现不是同步的。若是多个线程同时访问连接的哈希Set,而其中至少一个线程修改了该 Set,则它必须保持外部同步。
在LinkedHashMap的实现原理中,经过例子演示了 HashMap 和 LinkedHashMap 的区别。触类旁通,咱们如今学习的LinkedHashSet与以前的很相同,只不过以前存的是键值对,而如今存的只有值。
因此我就再也不具体的贴代码在这边了,但咱们能够确定的是,LinkedHashSet 是能够按照插入顺序或者访问顺序进行迭代。
对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。
LinkedHashSet 底层使用 LinkedHashMap 来保存全部元素,它继承与 HashSet,其全部的方法操做上又与 HashSet 相同,所以 LinkedHashSet 的实现上很是简单,只提供了四个构造方法,并经过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操做上与父类 HashSet 的操做相同,直接调用父类 HashSet 的方法便可。LinkedHashSet 的源代码以下:
以上几乎就是 LinkedHashSet 的所有代码了,那么读者可能就会怀疑了,不是说 LinkedHashSet 是基于 LinkedHashMap 实现的吗?那我为何在源码中甚至都没有看到出现过 LinkedHashMap。不要着急,咱们能够看到在 LinkedHashSet 的构造方法中,其调用了父类的构造方法。咱们能够进去看一下:
在父类 HashSet 中,专为 LinkedHashSet 提供的构造方法以下,该方法为包访问权限,并未对外公开。
由上述源代码可见,LinkedHashSet 经过继承 HashSet,底层使用 LinkedHashMap,以很简单明了的方式来实现了其自身的全部功能。
以上就是关于 LinkedHashSet 的内容,咱们只是从概述上以及构造方法这几个方面介绍了,并非咱们不想去深刻其读取或者写入方法,而是其自己没有实现,只是继承于父类 HashSet 的方法。
因此咱们须要注意的点是:
ArrayList 能够理解为动态数组,用 MSDN 中的说法,就是 Array 的复杂版本。与 Java 中的数组相比,它的容量能动态增加。ArrayList 是 List 接口的可变数组的实现。实现了全部可选列表操做,并容许包括 null 在内的全部元素。除了实现 List 接口外,此类还提供一些方法来操做内部用来存储列表的数组的大小。(此类大体上等同于 Vector 类,除了此类是不一样步的。)
每一个 ArrayList 实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它老是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增加。自动增加会带来数据向新数组的从新拷贝,所以,若是可预知数据量的多少,可在构造 ArrayList 时指定其容量。在添加大量元素前,应用程序也可使用 ensureCapacity 操做来增长 ArrayList 实例的容量,这能够减小递增式再分配的数量。
注意,此实现不是同步的。若是多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操做,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)
咱们先学习了解其内部的实现原理,才能更好的理解其应用。
对于 ArrayList 而言,它实现 List 接口、底层使用数组保存全部元素。其操做基本上是对数组的操做。下面咱们来分析 ArrayList 的源代码:
ArrayList 继承了 AbstractList,实现了 List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
ArrayList 实现了 RandmoAccess 接口,即提供了随机访问功能。RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在 ArrayList 中,咱们便可以经过元素的序号快速获取元素对象;这就是快速随机访问。
ArrayList 实现了 Cloneable 接口,即覆盖了函数 clone(),能被克隆。 ArrayList 实现 java.io.Serializable 接口,这意味着 ArrayList 支持序列化,能经过序列化去传输。
ArrayList 提供了三种方式的构造器:
public ArrayList()
能够构造一个默认初始容量为10的空列表;public ArrayList(int initialCapacity)
构造一个指定初始容量的空列表;public ArrayList(Collection<? extends E> c)
构造一个包含指定 collection 的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。ArrayList 中提供了多种添加元素的方法,下面将一一进行讲解:
1.set(int index, E element):该方法首先调用rangeCheck(index)
来校验 index 变量是否超出数组范围,超出则抛出异常。然后,取出原 index 位置的值,而且将新的 element 放入 Index 位置,返回 oldValue。
2.add(E e):该方法是将指定的元素添加到列表的尾部。当容量不足时,会调用 grow 方法增加容量。
3.add(int index, E element):在 index 位置插入 element。
4.addAll(Collection<? extends E> c)
和 addAll(int index, Collection<? extends E> c)
:将特定 Collection 中的元素添加到 Arraylist 末尾。
在 ArrayList 的存储方法,其核心本质是在数组的某个位置将元素添加进入。但其中又会涉及到关于数组容量不够而增加等因素。
这个方法就比较简单了,ArrayList 可以支持随机访问的缘由也是很显然的,由于它内部的数据结构是数组,而数组自己就是支持随机访问。该方法首先会判断输入的index值是否越界,而后将数组的 index 位置的元素返回便可。
ArrayList 提供了根据下标或者指定对象两种方式的删除功能。须要注意的是该方法的返回值并不相同,以下:
注意:从数组中移除元素的操做,也会致使被移除的元素之后的全部元素的向左移动一个位置。
从上面介绍的向 ArrayList 中存储元素的代码中,咱们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,若是超出,数组将会进行扩容,以知足添加数据的需求。数组扩容有两个方法,其中开发者能够经过一个 public 的方法ensureCapacity(int minCapacity)
来增长 ArrayList 的容量,而在存储元素等操做过程当中,若是遇到容量不足,会调用priavte方法private void ensureCapacityInternal(int minCapacity)
实现。
从上述代码中能够看出,数组进行扩容时,会将老数组中的元素从新拷贝一份到新的数组中,每次数组容量的增加大约是其原容量的 1.5 倍(从int newCapacity = oldCapacity + (oldCapacity >> 1)
这行代码得出)。这种操做的代价是很高的,所以在实际使用时,咱们应该尽可能避免数组容量的扩张。当咱们可预知要保存的元素的多少时,要在构造 ArrayList 实例时,就指定其容量,以免数组扩容的发生。或者根据实际需求,经过调用ensureCapacity 方法来手动增长 ArrayList 实例的容量。
ArrayList 也采用了快速失败的机制,经过记录 modCount 参数来实现。在面对并发的修改时,迭代器很快就会彻底失败,而不是冒着在未来某个不肯定时间发生任意不肯定行为的风险。 关于 Fail-Fast 的更详细的介绍,我在以前将 HashMap 中已经提到。
LinkedList 和 ArrayList 同样,都实现了 List 接口,但其内部的数据结构有本质的不一样。LinkedList 是基于链表实现的(经过名字也能区分开来),因此它的插入和删除操做比 ArrayList 更加高效。但也是因为其为基于链表的,因此随机访问的效率要比 ArrayList 差。
看一下 LinkedList 的类的定义:
LinkedList 继承自 AbstractSequenceList,实现了 List、Deque、Cloneable、java.io.Serializable 接口。AbstractSequenceList 提供了List接口骨干性的实现以减小实现 List 接口的复杂度,Deque 接口定义了双端队列的操做。
在 LinkedList 中除了自己本身的方法外,还提供了一些可使其做为栈、队列或者双端队列的方法。这些方法可能彼此之间只是名字不一样,以使得这些名字在特定的环境中显得更加合适。
LinkedList 也是 fail-fast 的(前边提过不少次了)。
LinkedList 是基于链表结构实现,因此在类中包含了 first 和 last 两个指针(Node)。Node 中包含了上一个节点和下一个节点的引用,这样就构成了双向的链表。每一个 Node 只能知道本身的前一个节点和后一个节点,但对于链表来讲,这已经足够了。
该方法是在链表的 end 添加元素,其调用了本身的方法 linkLast(E e)。
该方法首先将 last 的 Node 引用指向了一个新的 Node(l),而后根据l新建了一个 newNode,其中的元素就为要添加的 e;然后,咱们让 last 指向了 newNode。接下来是自身进行维护该链表。
该方法是在指定 index 位置插入元素。若是 index 位置正好等于 size,则调用 linkLast(element) 将其插入末尾;不然调用 linkBefore(element, node(index))方法进行插入。该方法的实如今下面,你们能够本身仔细的分析一下。(分析链表的时候最好可以边画图边分析)
LinkedList 的方法实在是太多,在这无法一一举例分析。但不少方法其实都只是在调用别的方法而已,因此建议你们将其几个最核心的添加的方法搞懂就能够了,好比 linkBefore、linkLast。其本质也就是链表之间的删除添加等。
咱们在以前的博文中了解到关于 HashMap 和 Hashtable 这两种集合。其中 HashMap 是非线程安全的,当咱们只有一个线程在使用 HashMap 的时候,天然不会有问题,但若是涉及到多个线程,而且有读有写的过程当中,HashMap 就不能知足咱们的须要了(fail-fast)。在不考虑性能问题的时候,咱们的解决方案有 Hashtable 或者Collections.synchronizedMap(hashMap),这两种方式基本都是对整个 hash 表结构作锁定操做的,这样在锁表的期间,别的线程就须要等待了,无疑性能不高。
因此咱们在本文中学习一个 util.concurrent 包的重要成员,ConcurrentHashMap。
ConcurrentHashMap 的实现是依赖于 Java 内存模型,因此咱们在了解 ConcurrentHashMap 的前提是必须了解Java 内存模型。但 Java 内存模型并非本文的重点,因此我假设读者已经对 Java 内存模型有所了解。
ConcurrentHashMap 的结构是比较复杂的,都深究去本质,其实也就是数组和链表而已。咱们由浅入深慢慢的分析其结构。
先简单分析一下,ConcurrentHashMap 的成员变量中,包含了一个 Segment 的数组(final Segment<K,V>[] segments;
),而 Segment 是 ConcurrentHashMap 的内部类,而后在 Segment 这个类中,包含了一个 HashEntry 的数组(transient volatile HashEntry<K,V>[] table;
)。而 HashEntry 也是 ConcurrentHashMap 的内部类。HashEntry 中,包含了 key 和 value 以及 next 指针(相似于 HashMap 中 Entry),因此 HashEntry 能够构成一个链表。
因此通俗的讲,ConcurrentHashMap 数据结构为一个 Segment 数组,Segment 的数据结构为 HashEntry 的数组,而 HashEntry 存的是咱们的键值对,能够构成链表。
首先,咱们看一下 HashEntry 类。
HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。其类的定义为:
HashEntry 的学习能够类比着 HashMap 中的 Entry。咱们的存储键值对的过程当中,散列的时候若是发生“碰撞”,将采用“分离链表法”来处理碰撞:把碰撞的 HashEntry 对象连接成一个链表。
以下图,咱们在一个空桶中插入 A、B、C 两个 HashEntry 对象后的结构图(其实应该为键值对,在这进行了简化以方便更容易理解):
Segment 的类定义为static final class Segment<K,V> extends ReentrantLock implements Serializable
。其继承于 ReentrantLock 类,从而使得 Segment 对象能够充当锁的角色。Segment 中包含HashEntry 的数组,其能够守护其包含的若干个桶(HashEntry的数组)。Segment 在某些意义上有点相似于 HashMap了,都是包含了一个数组,而数组中的元素能够是一个链表。
table:table 是由 HashEntry 对象组成的数组若是散列时发生碰撞,碰撞的 HashEntry 对象就以链表的形式连接成一个链表table数组的数组成员表明散列映射表的一个桶每一个 table 守护整个 ConcurrentHashMap 包含桶总数的一部分若是并发级别为 16,table 则守护 ConcurrentHashMap 包含的桶总数的 1/16。
count 变量是计算器,表示每一个 Segment 对象管理的 table 数组(若干个 HashEntry 的链表)包含的HashEntry 对象的个数。之因此在每一个Segment对象中包含一个 count 计数器,而不在 ConcurrentHashMap 中使用全局的计数器,是为了不出现“热点域”而影响并发性。
咱们经过下图来展现一下插入 ABC 三个节点后,Segment 的示意图:
其实从我我的角度来讲,Segment结构是与HashMap很像的。
ConcurrentHashMap 的结构中包含的 Segment 的数组,在默认的并发级别会建立包含 16 个 Segment 对象的数组。经过咱们上面的知识,咱们知道每一个 Segment 又包含若干个散列表的桶,每一个桶是由 HashEntry 连接起来的一个链表。若是 key 可以均匀散列,每一个 Segment 大约守护整个散列表桶总数的 1/16。
下面咱们还有经过一个图来演示一下 ConcurrentHashMap 的结构:
在 ConcurrentHashMap 中,当执行 put 方法的时候,会须要加锁来完成。咱们经过代码来解释一下具体过程: 当咱们 new 一个 ConcurrentHashMap 对象,而且执行put操做的时候,首先会执行 ConcurrentHashMap 类中的 put 方法,该方法源码为:
咱们经过注释能够了解到,ConcurrentHashMap 不容许空值。该方法首先有一个 Segment 的引用 s,而后会经过 hash() 方法对 key 进行计算,获得哈希值;继而经过调用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法进行存储操做。该方法源码为:
关于该方法的某些关键步骤,在源码上加上了注释。
须要注意的是:加锁操做是针对的 hash 值对应的某个 Segment,而不是整个 ConcurrentHashMap。由于 put 操做只是在这个 Segment 中完成,因此并不须要对整个 ConcurrentHashMap 加锁。因此,此时,其余的线程也能够对另外的 Segment 进行 put 操做,由于虽然该 Segment 被锁住了,但其余的 Segment 并无加锁。同时,读线程并不会由于本线程的加锁而阻塞。
正是由于其内部的结构以及机制,因此 ConcurrentHashMap 在并发访问的性能上要比Hashtable和同步包装以后的HashMap的性能提升不少。在理想状态下,ConcurrentHashMap 能够支持 16 个线程执行并发写操做(若是并发级别设置为 16),及任意数量线程的读操做。
在实际的应用中,散列表通常的应用场景是:除了少数插入操做和删除操做外,绝大多数都是读取操做,并且读操做在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap 针对读操做作了大量的优化。经过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,使得 大多数时候,读操做不须要加锁就能够正确得到值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提升。
ConcurrentHashMap 是一个并发散列映射表的实现,它容许彻底并发的读取,而且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不一样线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也致使对容器的访问变成串行化的了。
ConcurrentHashMap 的高并发性主要来自于三个方面:
使用分离锁,减少了请求 同一个锁的频率。
经过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操做大多数时候不须要加锁就能成功获取到须要的值。因为散列映射表在实际应用中大多数操做都是成功的 读操做,因此 2 和 3 既能够减小请求同一个锁的频率,也能够有效减小持有锁的时间。经过减少请求同一个锁的频率和尽可能减小持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提升。
咱们平时总会有一个电话本记录全部朋友的电话,可是,若是有朋友常常联系,那些朋友的电话号码不用翻电话本咱们也能记住,可是,若是长时间没有联系了,要再次联系那位朋友的时候,咱们又不得不求助电话本,可是,经过电话本查找仍是很费时间的。可是,咱们大脑可以记住的东西是必定的,咱们只能记住本身最熟悉的,而长时间不熟悉的天然就忘记了。
其实,计算机也用到了一样的一个概念,咱们用缓存来存放之前读取的数据,而不是直接丢掉,这样,再次读取的时候,能够直接在缓存里面取,而不用再从新查找一遍,这样系统的反应能力会有很大提升。可是,当咱们读取的个数特别大的时候,咱们不可能把全部已经读取的数据都放在缓存里,毕竟内存大小是必定的,咱们通常把最近常读取的放在缓存里(至关于咱们把最近联系的朋友的姓名和电话放在大脑里同样)。
LRU 缓存利用了这样的一种思想。LRU 是 Least Recently Used 的缩写,翻译过来就是“最近最少使用”,也就是说,LRU 缓存把最近最少使用的数据移除,让给最新读取的数据。而每每最常读取的,也是读取次数最多的,因此,利用 LRU 缓存,咱们可以提升系统的 performance。
要实现 LRU 缓存,咱们首先要用到一个类 LinkedHashMap。
用这个类有两大好处:一是它自己已经实现了按照访问顺序的存储,也就是说,最近读取的会放在最前面,最最不常读取的会放在最后(固然,它也能够实现按照插入顺序存储)。第二,LinkedHashMap 自己有一个方法用于判断是否须要移除最不常读取的数,可是,原始方法默认不须要移除(这是,LinkedHashMap 至关于一个linkedlist),因此,咱们须要 override 这样一个方法,使得当缓存里存放的数据个数超过规定个数后,就把最不经常使用的移除掉。关于 LinkedHashMap 中已经有详细的介绍。
代码以下:(可直接复制,也能够经过LRUcache-Java下载)
HashMap 和 HashSet 都是 collection 框架的一部分,它们让咱们可以使用对象的集合。collection 框架有本身的接口和实现,主要分为 Set 接口,List 接口和 Queue 接口。它们有各自的特色,Set 的集合里不容许对象有重复的值,List 容许有重复,它对集合中的对象进行索引,Queue 的工做原理是 FCFS 算法(First Come, First Serve)。
首先让咱们来看看什么是 HashMap 和 HashSet,而后再来比较它们之间的分别。
HashSet 实现了 Set 接口,它不容许集合中有重复的值,当咱们提到 HashSet 时,第一件事情就是在将对象存储在 HashSet 以前,要先确保对象重写 equals()和 hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。若是咱们没有重写这两个方法,将会使用这个方法的默认实现。
public boolean add(Object o)
方法用来在 Set 中添加元素,当元素值重复时则会当即返回 false,若是成功添加的话会返回 true。
HashMap 实现了 Map 接口,Map 接口对键值对进行映射。Map 中不容许重复的键。Map 接口有两个基本的实现,HashMap 和 TreeMap。TreeMap 保存了对象的排列次序,而 HashMap 则不能。HashMap 容许键和值为 null。HashMap 是非 synchronized 的,但 collection 框架提供方法能保证 HashMap synchronized,这样多个线程同时访问 HashMap 时,能保证只有一个线程更改 Map。
public Object put(Object Key,Object value)
方法用来将元素添加到 map 中。
HashMap | HashSet |
---|---|
HashMap实现了Map接口 | HashSet实现了Set接口 |
HashMap储存键值对 | HashSet仅仅存储对象 |
使用put()方法将元素放入map中 | 使用add()方法将元素放入set中 |
HashMap中使用键对象来计算hashcode值 | HashSet使用成员对象来计算hashcode值,对于两个对象来讲hashcode可能相同,因此equals()方法用来判断对象的相等性,若是两个对象不一样的话,那么返回false |
HashMap比较快,由于是使用惟一的键来获取对象 | HashSet较HashMap来讲比较慢 |
ct o)
方法用来在 Set 中添加元素,当元素值重复时则会当即返回 false,若是成功添加的话会返回 true。
HashMap 实现了 Map 接口,Map 接口对键值对进行映射。Map 中不容许重复的键。Map 接口有两个基本的实现,HashMap 和 TreeMap。TreeMap 保存了对象的排列次序,而 HashMap 则不能。HashMap 容许键和值为 null。HashMap 是非 synchronized 的,但 collection 框架提供方法能保证 HashMap synchronized,这样多个线程同时访问 HashMap 时,能保证只有一个线程更改 Map。
public Object put(Object Key,Object value)
方法用来将元素添加到 map 中。
HashMap | HashSet |
---|---|
HashMap实现了Map接口 | HashSet实现了Set接口 |
HashMap储存键值对 | HashSet仅仅存储对象 |
使用put()方法将元素放入map中 | 使用add()方法将元素放入set中 |
HashMap中使用键对象来计算hashcode值 | HashSet使用成员对象来计算hashcode值,对于两个对象来讲hashcode可能相同,因此equals()方法用来判断对象的相等性,若是两个对象不一样的话,那么返回false |
HashMap比较快,由于是使用惟一的键来获取对象 | HashSet较HashMap来讲比较慢 |