一、容器产生的原因
1.数组的缺点:大小一旦给定就无法更改,除非复制到一个新的数组中,开销大;而容器类都可以自动地调整自己的尺寸。
2.容器功能的多样性:容器可以实现各种不同要求,如按不同依据将元素进行排序或者保证容器内无重复元素等等。
关于容器的泛型参数:
当指定了某个类型作为类型参数时,则可以将该类型及其子类型的对象放入到容器当中,泛型保证了安全性。
二、Collection接口
Collection保存单一的元素,并且Collection继承了Iterable接口,而Map中保存键值对。
1.Collection接口中的方法
Collection中提供了判空、大小、遍历、是否包含某元素、是否包含其他Collection全部、添加某元素、添加其他Collecton全部、删除某元素、删除其他Collection全部(求差)、删除全部元素*以及、只保留与其他Collection重合部分(求交)、以及toArray方法(所有Collection实现类都能转换成数组并且如迭代器有是顺序那么数组中也是相同顺序的)。
2.Collection实现类中的构造函数
Collection一个重要的作用就是作为它的具体实现集合之间相互转换的中介,比较常用的Collection类如ArrayList、LinkedList、HashSet、LinkedHashSet、TreeSet中除了都有无参构造函数外还全部都有一个接受Collection作为参数的构造函数(LinkedList有且仅有这两个)。
其中ArrayList(10)、HashSet(16,0.75)、LinkedHashSet(16,0.75)都有一个在创建时指定容量的构造函数,对于ArrayList而言是因为其底层是基于数组实现的。
其中HashSet和LinkedHashSet(LinkedHashSet继承自HashSet)还多了一个可以同时指定容量和负载因子的构造函数,如不指定则默认是0.75。这是因为HashSet内部是以一个HashMap对象实现的(构造函数中创建赋给Map类型的成员变量map)、LinkedHashSet中是以一个LinkedHashMap对象实现的(构造函数中调用父类的一个默认访问权限级别的构造函数来创建然后同样赋给map),因为HashMap和LinkedHashMap都是用数组+(双向节点)链表来实现的,所以就有了容量和负载因子这两个参数,也相应地有了这两个构造函数。
其中TreeSet则有一个接受SortedSet作为参数的构造函数和一个接受比较器Comparator作为参数的构造函数。前者除了转换集合类型外还有个作用是可以按照原本SortedSet里的比较器来进行排序(如果存在),也就是说转换后新旧SortedSet里面的元素顺序是相同的。
3.遍历Collection实现类中三种方法
- 使用聚合操作(Aggregate Operations)
//待补充...话说思否不能设置字体颜色的么
- 使用foreach来进行遍历
- 使用Iterator()方法或者spliterator()方法所返回的顺序迭代器和并行迭代器。当我们需要在遍历过程中删除元素或者需要并行遍历Collection时都必须使用Iterator。
三、List接口及其实现类
Collection中的List有三个特点:1.可以允许重复的对象。2.可以插入多个null元素。3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。List重新规定了equals和hashCode方法的实现,这使得equals可以用来不同类型之间的List实现类对象之间来比较所包含元素是否完全相同,这个相同是按顺序相同的,即54321与12345是不相同的。
常用的实现类有ArrayList和LinkedList,当需要大量的随机访问则使用ArrayList,当需要经常从表前半部分插入和删除元素则应该根据靠前程度使用LinkedList(因为对于ArrayList而言插入或者删除元素的位置越靠前,需要复制元素的次数就越接近size(添加是size-i删除是size-i-1);对于LinkedList而言,它是根据位置位于前半部分还是后半部分来选则是从前往后遍历找还是从后往前找,对它而言位于插入或者删除中间的元素反而是效率最低的。所以前部分是LinkedList比ArrayList效率更高的部分)。
除了继承自Collection的方法,List接口还额外增加了以下方法:
- 索引访问:可以根据索引进行get、set、add、addAll和remove
- 查找元素:indexof和lastIndexof
- 迭代器:
新增了一个可以返回更适合List的迭代器对象的方法listIterator(Iterator的子类)。ListIterator允许从任一方向来遍历List对象,并在遍历(迭代)过程中进行修改该List对象,还能获得迭代器的当前位置。hasNext、next是正向遍历,hasPrevious、previous是逆向遍历。listIterator有两个版本,一个是无参数的,会返回一个游标指向List开头的ListIterator,另一个是带有一个int参数的,会返回一个游标指向指定位置的ListIterator。
混合调用next和previous会返回同一个对象,extIndex返回的是下次调用next所要返回的元素的位置,previousIndex返回的是下次调用previous所要返回的元素的位置。在同一游标位置nextIndex总是比previousIndex更大的,以下是两种边界情况:一种返回-1另一种返回list.size() (1) a call to previousIndex when the cursor is before the initial element returns -1 and (2) a call to nextIndex when the cursor is after the final element returns list.size().
- 范围视图:
subList,返回的List(banked)是由原本的List(banking)所支持的,因此对原本数组元素的更改会反映到返回的数组当中。任何能对List使用的方法对返回的subList一样能够使用该方法可得到一个banked List,但所有的方法都还是应该推荐在baking List上使用,而不是banked List。
因为一旦你通过banking List或者另一个由subList得到的banked List2对链表进行了结构修改(其实就是增删元素,修改元素的内容并没有影响),那么该baked List的所有public方法(除了subList外和一些继承自Object的方法外),都会在运行时报ConcurrentModificationException异常。
Arrays.asList方法:可以让一个数组被看做是一个List,但该List底层的实现还是原本的数组,对List中元素的更改也就是对数组的更改。数组是无法调整大小的,也因此,这个List无法add或者remove元素。
1. ArrayList
ArrayList:内部使用了一个名为elementData的Object数组引用变量(后面以数组变量称呼),在ArrayList对象创建之后并且还没有添加元素之前,该变量默认指向一个static(所以变量位于方法区) final的引用变量DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这个引用变量指向一个空的数组对象,这是为了避免我们反复去创建未使用的数组,如果当我们反复创建无用的数组了,那么它们其中的elementData就全都顺着引用指向着那一个空的数组对象了。
当我们首次添加一个元素时,就会新建大小为默认值10的Object数组对象传给数组变量,然后将元素放进去。但这个时候size只会是1而不是10,因为size指代的是真正存放的元素的数量而不是容量。而当每次size刚超过容量时就会进行1.5倍的扩容,比如我们有了10个元素装满了,现在添加第11个元素的时候就会把数组扩容成15,然后再将原数组复制过去以及第11个元素放进去,size变成11。实际上复制操作最底层都是通过System.arraycopy()来实现的,也就是直接赋值的浅拷贝(可见笔记关于Object的clone方法、浅拷贝、深拷贝),因为native方法效率比循环复制要高。
当在对ArrayList根据索引进行一次插入(复制次数size-i)或者删除(复制次数size-i-1)元素的时候,索引越靠后,需要复制的次数越少,效率越高,索引越靠前需要复制的次数越多,效率越低。
2. LinkedList
LinkedList就是一个双向链表的实现,它同时实现了List接口和Deque接口,也是说它即可以看作一个顺序链表,又可以看做一个队列(Queue),同时又可以看做一个栈(Satck)。
顺便谈下关于栈,在java中有个现成的栈类,就是java.util.Stack这个类,但这个类java官方在api中也指出不再推荐使用。而是在需要使用这种先进后出的栈时推荐使用Deque接口下的实现类,比如这里的LinkedList,但更推荐的是ArrayDeque。
LinkedList对指定位置的增删查改,都会通过与size>>1(表示size/2,使用移位运算提升代码的运行效率)的相比较的方式来选择从前遍历还是从后遍历。所以当要增删查改的位置刚好位于中间时,效率是最低的。
四、Set接口及其实现类
Set的特点是不接受重复元素,其中TreeSet不接受null,因为TreeSet是用TreeMap实现的,TreeMap其实就是个红黑树,而在红黑树当中是不能插入一个空节点的;其他两个HashSet和LinkedHashSet则可以接受null元素。Set重新规定了equals和hashCode方法的实现,这使得equals可以用来不同类型之间的Set实现类对象之间来比较所包含元素是否完全相同(与List相比不用顺序相同)。
HashSet提供最快的查询速度,而TreeSet保持元素处于排序状态,LinkedHashSet以插入顺序保存元素。其中LinkedHashSet是HashSet的子类。
1. HashSet
HashSet底层是用一个HashMap来实现的,这个HashMap的所有键值映射的值都是同一个对象(一个Obect对象),就是下面代码当中的final修饰的PRESENT。
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
//这里的0.75还有16都是与HashMap中默认加载因子和默认容量是一致的
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
HashSet中的大小、增、删、查、遍历等操作都是通过map来进行的,代码如下所示,值的提一下的是remove方法,因为在HashSet里面的所有元素作为键对应值都是PRESENT,在map的remove方法当中会返回删除元素的值(在这里一定就是PRESENT了)或者null,所以用返回的值与PRESENT进行比较也是可以得出是否删除成功也是可以的。
public Iterator<E> iterator() {
return map.keySet().iterator();
}
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}
最后说下HashSet的专门为LinkedHashSet预留的一个构造函数,这是一个包访问权限的构造函数,实际上被设置为只被LinkedHashSet所调用到了,因为LinkedHashSet继承了HashSet。这个构造函数是将返回了一个LinkedHashMap对象给map,这也是LinkedHashMap的存储实现原理。
/**
* Constructs a new, empty linked hash set. (This package private
* constructor is only used by LinkedHashSet.) The backing
* HashMap instance is a LinkedHashMap with the specified initial
* capacity and the specified load factor.
*
* @param initialCapacity the initial capacity of the hash map
* @param loadFactor the load factor of the hash map
* @param dummy ignored (distinguishes this
* constructor from other int, float constructor.)
* @throws IllegalArgumentException if the initial capacity is less
* than zero, or if the load factor is nonpositive
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
2. LinkedHashSet
正如前面所述,LinkedHashSet是继承了HashSet的,大部分操作也是直接继承的,只有少部分自己的方法,并且构造器方法都是想上调用了HashSet的那个创建一个LinkedHashMap的构造器方法,如下所示。所以LinkedHashSet底层是用LinkedHashMap来实现的,
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
3. TreeSet
TreeSet底层是用TreeMap来实现的,如下面代码所示在构造器函数中创建了一个TreeMap对象,并将其赋值给了m(NavigableMap接口类型,TreeMap也实现了该类),与HashSet同样的做法:将键作为元素,值则都指向PRESENT,同样对应的查找删除添加操作也都是调用TreeMap来完成的,这里不再重复说明。
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
五、Queue接口及其实现类
Queue常用的实现类是ArrayDeque和LinkedList,ArrayDeque是Deque 接口的大小可变数组的实现。数组双端队列没有容量限制;它们可根据需要增加以支持使用。它们不是线程安全的;在没有外部同步时,它们不支持多个线程的并发访问。禁止 null 元素。此类很可能在用作堆栈时快于Stack,在用作队列时快于LinkedList。
Queue中的三类方法如下:
- 插入:The addfirst and offerFirst methods insert elements at the beginning of the Deque instance. The addLast and offerLast methods insert elements at the end of theDeque instance. When the capacity of the Deque instance is restricted, the preferred methods are offerFirst and offerLast because addFirst might fail to throw an exception if it is full.
- 删除:The removeFirst and pollFirst methods remove elements from the beginning of the Deque instance. The removeLast and pollLast methods remove elements from the end. The methods pollFirst and pollLast return null if the Deque is empty whereas the methods removeFirst and removeLast throw an exception if the Deque instance is empty.
- 查询:The methods getFirst and peekFirst retrieve the first element of the Deque instance. These methods dont remove the value from the Deque instance. Similarly, themethods getLast and peekLast retrieve the last element. The methods getFirst and getLast throw an exception if the deque instance is empty whereas the methods
peekFirst and peekLast return NULL.
六、Map接口及其实现类
(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。
1. HashMap
关于HashMap主要看这篇就好了Java 8系列之重新认识HashMap,然后下面是从文中稍微摘取了自认为几个比较重要的点吧:
- 存储结构:HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
- 插入:HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。
- 容量与加载因子:HashMap默认的容量即数组的长度是16,加载因子是0.75,对应的阈值为threshold=length*Load factor即12,当数组中存储的Node个数超过了12时就会进行两倍的扩容。为什么默认数组长度是16这个在后面的散列值计算方法里面有说。为什么加载因子是0.75,原因在于这是一个是对空间和时间效率的一个平衡选择,建议不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
这也符合散列表以空间换时间的特点,小于1的负载因子的存在就是为了让数组中的链表长度尽可能短,因为链表查找是更花费时间相比于数组的访问,负载因子如为1就是在键的hash值扰动取模后均匀分布的理想情况下,每个桶内的元素个数期望是1,链表长度就只是1,这是最理想的情况了。但实际上分布达不到这么均匀,为了减少链表长度把负载因子设置成了0.75来保证链表长度尽可能短。当然仅这一种减少时间的做法java官方可能还不够,如果就是出现了小概率事件某个桶内链表长度比较长怎么办,java8引入了树化桶方法。在某个桶内链表长度大于8时将其该链表转换为红黑树结构
- 散列值处理方法[12]:为了提高散列值取余(取模)的速度,HashMap中用了一个按位与运算的方式来代替,当除数是2的幂次方时,a%b与a&(b-1)的结果是等价。先假设低位是足够随机均匀的,取模运算参与运算的是低位,为了保证低位的所有位都被用到,就将数组长度取为了2的整次幂,这样数组长度-1的二进制码就全为1,就能使散列值的低位信息都能保留下来。
但仅仅直接是原始的hashCode值低位信息显然是不行的,hashCode值是为了保证整体均匀的(即尽可能不同的对象对应不同的散列码),低位可能并不怎么均匀,为了解决这种情况HashMap中会将原始的hashCode值与高16位进行一个异或(不同为1相同为0)操作,这样就混合了原始hashCode低位和高位,加大低位随机均匀性。然后用这个混合后的hash值再去进行按位与运算。
以上也就是为什么要使用扰动函数、默认容量为16以及自己设定的容量会被自动提升为最近的2次幂大小的原因。
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value(如果值不为null,不进行覆盖,这是为putIfAbsent这种插入方法准备的)
* @param evict if false, the table is in creation mode.
* evict参数是因为LinkedHashMap预留了一个可以链表中长度固定,并保持最新的N的节点数据的方法afterNodeInsertion(因为removeEldestEntry始终返回false所以目前并不生效),
* 可以通过重写removeEldestEntry来就能进行实现了。
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//看看对应桶上是不是已经有元素了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//桶中没有结点的话,就将其直接放进去
else {//桶中已有结点的话
Node<K,V> e; K k;
//则先看hash值是否相同(来到这个位置是根据的是hash扰动后的值,可能hash值就不一样),键的内存地址是否相同,键的内容是否相等。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//三个条件均满足了则说明键相等,要对这个结点(可能是链表的头结点,也可能是红黑树的根节点)进行更新了。不满足则要进行插入了
else if (p instanceof TreeNode)//如果是红黑树结点那就调用插入到红黑树中的方法
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//链表结点则以下
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//循环后发现没有相等键的结点,则插入到最后一个节点后面
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 保证了在插入后链表长度为9时就进入桶树化函数 treeify是树化的意思。
//但这个函数只会在数组长度大于等于64时进行将hash确定的这个桶内的链表转换成红黑树,对应结点也转换成了红黑树结点。
//如果数组长度小于64时就只会进行扩容操作了,而不是转换成红黑树,因为扩容后很可能链表长度就减少了
treeifyBin(tab, hash);
break;//树化桶执行完毕之后结束循环,并且这个时候的e是null
}
//在循环中查找有无键相等的结点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//看hash值是否相同(来到这个位置是根据的是hash扰动后的值,可能hash值就不一样),键的内存地址是否相同,键的内容是否相等。
break;//相等则从循环中跳出来,这个时候的e保存的是桶内键相等的结点的引用
p = e;
}
}
//e!=null说明e桶内键相等的结点的引用,则进行值的覆盖
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2. LinkedHashMap
LinkedHashMap是HashMap的子类,相比HashMap增加的就是更换了结点为自己的内部静态类LinkedHashMap.Entry,这个Entry继承自HashMap.Node,增加了before和after指针用来记录插入顺序。如下图[via9]所示
3. TreeMap
用红黑树实现,关于红黑树实现原理已经写过了就不再写了。
七、并发下的容器类
1.对于ArrayList和LinkedList
这两个List实现类都不是同步的。如果多个线程同时访问一个ArrayList或者LinkedList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)
这一般通过对封装该List的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedList方法将该列表“包装”起来。这最好在创建时完成,以防止意外对列表进行不同步的访问,如下所示:
List list = Collections.synchronizedList(new ArrayList(...));
List list = Collections.synchronizedList(new LinkedList(...));
2.对于HashSet、LinkedHashSet和TreeSet
注意,这三个Set实现类都不是同步的。如果多个线程同时访问HashSet、LinkedHashSet或者TreeSet,而其中至少一个线程修改了该set,则它必须保持外部同步。
这一般通过对封装该set的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedSet (对于TreeSet是Collections.synchronizedSortedSet)方法来“包装”该 set。最好在创建时完成这一操作,以防止意外的非同步访问:
Set s = Collections.synchronizedSet(new LinkedHashSet(...));
Set s = Collections.synchronizedSet(new HashSet(...));
SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));
3.对于HashMap、LinkedHashMap和TreeMap
注意,这三个Map实现类都不是同步的。如果多个线程同时访问一个HashMap、LinkedHashMap或者TreeMap,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap(对于TreeMap应该使用Collections.synchronizedSortedMap)方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:
Map m = Collections.synchronizedMap(new HashMap(...));
Map m = Collections.synchronizedMap(new LinkedHashMap(...));
SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));
(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。对于LinkedHashMap而言,当按访问排序的HashMap时,结构上的修改还包括影响迭代顺序的任何操作,但此时仅利用get查询LinkedHashMapMap不是结构修改)
参考文章:
Java™ Platform Standard Ed. 8 Api
在中间位置添加元素,ArrayList比LinkedList效率更高?
arraylist add(int index) 方法时 index是处于前半部分还是后半部分效率高
ArrayList初始化
ArrayList底层数组扩容原理
List、Set、Map的区别
Java集合框架源码剖析:LinkedHashSet 和 LinkedHashMap
浅谈java 集合框架
JDK 源码中 HashMap 的 hash 方法原理是什么?胖君的回答
编程语言中,取余和取模的区别到底是什么?
深入理解 hashcode 和 hash 算法