Java集合

序言

Java集合是我们使用最频繁的工具,也是面试的热点,但我们对它的理解仅限于使用上,而且大多数情况没有考虑过其使用规范。本系列文章将跟随源码的思路,分析实现的每个细节,以期在使用时避免各种不规范的坑。在这里,我们会惊艳于开发者优秀的设计,也会感激先辈们付出的艰辛努力,更重要的是知其所以然,少犯错误,写出优秀的代码。

基础知识概述

对数据的操作,大抵就是,以及在某些时候根据位置获取数据,有时可能还需要进行排序又可以理解为一致的操作,因为要修改一条数据需要先找到它,然后替换即可。接下来我们就从这三点简要分析下当前使用比较广泛的几种数据结构。

数组

数组在内存中占据一段连续的内存,所有的数据在内存中连续排列。它的大小是固定的,这一特性使得数组对于插入操作并不友好,我们分析ArrayList时就会看到这种操作的复杂。但数组对于位置的访问是极其友好的,它支持所谓RandomAccess特性,这使得基于位置的操作可以迅速完成,其时间复杂度为**O(1)。数组的数据顺序与插入顺序一致,所以查询操作需要遍历,其时间复杂度为O(n)**。

所以数组最大的优势在于基于位置的访问,在扩展性方面表现无力。

链表

不同于数组,链表是通过指针域来表示数据与数据之间的位置关系的,所以链表在头部或尾部插入数据的复杂度仅为**O(1)。链表不具备RandomAccess特性,所以无法提供基于位置的访问。其查询操作也必须从从到尾遍历,复杂度为O(n)**。

所以链表最大的优势在于插入,而查询的表现很一般。

那有没有一种结构能够结合数组和链表的优点,使得查询和插入都具有优秀的表现呢?答案是肯定的,这就是散列表。

散列表

散列表就是Hash Table,这种结构使用key-value形式存储数据,我们经常使用的HashMapHashTable就基于它。

数组和链表在查询时表现一般的原因在于它们并不记得数据的位置,所以只能用待查询的数据和存储的数据依次比对。散列表使用一种巧妙的方式来减少甚至避免这种依次比对,它的原理是通过一个函数把任何的key转为int,每次查找时只需要执行一次这个函数便可以迅速定位。这个过程是不是像查字典呢?

散列表并不像上述那般完美,因为并不会有一个函数,能够保证所有的key转换结果都不同,也就是会发生所谓的哈希碰撞,而且它必须依赖于其他的数据结构,这部分知识会在后续文章中详细介绍。

良好设计的散列表可以使等操作的时间复杂度均为**O(1)**。

二叉排序树

二叉排序树是解决查询问题的另一方案,如果数据在插入时是有序的,在查询时就可以使用二分法二分法的原理很简单,比如猜一个在0-100之间的数,第一次猜50就可以直接排除一半的数据,每次按照这个规则就可以很快的获取正确答案。二分法的时间复杂度为**O(lg n)**。

树的结构对二分法有天然的支持(但这不是树最重要的用途)。二叉排序树牺牲了一部分插入的时间,但提高了查询的速度,同时有序的数据也可以做些其他的操作。如果查询的操作重要性超过了插入,我们应该考虑这种结构。二叉排序树也存在一些不平衡导致效率下降的问题,所以有了AVL树红黑树,以及用于数据库索引的B树B+树等概念,关于二叉排序树的知识也会在后续文章中介绍。

分析过程

以上介绍的数据结构的知识是我们理解Java集合类的基础,掌握这些核心原理,我们分析集合类源码时才不会吃力,我们会先对这些数据结构进行简要介绍,其他和本系列文章无关的概念不会涉及,大家可以查阅相关专业书籍进行系统学习。

由于集合类的源码十分庞大,从接口抽象设计到具体实现涉及到数十个类,我们不可能每行代码都进行分析,一些在前面分析过的点在后续部分也会略过,但对于我们应该注意的点都会详细解读。有一些过于复杂的代码,还会用图示进行直观的演示,以帮助理解整个运行机制。

本文的源码全部基于JDK1.8,不同版本的实现代码可能稍有差别。

Java集合类分为两大部分:CollectionMapCollection又主要由ListQueueSet三大模块组成。本系列文章也会基于这样的结构进行,我们会先了解一些用到的数据结构,然后按照从接口抽象到具体实现的顺序来一步步揭开集合的神秘面纱。

由于Set的结构与Map完全一致,且Set的内部都是基于对应的Map实现的,所以只需要知道Set是什么即可,其具体实现如果感兴趣可以自行阅读源码。

Iterable

当我们想要遍历集合时,Java为我们提供了多种选择,通常有以下三种写法:

  • 写法1:for循环
1
2
3
for (int i = 0, len = strings.size(); i < len; i++) {
System.out.println(strings.get(i));
}
  • 写法2:foreach循环
1
2
3
for (String var : strings) {
System.out.println(var);
}
  • 写法3:Iterator
1
2
3
4
Iterator iterator = strings.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}

Iterable是迭代器的意思,作用是为集合类提供for-each循环的支持。由于使用for循环需要通过位置获取元素,而这种获取方式仅有数组支持,其他许多数据结构,比如链表,只能通过查询获取数据,这会大大的降低效率。Iterable就可以让不同的集合类自己提供遍历的最佳方式。

Iterable的文档声明仅有一句:

Implementing this interface allows an object to be the target of the “for-each loop” statement.

它的作用就是为Java对象提供foreach循环,其主要方法是返回一个Iterator对象:

1
Iterator<T> iterator();

也就是说,如果想让一个Java对象支持foreach,只要实现Iterable接口,然后就可以像集合那样,通过Iterator iterator = strings.iterator()方式,或者使用foreach,进行遍历了。

Iterator

Iterator是foreach遍历的主体,它的代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
// 判断一个对象集合是否还有下一个元素
boolean hasNext();

// 获取下一个元素
E next();

// 删除最后一个元素。默认是不支持的,因为在很多情况下其结果不可预测,比如数据集合在此时被修改
default void remove(){...}

// 主要将每个元素作为参数发给action来执行特定操作
default void forEachRemaining(Consumer<? super E> action){...}

Iterator还有一个子接口,是为需要双向遍历数据时准备的,在后续分析ArrayListLinkedList时都会看到它。它主要增加了以下几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 是否有前一个元素
boolean hasPrevious();

// 获取前一个元素
E previous();

// 获取下一个元素的位置
int nextIndex();

// 获取前一个元素的位置
int previousIndex();

// 添加一个元素
void add(E e);

// 替换当前元素值
void set(E e);

总结

在Java中有许多特性都是通过接口来实现的,foreach循环也是。foreach主要是解决for循环依赖下标的问题,为高效遍历更多的数据结构提供了支持。

Collection

Collection

CollectionListQueueSet的超集,它直接继承于Iterable,也就是所有的Collection集合类都支持for-each循环。除此之外,Collection也是面向接口编程的典范,通过它可以在多种实现类间转换,这也是面向对象编程的魅力之一。

方法定义

在阅读源码前,我们可以先自行想象一下,如果我们想封装下数组或链表以方便操作,我们需要封装哪些功能呢?比如:统计大小、插入或删除数据、清空、是否包含某条数据,等等。而Collection就是对这些常用操作进行提取,只是其很全面、很通用。下面我们看看它都提供了哪些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//返回集合的长度,如果长度大于Integer.MAX_VALUE,返回Integer.MAX_VALUE
int size();

//如果集合元素总数为0,返回true
boolean isEmpty();

//判断集合中是否包含指定的元素,其依据是equals()方法
boolean contains(Object o);

//返回一个包含集合中所有元素的数组
Object[] toArray();

//与上个类似,只是增加了类型的转换
<T> T[] toArray(T[] a);

//向集合中加入一个元素,如果成功加入则返回true,如果加入失败,或者因集合本身已经包含同个元素而不再加入时,返回false
boolean add(E e);

//从集合中删除指定元素的单个实例
boolean remove(Object o);

//如果集合包含指定集合中的所有元素,返回true
boolean containsAll(Collection<?> c);

//把指定集合中的所有元素添加到集合中,但在此期间,如果指定的集合发生了改变,可能出现意想不到的事情
boolean addAll(Collection<? extends E> c);

//从集合中删除所有包含在指定集合中的元素
boolean removeAll(Collection<?> c);

//仅保留集合中包含在指定集合中的元素
boolean retainAll(Collection<?> c);

//清空集合
void clear();

//将此方法抽象,是保证所有子类都覆写此方法,以保证equals的正确行为
boolean equals(Object o);

//同上
int hashCode();

//这个方法在JDK1.8中提供了默认的实现,会使用Iterator的形式删除符合条件的元素
default boolean removeIf(Predicate<? super E> filter){
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}

超实现类:AbstractCollection

Collection中定义的许多方法,根据现有的定义以及继承的Iterable,都可以在抽象类中实现,这样可以减少实现类需要实现的方法,这个抽象类就是AbstractCollection

首先我们关注下其文档,里面有两句说明可能会影响我们的继承:

To implement an unmodifiable collection, the programmer needs only to extend this class and provide implementations for the iterator and size methods. (The iterator returned by the iterator method must implement hasNext and next.)

To implement a modifiable collection, the programmer must additionally override this class’s add method (which otherwise throws an UnsupportedOperationException), and the iterator returned by the iterator method must additionally implement its remove method.

大体意思是说,如果要实现一个不可修改的集合,只需要重写iteratorsize接口就可以,并且返回的Iterator需要实现hasNextnext。而要实现一个可以修改的集合,还必须重写add方法(默认会抛出异常),返回的Iterator还需要实现remove方法。

方法定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//这个毫无疑问,是可以直接获取的
public boolean isEmpty() {
return size() == 0;
}

//这个方法因为Iterator的存在,可以进行一致性封装,这里需要注意的是对象的比较是通过equals方法,因为调用到了it.next()与it.hasNext(),这也是为什么文档注释会写实现集合类需要重写Iterator的这两个方法。
public boolean contains(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return true;
} else {
while (it.hasNext())
if (o.equals(it.next()))
return true;
}
return false;
}

//和contains类似,也是通过Iterator实现的,但其会调用it.remove()方法,这也是为什么文档注释会写实现可以修改的集合类时需要重写Iterator的remove方法。
public boolean remove(Object o) {
//...省略,这里调用了it.remove()方法
}

类似的方法还有containsAll(Collection<?> c)addAll(Collection<? extends E> c)removeAll(Collection<?> c)retainAll(Collection<?> c)clear()等,都需要利用到Iterator的特性,这里就不再一一赘述了。

另外还有一个toArray()的方法实现略微不同,可以看看其具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//这个实现相对复杂一些,可以看到扩容最主要的手段是Arrays.copyOf()方法,
//也就是需要将原数组通过复制到新的数组中来实现的。
//注意这里返回的顺序和Iterator顺序一致
//在这里实现是为了方便不同具体实现类互相转换,我们在后续会多次见到此方法
public Object[] toArray() {
//先根据当前集合大小声明一个数组
Object[] r = new Object[size()];
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
//集合元素没那么多,说明不需要那么大的数组
if (! it.hasNext())
return Arrays.copyOf(r, i); //仅返回赋完值的部分
r[i] = it.next();
}
//元素比从size()中获取的更多,就需要进一步调整数组大小
return it.hasNext() ? finishToArray(r, it) : r;
}

private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
//记录当前大小
int i = r.length;
while (it.hasNext()) {
int cap = r.length;
//r的长度不够,继续分配
if (i == cap) {
//扩充方式为cap+cap/2+1,也就是1.5倍扩容
int newCap = cap + (cap >> 1) + 1;
// 超过了最大容量,MAX_ARRAY_SIZE=Integer.MAX_VALUE-8
if (newCap - MAX_ARRAY_SIZE > 0)
//重新设置cap的值
newCap = hugeCapacity(cap + 1);

//对r进行扩容
r = Arrays.copyOf(r, newCap);
}
//赋值,进入下一轮循环
r[i++] = (T)it.next();
}
// 由于之前扩容是1.5倍进行的,最后再将其设置到和r实际需要的相同
return (i == r.length) ? r : Arrays.copyOf(r, i);
}

private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // 超过了最大正整数,也就是负数
throw new OutOfMemoryError
("Required array size too large");
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

//和toArray()方法类似,就不再赘述,具体可以查看源码
public <T> T[] toArray(T[] a) {
//...
}

除了以上这些方法,AbstractCollection还实现了toString方法,其是通过StringBuilder拼接了每个元素的toString完成的,也并不复杂。这里可以看下其源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";

StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}

超接口List

ListCollection三大直接子接口之一,其中的数据可以通过位置检索,用户可以在指定位置插入数据。List的数据可以为空,可以重复。以下是其文档注释,只看前两段:

An ordered collection (also known as a *sequence*). The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the list.

Unlike sets, lists typically allow duplicate elements. More formally, lists typically allow pairs of elements e1 and e2 such that e1.equals(e2), and they typically allow multiple null elements if they allow null elements at all. It is not inconceivable that someone might wish to implement a list that prohibits duplicates, by throwing runtime exceptions when the user attempts to insert them, but we expect this usage to be rare.

List特有的方法

其不同于Collection的方法,主要有以下这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//在指定位置,将指定的集合插入到当前的集合中
boolean addAll(int index, Collection<? extends E> c);

//这是一个默认实现的方法,会通过Iterator的方式对每个元素进行指定的操作
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}

//排序,依据指定的规则对当前集合进行排序,可以看到,排序是通过Arrays这个工具类完成的。
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}

//获取指定位置的元素
E get(int index);

//修改指定位置元素的值
E set(int index, E element);

//将指定元素添加到指定的位置
void add(int index, E element);

//将指定位置的元素移除
E remove(int index);

//返回一个元素在集合中首次出现的位置
int indexOf(Object o);

//返回一个元素在集合中最后一次出现的位置
int lastIndexOf(Object o);

//ListIterator继承于Iterator,主要增加了向前遍历的功能
ListIterator<E> listIterator();

//从指定位置开始,返回一个ListIterator
ListIterator<E> listIterator(int index);

//返回一个子集合[fromIndex, toIndex),非结构性的修改返回值会反映到原表,反之亦然。
//如果原表进行了结构修改,则返回的子列表可能发生不可预料的事情
List<E> subList(int fromIndex, int toIndex);

通过以上对接口的分析可以发现,Collection主要提供一些通用的方法,而List则针对线性表的结构,提供了对位置以及子表的操作。

超实现类:AbstractList

有了分析AbstractCollection的经验,我们分析AbstractList就更容易了。首先也看下其文档中强调的部分:

To implement an unmodifiable list, the programmer needs only to extend this class and provide implementations for the *get(int)* and *size()* methods.

To implement a modifiable list, the programmer must additionally override the *set(int, E)* method (which otherwise throws an UnsupportedOperationException). If the list is variable-size the programmer must additionally override the *add(int, E)* and *remove(int)* methods.

大致意思是说,要实现一个不可修改的集合,只需要复写getsize就可以了。要实现一个可以修改的集合,还需要复写set方法,如果要动态调整大小,就必须再实现addremove方法。

然后看下其源码实现了哪些功能吧:

1
2
3
4
5
6
7
8
9
10
11
12
//在AbstractCollection中,add方法默认会抛出异常,
//而在这里是调用了add(int index, E e)方法,但这个方法也是没有实现的。
//这里默认会把元素添加到末尾。
public boolean add(E e) {
add(size(), e);
return true;
}

//同上,这个只需要进行一次遍历即可
public boolean addAll(int index, Collection<? extends E> c) {
//...
}

接下来,还有几个方法和IteratorListIterator息息相关,在AbstractList中有具体的实现,我们先看看它是如何把集合转变成Iterator对象并支持foreach循环的吧。

我们追踪源码发现,在iterator()方法中直接返回了一个Itr对象

1
2
3
public Iterator<E> iterator() {
return new Itr();
}

这样我们就明白了,它是实现了一个内部类,这个内部类实现了Iterator接口,合理的处理hasNextnextremove方法。这个源码就不粘贴啦,其中仅仅在remove时考虑了一下多线程问题,有兴趣的可以自己去看看。

另外一个就是ListIterator

1
2
3
public ListIterator<E> listIterator() {
return listIterator(0);
}

可以看到,listIterator方法依赖于listIterator(int index)方法。有了上边的经验,我们可以推测,它也是通过一个内部类完成的。

1
2
3
4
5
public ListIterator<E> listIterator(final int index) {
rangeCheckForAdd(index);

return new ListItr(index);
}

事实证明,和我们想的一样,AbstractList内部还定义了一个ListItr,实现了ListIterator接口,其实现也很简单,就不粘贴源码啦。

接下来我们看看,利用这两个实现类,AbstractList都做了哪些事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//寻找一个元素首次出现的位置,只需要从前往后遍历,找到那个元素并返回其位置即可。
public int indexOf(Object o) {
ListIterator<E> it = listIterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return it.previousIndex();
} else {
while (it.hasNext())
if (o.equals(it.next()))
return it.previousIndex();
}
return -1;
}

//同理,寻找一个元素最后一次出现的位置,只需要从列表最后一位向前遍历即可。
//看到listIterator(int index)方法是可以传递参数的,这个我想我们都可以照着写出来了。
public int lastIndexOf(Object o) {
//...
}

//这个方法是把从fromIndex到toIndex之间的元素从集合中删除。
//clear()方法也是调用这个实现的(我认为clear实现意义并不大,因为在其上级AbstractCollection中已经有了具体实现)。
protected void removeRange(int fromIndex, int toIndex) {
ListIterator<E> it = listIterator(fromIndex);
for (int i=0, n=toIndex-fromIndex; i<n; i++) {
it.next();
it.remove();
}
}

接下来还有两块内容比较重要,一个是关于SubList的,一个是关于equalshashcode的。

我们先看看SubList相关的内容。SubList并不是新建了一个集合,只是持有了当前集合的引用,然后控制一下用户可以操作的范围,所以在接口定义时就说明了其更改会直接反应到原集合中。SubList定义在AbstractList内部,并且是AbstractList的子类。在AbstractList的基础上增加了对可选范围的控制。

equalshashcode的实现,也关乎我们的使用。在AbstractList中,这两个方法不仅与其实例有关,也和其内部包含的元素有关,所以在定义数据元素时,也应该复写这两个方法,以保证程序的正确运行。这里看下其源码加深一下印象吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof List))
return false;

ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
//这里用到了数据元素的equals方法
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
return !(e1.hasNext() || e2.hasNext());
}
1
2
3
4
5
6
7
public int hashCode() {
int hashCode = 1;
for (E e : this)
//这里用到了数据元素的hashCode方法
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}

ArrayList

ArrayListVector的翻版,只是去除了线程安全。Vector因为种种原因不推荐使用了,这里我们就不对其进行分析了。ArrayList是一个可以动态调整大小的List实现,其数据的顺序与插入顺序始终一致,其余特性与List中定义的一致。

ArrayList继承结构

image-20200728152447714

​ ArrayList结构图

可以看到,ArrayListAbstractList的子类,同时实现了List接口。除此之外,它还实现了三个标识型接口,这几个接口都没有任何方法,仅作为标识表示实现类具备某项功能。RandomAccess表示实现类支持快速随机访问,Cloneable表示实现类支持克隆,具体表现为重写了clone方法,java.io.Serializable则表示支持序列化,如果需要对此过程自定义,可以重写writeObjectreadObject方法。

一般面试问到与ArrayList相关的问题时,可能会问ArrayList的初始大小是多少?很多人在初始化ArrayList时,可能都是直接调用无参构造函数,从未关注过此问题。例如,这样获取一个对象:

1
ArrayList<String> strings = new ArrayList<>();

我们都知道,ArrayList是基于数组的,而数组是定长的。那ArrayList为何不需要指定长度,就能使我们既可以插入一条数据,也可以插入一万条数据?回想刚刚文档的第一句话:

Resizable-array implementation of the List interface.

ArrayList可以动态调整大小,所以我们才可以无感知的插入多条数据,这也说明其必然有一个默认的大小。而要想扩充数组的大小,只能通过复制。这样一来,默认大小以及如何动态调整大小会对使用性能产生非常大的影响。我们举个例子来说明此情形:

比如默认大小为5,我们向ArrayList中插入5条数据,并不会涉及到扩容。如果想插入100条数据,就需要将ArrayList大小调整到100再进行插入,这就涉及一次数组的复制。如果此时,还想再插入50条数据呢?那就得把大小再调整到150,把原有的100条数据复制过来,再插入新的50条数据。自此之后,我们每向其中插入一条数据,都要涉及一次数据拷贝,且数据量越大,需要拷贝的数据越多,性能也会迅速下降。

其实,ArrayList仅仅是对数组操作的封装,里面采取了一定的措施来避免以上的问题,如果我们不利用这些措施,就和直接使用数组没有太大的区别。那我们就看看ArrayList用了哪些措施,并且如何使用它们吧。我们先从初始化说起。

构造方法与初始化

ArrayList一共有三个构造方法,用到了两个成员变量。

1
2
3
4
5
6
7
8
//这是一个用来标记存储容量的数组,也是存放实际数据的数组。
//当ArrayList扩容时,其capacity就是这个数组应有的长度。
//默认时为空,添加进第一个元素后,就会直接扩展到DEFAULT_CAPACITY,也就是10
//这里和size区别在于,ArrayList扩容并不是需要多少就扩展多少的
transient Object[] elementData;

//这里就是实际存储的数据个数了
private int size;

除了以上两个成员变量,我们还需要掌握一个变量,它是

1
protected transient int modCount = 0;

这个变量主要作用是防止在进行一些操作时,改变了ArrayList的大小,那将使得结果不可预测。

下面我们看看构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//默认构造方法。文档说明其默认大小为10,但正如elementData定义所言,
//只有插入一条数据后才会扩展为10,而实际上默认是空的
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

//带初始大小的构造方法,一旦指定了大小,elementData就不再是原来的机制了。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

//从一个其他的Collection中构造一个具有初始化数据的ArrayList。
//这里可以看到size是表示存储数据的数量
//这也展示了Collection这种抽象的魅力,可以在不同的结构间转换
public ArrayList(Collection<? extends E> c) {
//转换最主要的是toArray(),这在Collection中就定义了
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

重要方法

ArrayList已经是一个具体的实现类了,所以在List接口中定义的所有方法在此都做了实现。其中有些在AbstractList中实现过的方法,在这里再次被重写,我们稍后就可以看到它们的区别。

先看一些简单的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//还记得在AbstractList中的实现吗?那是基于Iterator完成的。
//在这里完全没必要先转成Iterator再进行操作
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

//和indexOf是相同的道理
public int lastIndexOf(Object o) {
//...
}

//一样的道理,已经有了所有元素,不需要再利用Iterator来获取元素了
//注意这里返回时把elementData截断为size大小
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}

//带类型的转换,看到这里a[size] = null;这个用处真不大,除非你确定所有元素都不为空,
//才可以通过null来判断获取了多少有用数据。
public <T> T[] toArray(T[] a) {
if (a.length < size)
// 给定的数据长度不够,复制出一个新的并返回
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}

数据操作最重要的就是增删改查,改查都不涉及长度的变化,而增删就涉及到动态调整大小的问题,我们先看看改和查是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

//只要获取的数据位置在0-size之间即可
public E get(int index) {
rangeCheck(index);

return elementData(index);
}

//改变下对应位置的值
public E set(int index, E element) {
rangeCheck(index);

E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}

增和删是ArrayList最重要的部分,这部分代码需要我们细细研究,我们看看它是如何处理我们例子中的问题的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//在最后添加一个元素
public boolean add(E e) {
//先确保elementData数组的长度足够
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

public void add(int index, E element) {
rangeCheckForAdd(index);

//先确保elementData数组的长度足够
ensureCapacityInternal(size + 1); // Increments modCount!!
//将数据向后移动一位,空出位置之后再插入
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

以上两种添加数据的方式都调用到了ensureCapacityInternal这个方法,我们看看它是如何完成工作的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//在定义elementData时就提过,插入第一个数据就直接将其扩充至10
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

//这里把工作又交了出去
ensureExplicitCapacity(minCapacity);
}

//如果elementData的长度不能满足需求,就需要扩充了
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

//扩充
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//可以看到这里是1.5倍扩充的
int newCapacity = oldCapacity + (oldCapacity >> 1);

//扩充完之后,还是没满足,这时候就直接扩充到minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//防止溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

至此,我们彻底明白了ArrayList的扩容机制了。首先创建一个空数组*elementData*,第一次插入数据时直接扩充至10,然后如果*elementData**的长度不足,就扩充1.5倍,如果扩充完还不够,就使用需要的长度作为elementData***的长度。

这样的方式显然比我们例子中好一些,但是在遇到大量数据时还是会频繁的拷贝数据。那么如何缓解这种问题呢,ArrayList为我们提供了两种可行的方案:

  • 使用ArrayList(int initialCapacity)这个有参构造,在创建时就声明一个较大的大小,这样解决了频繁拷贝问题,但是需要我们提前预知数据的数量级,也会一直占有较大的内存。
  • 除了添加数据时可以自动扩容外,我们还可以在插入前先进行一次扩容。只要提前预知数据的数量级,就可以在需要时直接一次扩充到位,与ArrayList(int initialCapacity)相比的好处在于不必一直占有较大内存,同时数据拷贝的次数也大大减少了。这个方法就是**ensureCapacity(int minCapacity)**,其内部就是调用了ensureCapacityInternal(int minCapacity)

其他还有一些比较重要的函数,其实现的原理也大同小异,这里我们不一一分析了,但还是把它们列举出来,以便使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//将elementData的大小设置为和size一样大,释放所有无用内存
public void trimToSize() {
//...
}

//删除指定位置的元素
public E remove(int index) {
//...
}

//根据元素本身删除
public boolean remove(Object o) {
//...
}

//在末尾添加一些元素
public boolean addAll(Collection<? extends E> c) {
//...
}

//从指定位置起,添加一些元素
public boolean addAll(int index, Collection<? extends E> c){
//...
}

//删除指定范围内的元素
protected void removeRange(int fromIndex, int toIndex){
//...
}

//删除所有包含在c中的元素
public boolean removeAll(Collection<?> c) {
//...
}

//仅保留所有包含在c中的元素
public boolean retainAll(Collection<?> c) {
//...
}

ArrayList还对父级实现的ListIterator以及SubList进行了优化,主要是使用位置访问元素,我们就不再研究了。

其他实现方法

ArrayList不仅实现了List中定义的所有功能,还实现了equalshashCodeclonewriteObjectreadObject等方法。这些方法都需要与存储的数据配合,否则结果将是错误的或者克隆得到的数据只是浅拷贝,或者数据本身不支持序列化等,这些我们定义数据时注意到即可。我们主要看下其在序列化时自定义了哪些东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//这里就能解开我们的迷惑了,elementData被transient修饰,也就是不会参与序列化
//这里我们看到数据是一个个写入的,并且将size也写入了进去
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();

// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);

// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}

//modCount的作用在此体现,如果序列化时进行了修改操作,就会抛出异常
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

readObject是一个相反的过程,就是把数据正确的恢复回来,并将elementData设置好即可,感兴趣可以自行阅读源码。

总结

总体而言,ArrayList还是和数组一样,更适合于数据随机访问,而不太适合于大量的插入与删除,如果一定要进行插入操作,要使用以下三种方式:

  • 使用ArrayList(int initialCapacity)这个有参构造,在创建时就声明一个较大的大小。
  • 使用**ensureCapacity(int minCapacity)**,在插入前先扩容。
  • 使用LinkedList,这个无可厚非哈,我们很快就会介绍这个适合于增删的集合类。

超接口Queue

队列在软件开发中担任着重要的职责,java函数的调用用到了栈的技术,在处理并发问题时,BlockingQueue很好的解决了数据传输的问题。接下来我们看看Java是如何定义队列的吧。

首先,Queue也继承自Collection,说明它是集合家族的一员。Queue接口主要提供了以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//将元素插入队列
boolean add(E e);

//将元素插入队列,与add相比,在容量受限时应该使用这个
boolean offer(E e);

//将队首的元素删除,队列为空则抛出异常
E remove();

//将队首的元素删除,队列为空则返回null
E poll();

//获取队首元素,但不移除,队列为空则抛出异常
E element();

//获取队首元素,但不移除,队列为空则返回null
E peek();

超实现类AbstractQueue

Queue的定义很简单,所以其实现类也很简单,用简单的代码做复杂的事情,值得我们学习。

AbstractQueue仅实现了addremoveelement三个方法,并且分别调用了另外一个仅细微区别的方法,我们这里只看其一

1
2
3
4
5
6
7
//这里我们就明白,对于有容量限制的,直接调用offer肯定会更快
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}

此外,它还实现了clearaddAll方法,重写这些方法可以使其更符合当前场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void clear() {
while (poll() != null)
;
}

public boolean addAll(Collection<? extends E> c) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}

接口Deque

Deque全称为double ended queue,即双向队列,它允许在两侧插入或删除元素,同时也建议我们不要向其中插入null值。除此之外,其余特性则和父级Queue类似。Deque大多数情况下不会限制元素的数量,但这不是必须的。

Deque中定义的方法主要分为四部分,第一部分就如Deque定义所言,提供两侧插入或删除的方法。第二部分是继承自Queue的实现。第三部分表示如果要基于此实现一个Stack,需要实现的方法。最后一部分是继承自Collection的方法。

两侧插入、删除

这里方法和Queue定义方式一致,但却是针对两侧插入删除的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//在队首添加元素
void addFirst(E e);
//在队首添加元素
boolean offerFirst(E e);

//在队尾添加元素
void addLast(E e);
boolean offerLast(E e);

//删除队首元素
E removeFirst();
E pollFirst();

//删除队尾元素
E removeLast();
E pollLast();

//获取队首元素
E getFirst();
E peekFirst();

//获取队尾元素
E getLast();
E peekLast();

//删除第一个事件,大多数指的是删除第一个和 o equals的元素
boolean removeFirstOccurrence(Object o);
//删除最后一个事件,大多数指的是删除最后一个和 o equals的元素
boolean removeLastOccurrence(Object o);

与Queue对应的方法

因为Queue遵循FIFO,所以其方法在Deque中对应关系有所改变,结合Deque的定义,我们很容易就想到它们的对应关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//与addLast(E e)等价
boolean add(E e);

//与offerLast(E e)等价
boolean offer(E e);

//与removeFirst()等价
E remove();

//与pollFirst()等价
E poll();

//与getFirst()等价
E element();

//与peekFirst()等价
E peek();

实现Stack

Stack仅在一侧支持插入删除操作等操作,遵循LIFO原则。

1
2
3
4
5
//与addFirst()等价
void push(E e);

//与removeFirst()等价
E pop();

继承于Collection的方法

这里主要关注两个方法。

1
2
3
4
5
//顺序是从队首到队尾
Iterator<E> iterator();

//顺序是从队尾到队首
Iterator<E> descendingIterator();

ArrayDeque

先看下其文档:

Resizable-array implementation of the Deque interface. Array deques have no capacity restrictions; they grow as necessary to support usage. They are not thread-safe; in the absence of external synchronization, they do not support concurrent access by multiple threads. Null elements are prohibited. This class is likely to be faster than *Stack* when used as a stack, and faster than *LinkedList* when used as a queue.

文档中并没有过多的介绍实现细节,但说它是Resizable-array implementation of the Deque interface,也就是用可动态调整大小的数组来实现了Deque,听起来是不是像ArrayList?但ArrayDeque对数组的操作方式和ArrayList有较大的差别。下面我们就深入其源码看看它是如何巧妙的使用数组的,以及为何说

faster than *Stack* when used as a stack, and faster than *LinkedList* when used as a queue.

构造函数与重要成员变量

ArrayDeque共有四个成员变量,其中两个我们在分析ArrayList时已经见过了,还有两个我们需要认真研究一下:

1
2
3
4
5
6
7
8
9
10
11
12
//存放元素,长度和capacity一致,并且总是2的次幂
//这一点,我们放在后面解释
transient Object[] elements;

//capacity最小值,也是2的次幂
private static final int MIN_INITIAL_CAPACITY = 8;

//标记队首元素所在的位置
transient int head;

//标记队尾元素所在的位置
transient int tail;

其构造函数共有三个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//默认构造函数,将elements长度设为16,相当于最小capacity的两倍
public ArrayDeque() {
elements = new Object[16];
}

//带初始大小的构造
public ArrayDeque(int numElements) {
allocateElements(numElements);
}

//从其他集合类导入初始数据
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}

这里看到有两个构造函数都用到了allocateElements方法,这是一个非常经典的方法,我们接下来就先重点研究它。

寻找最近的2次幂

在定义elements变量时说,其长度总是2的次幂,但用户传入的参数并不一定符合规则,所以就需要根据用户的输入,找到比它大的最近的2次幂。比如用户输入13,就把它调整为16,输入31,就调整为32,等等。考虑下,我们有什么方法可以实现呢?

来看下ArrayDeque是怎么做的吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void allocateElements(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
if (numElements >= initialCapacity) {
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;

if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
elements = new Object[initialCapacity];
}

看到这段迷之代码了吗?在HashMap中也有一段类似的实现。但要读懂它,我们需要先掌握以下几个概念:

  • 在java中,int的长度是32位,有符号int可以表示的值范围是 -$2^31$ 到 $2^31$-1,其中最高位是符号位,0表示正数,1表示负数。
  • >>>:无符号右移,忽略符号位,空位都以0补齐。
  • |:位或运算,按位进行或操作,逢1为1。

我们知道,计算机存储任何数据都是采用二进制形式,所以一个int值为80的数在内存中可能是这样的:

0000 0000 0000 0000 0000 0000 0101 0000

比80大的最近的2次幂是128,其值是这样的:

0000 0000 0000 0000 0000 0000 1000 0000

我们多找几组数据就可以发现规律:

  • 每个2的次幂用二进制表示时,只有一位为 1,其余位均为 0(不包含符合位)
  • 要找到比一个数大的2的次幂(在正数范围内),只需要将其最高位左移一位(从左往右第一个 1 出现的位置),其余位置 0 即可。

但从实践上讲,没有可行的方法能够进行以上操作,即使通过&操作符可以将某一位置 0 或置 1,也无法确认最高位出现的位置,也就是基于最高位进行操作不可行。

但还有一个很整齐的数字可以被我们利用,那就是 2n-1,我们看下128-1=127的表示形式:

0000 0000 0000 0000 0000 0000 0111 1111

把它和80对比一下:

0000 0000 0000 0000 0000 0000 0101 0000 //80
0000 0000 0000 0000 0000 0000 0111 1111 //127

可以发现,我们只要把80从最高位起每一位全置为1,就可以得到离它最近且比它大的 2n-1,最后再执行一次+1操作即可。具体操作步骤为(为了演示,这里使用了很大的数字):
原值:

0011 0000 0000 0000 0000 0000 0000 0010

  1. 无符号右移1位

0001 1000 0000 0000 0000 0000 0000 0001

  1. 与原值|操作:

0011 1000 0000 0000 0000 0000 0000 0011

可以看到最高2位都是1了,也仅能保证前两位为1,这时就可以直接移动两位

  1. 无符号右移2位

0000 1110 0000 0000 0000 0000 0000 0000

  1. 与原值|操作:

0011 1110 0000 0000 0000 0000 0000 0011

此时就可以保证前4位为1了,下一步移动4位

  1. 无符号右移4位

0000 0011 1110 0000 0000 0000 0000 0000

  1. 与原值|操作:

0011 1111 1110 0000 0000 0000 0000 0011

此时就可以保证前8位为1了,下一步移动8位

  1. 无符号右移8位

0000 0000 0011 1111 1110 0000 0000 0000

  1. 与原值|操作:

0011 1111 1111 1111 1110 0000 0000 0011

此时前16位都是1,只需要再移位操作一次,即可把32位都置为1了。

  1. 无符号右移16位

0000 0000 0000 0000 0011 1111 1111 1111

  1. 与原值|操作:

0011 1111 1111 1111 1111 1111 1111 1111

  1. 进行+1操作:

0100 0000 0000 0000 0000 0000 0000 0000

如此经过11步操作后,我们终于找到了合适的2次幂。写成代码就是:

1
2
3
4
5
6
initialCapacity |= (initialCapacity >>>  1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;

不过为了防止溢出,导致出现负值(如果把符号位置为1,就为负值了)还需要一次校验:

1
2
if (initialCapacity < 0)   // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements

至此,初始化的过程就完毕了。

重要操作方法

add分析

Deque主要定义了一些关于First和Last的操作,如add、remove、get等。我们看看它是如何实现的吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//在队首添加一个元素,非空
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}

//在队尾添加一个元素,非空
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}

这里,又有一段迷之代码需要我们认真研究了,这也是ArrayDeque值得我们研究的地方之一,通过位运算提升效率。

1
elements[head = (head - 1) & (elements.length - 1)] = e;

很明显这是一个赋值操作,而且应该是给head之前的位置赋值,所以head = (head - 1)是合理的操作,那这个& (elements.length - 1)又表示什么呢?

在之前的定义与初始化中,elements.length要求为2的次幂,也就是 2n 形式,那这个& (elements.length - 1)也就是 2n-1 了,在内存中用二进制表示就是从最高位起每一位都是1。我们还以之前的127为例:

0000 0000 0000 0000 0000 0000 0111 1111

&就是按位与,全1才为1。那么任意一个正数和127进行按位与操作后,都只有最右侧7位被保留了下来,其他位全部置0(除符号位),而对一个负数而言,则会把它的符号位置为0,&操作后会变成正数。比如-1的值是1111 … 1111(32个1),和127按位操作后结果就变成了127 。所以,对于正数它就是取模,对于负数,它就是把元素插入了数组的结尾。所以,这个数组并不是向前添加元素就向前扩展,向后添加就向后扩展,它是循环的,类似这样:

image-20200728172012816

​ 循环队列示意图

初始时,head与tail都指向a[0],这时候数组是空的。当执行addFirst()方法时,head指针移动一位,指向a[elements.length-1],并赋值,也就是给a[elements.length-1]赋值。当执行addLast()操作时,先给a[0]赋值,再将tail指针移动一位,指向a[1]。所以执行完之后head指针位置是有值的,而tail位置是没有值的。

随着添加操作执行,数组总会占满,那么怎么判断它满了然后扩容呢?首先,如果head==tail,则说明数组是空的,所以在添加元素时必须保证head与tail不相等。假如现在只有一个位置可以添加元素了,类似下图:

image-20200728172054827

​ 循环队列即将充满示意图

此时,tail指向了a[8],head已经填充到a[9]了,只有a[8]是空闲的。很显然,不管是addFirst还是addLast,再添加一个元素后都会导致 head== tail。这时候就不得不扩容了,因为head == tail 是判断是否为空的条件。扩容就比较简单了,直接翻倍,我们看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void doubleCapacity() {
//只有head==tail时才可以扩容
assert head == tail;
int p = head;
int n = elements.length;
//在head之后,还有多少元素
int r = n - p; // number of elements to the right of p
//直接翻倍,因为capacity初始化时就已经是2的倍数了,这里无需再考虑
int newCapacity = n << 1;
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
//左侧数据拷贝
System.arraycopy(elements, p, a, 0, r);
//右侧数据拷贝
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0;
tail = n;
}

分析完add,那么get以及remove等都大同小异,感兴趣可以查看源码。我们还要看看在Deque中定义的removeFirstOccurrenceremoveLastOccurrence方法的具体实现。

Occurrence相关

removeFirstOccurrenceremoveLastOccurrence分别用于找到元素在队首或队尾第一次出现的位置并删除。其实现原理是一致的,我们分析一个即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean removeFirstOccurrence(Object o) {
if (o == null)
return false;
int mask = elements.length - 1;
int i = head;
Object x;
while ( (x = elements[i]) != null) {
if (o.equals(x)) {
delete(i);
return true;
}
i = (i + 1) & mask;
}
return false;
}

这里就是遍历所有元素,然后通过delete方法删除,我们看看delete实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private boolean delete(int i) {
//检查
checkInvariants();
final Object[] elements = this.elements;
final int mask = elements.length - 1;
final int h = head;
final int t = tail;
//待删除元素前面的元素个数
final int front = (i - h) & mask;
//待删除元素后面的元素个数
final int back = (t - i) & mask;

// Invariant: head <= i < tail mod circularity
//确认 i 在head和tail之间
if (front >= ((t - h) & mask))
throw new ConcurrentModificationException();

// Optimize for least element motion
//尽量最少操作数据
//前面数据比较少
if (front < back) {
if (h <= i) {
//这时 h 和 i 之间最近距离没有跨过位置0
System.arraycopy(elements, h, elements, h + 1, front);
} else { // Wrap around
System.arraycopy(elements, 0, elements, 1, i);
elements[0] = elements[mask];
System.arraycopy(elements, h, elements, h + 1, mask - h);
}
elements[h] = null;
head = (h + 1) & mask;
return false;
} else {
if (i < t) { // Copy the null tail as well
//这时 t 和 i 之间最近距离没有跨过位置0
System.arraycopy(elements, i + 1, elements, i, back);
tail = t - 1;
} else { // Wrap around
System.arraycopy(elements, i + 1, elements, i, mask - i);
elements[mask] = elements[0];
System.arraycopy(elements, 1, elements, 0, t);
tail = (t - 1) & mask;
}
return true;
}
}

总结

ArrayDeque通过循环数组的方式实现的循环队列,并通过位运算来提高效率,容量大小始终是2的次幂。当数据充满数组时,它的容量将翻倍。作为Stack,因为其非线程安全所以效率高于java.util.Stack,而作为队列,因为其不需要结点支持所以更快(LinkedList使用Node存储数据,这个对象频繁的new与clean,使得其效率略低于ArrayDeque)。但队列更多的用来处理多线程问题,所以我们更多的使用BlockingQueue,关于多线程的问题,以后再认真研究。

LinkedList

LinkedList弥补了ArrayList增删较慢的问题,但在查找方面又逊色于ArrayList,所以在使用时需要根据场景灵活选择。对于这两个频繁使用的集合类,掌握它们的源码并正确使用,可以让我们的代码更高效。

LinkedList既实现了List,又实现了Deque,前者使它能够像使用ArrayList一样使用,后者又使它能够承担队列的职责。LinkedList内部结构是一个双向链表,我们在分析ArrayDeque时提到过这个概念,就是扩充单链表的指针域,增加一个指向前一个元素的指针previous

AbstractSequentialList

AbstractSequentialListLinkedList的父级,它继承自AbstractList,并且是一个抽象类,它主要为顺序表的链式实现提供一个骨架:

This class provides a skeletal implementation of the List interface to minimize the effort required to implement this interface backed by a “sequential access” data store (such as a linked list). For random access data (such as an array), AbstractList should be used in preference to this class.

意思是它的主要作用是提供一个实现List接口的骨架,来减少我们实现基于链式存储的实现类时所需的工作量。AbstractSequentialList并没有做很多特殊的事情,其中最主要的是提供一个方法的默认实现,并将以下方法抽象,以期有更符合场景的实现:

1
public abstract ListIterator<E> listIterator(int index);

其他一些方法的实现都利用了这个listIterator方法,我们不再一一查看了。下面我们分析LinkedList的实现

LinkedList的结构

LinkedList的继承结构如下所示:

image-20200728174255467

​ LinkedList结构图

可以看到,LinkedList也实现了Cloneablejava.io.Serializable等方法,借鉴于ArrayList的经验,我们可以想到它的Clone也是浅克隆,在序列化方法也采用了同样的方式,我们就不再赘述了。

构造方法与成员变量

数据单元Node

在介绍链表结构时提到过,其数据单元分为数据域和指针域,分别存储数据和指向下一个元素的位置,在java中只要定义一个实体类就可以解决了。

1
2
3
4
5
6
7
8
9
10
11
private static class Node<E> {
E item; //数据
Node<E> next; //下一个元素
Node<E> prev; //上一个元素

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

成员变量

LinkedList成员变量主要有三个,而且其意义清晰可见。

1
2
3
4
5
6
7
8
// 记录当前链表的长度
transient int size = 0;

// 第一个节点
transient Node<E> first;

// 最后一个节点
transient Node<E> last;

构造函数

因为链表没有长度方面的问题,所以也不会涉及到扩容等问题,其构造函数也十分简洁了。

1
2
3
4
5
6
7
public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

一个默认的构造函数,什么都没有做,一个是用其他集合初始化,调用了一下addAll方法。addAll方法我们就不再分析了,它应该是和添加一个元素的方法是一致的。

重要方法

LinkedList既继承了List,又继承了Deque,那它必然有一堆addremoveaddFirstaddLast等方法。这些方法的含义也相差不大,实现也是类似的,因此LinkedList又提取了新的方法,来简化这些问题。我们看看这些私有方法,以及它们是如何与上述函数对应的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//将一个元素链接到首位
private void linkFirst(E e) {
//先将原链表存起来
final Node<E> f = first;
//定义一个新节点,其next指向原来的first
final Node<E> newNode = new Node<>(null, e, f);
//将first指向新建的节点
first = newNode;
//原链表为空表
if (f == null)
//把last也指向新建的节点,现在first与last都指向了它
last = newNode;
else
//把原链表挂载在新建节点,也就是现在的first之后
f.prev = newNode;
size++;
modCount++;
}

//与linkFirst类似
void linkLast(E e) {
//...
}

//在某个非空节点之前添加元素
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//先把succ节点的前置节点存起来
final Node<E> pred = succ.prev;
//新节点插在pred与succ之间
final Node<E> newNode = new Node<>(pred, e, succ);
//succ的prev指针移到新节点
succ.prev = newNode;
//前置节点为空
if (pred == null)
//说明插入到了首位
first = newNode;
else
//把前置节点的next指针也指向新建的节点
pred.next = newNode;
size++;
modCount++;
}

//删除首位的元素,元素必须非空
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}

private E unlinkLast(Node<E> l) {
//...
}

//删除一个指定的节点
E unlink(Node<E> x) {
//...
}

可以看到,LinkedList提供了一系列方法用来插入和删除,但是却没有再实现一个方法来进行查询,因为对链表的查询是比较慢的,所以它是通过另外的方法来实现的,我们看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

//可以说尽力了
Node<E> node(int index) {
// assert isElementIndex(index);

//size>>1就是取一半的意思
//折半,将遍历次数减少一半
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

最后,我们看下它如何对应那些继承来的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//引用了node方法,需要遍历
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}

//也可能需要遍历
public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

//也要遍历
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}

public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}

public E element() {
return getFirst();
}

public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}

public E remove() {
return removeFirst();
}

public boolean offer(E e) {
return add(e);
}

public boolean offerFirst(E e) {
addFirst(e);
return true;
}

//...

总结

LinkedList非常适合大量数据的插入与删除,但其对处于中间位置的元素,无论是增删还是改查都需要折半遍历,这在数据量大时会十分影响性能。在使用时,尽量不要涉及查询与在中间插入数据,另外如果要遍历,也最好使用foreach,也就是Iterator提供的方式。

超接口Map

数组与链表在处理数据时各有优缺点,数组查询速度很快而插入很慢,链表在插入时表现优秀但查询无力。哈希表则整合了数组与链表的优点,能在插入和查找等方面都有不错的速度。我们之后要分析的HashMap就是基于哈希表实现的,不过在JDK1.8中还引入了红黑树,其性能进一步提升了。本文主要分析JDK中关于Map的定义。

接口Map

Map的定义为:

An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.

也就是基于key-value的数据格式,并且key值不可以重复,每个key对应的value唯一。Map的key也可以为null,也不可重。

在分析其定义的方法前,我们要先了解一下Map.Entry这个接口。

接口Map.Entry

存储在Map中的数据需要实现此接口,主要提供对key和value的操作,也是我们使用最多的操作。我们先分析它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取对应的key
K getKey();

// 获取对应的value
V getValue();

// 替换原有的value
V setValue(V value);

// 希望我们实现equals和hashCode
boolean equals(Object o);
int hashCode();

// 从1.8起,还提供了比较的方法,类似的方法共四个
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}

重要方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 返回当前数据个数
int size();

// 是否为空
boolean isEmpty();

// 判断是否包含key,这里用到了key的equals方法,所以key必须实现它
boolean containsKey(Object key);

// 判断是否有key保存的值是value,这也基于equals方法
boolean containsValue(Object value);

// 通过key获取对应的value值
V get(Object key);

// 存入key-value
V put(K key, V value);

// 移除一个key-value对
V remove(Object key);

// 从其他Map添加
void putAll(Map<? extends K, ? extends V> m);

// 清空
void clear();

// 返回所有的key至Set集合中,因为key是不可重的,Set也是不可重的
Set<K> keySet();

// 返回所有的values
Collection<V> values();

// 返回key-value对到Set中
Set<Map.Entry<K, V>> entrySet();

// 希望我们实现equals和hashCode
boolean equals(Object o);
int hashCode();

此外,还有一些Java8相关的default方法,就不一一展示了。

1
2
3
4
5
6
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}

超实现类:AbstractMap

对应于AbstractCollectionAbstractMap的作用也是类似的,主要是提供一些方法的实现,可以方便继承。下面我们看看它都实现了哪些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 // 返回大小,这里大小基于entrySet的大小
public int size() {
return entrySet().size();
}

public boolean isEmpty() {
return size() == 0;
}

//基于entrySet操作
public boolean containsKey(Object key) {
Iterator<Map.Entry<K,V>> i = entrySet().iterator();
if (key==null) {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
return true;
}
} else {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
return true;
}
}
return false;
}

public boolean containsValue(Object value) {
//...
}

public V get(Object key) {
//...
}

public V remove(Object key) {
//...
}

public void clear() {
entrySet().clear();
}

除此以外,还定义了两个变量:

1
2
transient Set<K>        keySet;
transient Collection<V> values;

还提供了默认的实现方法,我们只看其中一个吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new AbstractSet<K>() {
public Iterator<K> iterator() {
return new Iterator<K>() {
private Iterator<Entry<K,V>> i = entrySet().iterator();

public boolean hasNext() {
return i.hasNext();
}

public K next() {
return i.next().getKey();
}

public void remove() {
i.remove();
}
};
}

public int size() {
return AbstractMap.this.size();
}

public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}

public void clear() {
AbstractMap.this.clear();
}

public boolean contains(Object k) {
return AbstractMap.this.containsKey(k);
}
};
keySet = ks;
}
return ks;
}

除了以上相关方法以外,AbstractMap还实现了equalshashCodetoStringclone等方法,这样在具体实现时可以省去很多工作。

接口SortedMap

由于乱序的数据对查找不利,例如无法使用二分法等降低算法的时间复杂度,如果数据在插入时就排好顺序,查找的性能就会提升很多。SortedMap接口就是为这种有序数据服务的。

SortedMap接口需要数据的key支持Comparable,或者可以被指定的Comparator接受。SortedMap主要提供了以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 返回排序数据所用的Comparator
Comparator<? super K> comparator();

// 返回在[fromKey, toKey)之间的数据
SortedMap<K,V> subMap(K fromKey, K toKey);

// 返回从第一个元素到toKey之间的数据
SortedMap<K,V> headMap(K toKey);

// 返回从fromKey到末尾之间的数据
SortedMap<K,V> tailMap(K fromKey);

//返回第一个数据的key
K firstKey();

//返回最后一个数据的key
K lastKey();

SortedMap主要提供了获取子集,以及获取最大值(最后一个值)和最小值(第一个值)的方法。

接口NavigableMap

SortedMap提供了获取最大值与最小值的方法,但对于一个已经排序的数据集,除了最大值与最小值之外,我们可以对任何一个元素,找到比它小的值和比它大的值,还可以按照按照原有的顺序倒序排序等。NavigableMap就为我们提供了这些功能。

NavigableMap主要有以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 找到第一个比指定的key小的值
Map.Entry<K,V> lowerEntry(K key);

// 找到第一个比指定的key小的key
K lowerKey(K key);

// 找到第一个小于或等于指定key的值
Map.Entry<K,V> floorEntry(K key);

// 找到第一个小于或等于指定key的key
K floorKey(K key);

// 找到第一个大于或等于指定key的值
Map.Entry<K,V> ceilingEntry(K key);

K ceilingKey(K key);

// 找到第一个大于指定key的值
Map.Entry<K,V> higherEntry(K key);

K higherKey(K key);

// 获取最小值
Map.Entry<K,V> firstEntry();

// 获取最大值
Map.Entry<K,V> lastEntry();

// 删除最小的元素
Map.Entry<K,V> pollFirstEntry();

// 删除最大的元素
Map.Entry<K,V> pollLastEntry();

//返回一个倒序的Map
NavigableMap<K,V> descendingMap();

// 返回一个Navigable的key的集合,NavigableSet和NavigableMap类似
NavigableSet<K> navigableKeySet();

// 对上述集合倒序
NavigableSet<K> descendingKeySet();

TreeMap

TreeMap红黑树的java实现,对红黑树不太了解的可以查阅这篇文章红黑树红黑树能保证增、删、查等基本操作的时间复杂度为**O(lgN)**。本文将对TreeMap的源码进行分析。

image-20200728175501329

​ TreeMap结构图

Entry定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;

Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
// ... 省略其他方法
}

构造函数与成员变量

成员变量

1
2
3
4
5
6
7
8
// 比较器
private final Comparator<? super K> comparator;

// 根节点
private transient Entry<K,V> root;

// 大小
private transient int size = 0;

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 默认构造,比较器采用key的自然比较顺序
public TreeMap() {
comparator = null;
}

// 指定比较器
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}

// 从Map集合导入初始数据
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}

// 从SortedMap导入初始数据
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}

这里用到的putAllbuildFromSorted方法,在分析完增删查等重要方法之后再进行分析。

重要方法

增加一个元素

红黑树最复杂的地方就在于增删了,我们就从增加一个元素开始分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public V put(K key, V value) {
// 暂存根节点
Entry<K,V> t = root;

// 根节点空,就是还没有元素
if (t == null) {
compare(key, key); // type (and possibly null) check
// 新建一个元素,默认颜色黑色
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}

// 根节点不为空,有元素时的情况
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
// 初始化时指定了comparator比较器
if (cpr != null) {
do {
// 把t暂存到parent中
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
// 比较小,往左侧插入
t = t.left;
else if (cmp > 0)
// 比较大,往右侧插入
t = t.right;
else
// 一样大,所以就是更新当前值
return t.setValue(value);
} while (t != null);
}
else {
// 使用key的比较器,while循环原理和上述一致
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}

// 不断的比较,找到了没有相应儿子的节点
//(cmp<0就是没有左儿子,cmp>0就是没有右儿子)
Entry<K,V> e = new Entry<>(key, value, parent);
// 把数据插入
if (cmp < 0)
parent.left = e;
else
parent.right = e;

// 新插入的元素破坏了红黑树规则,需要调整
fixAfterInsertion(e);
size++;
modCount++;
return null;
}

fixAfterInsertion是实现的重难点,我们主要java是如何实现的,在这篇文章红黑树中有详细的解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private void fixAfterInsertion(Entry<K,V> x) {
// 先把x节点染成红色,这样可以不增加黑高,简化调整问题
x.color = RED;

// 条件是父节点是红色的,且x不是root节点,
// 因为到root节点后就走到另外的分支了,而那个分支是正确的
while (x != null && x != root && x.parent.color == RED) {
//x的父节点是其祖父节点的左儿子
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
// y是x的叔叔,也就是祖父节点的右儿子
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//叔叔是红色的
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
// 调整完毕,继续向上循环
x = parentOf(parentOf(x));
} else {
// 叔叔是黑色的
if (x == rightOf(parentOf(x))) {
// x是右节点,以其父节点左旋
x = parentOf(x);
rotateLeft(x);
}
// 右旋
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
//x的父节点是其祖父节点的右儿子
// y是其叔叔
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
//叔叔是红色的
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
// 调整完毕,继续向上循环
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
// x是左节点,以其父节点右旋
x = parentOf(x);
rotateRight(x);
}
//左旋
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}

//root节点颜色为黑色
root.color = BLACK;
}

左旋和右旋代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 右旋与左旋思路一致,只分析其一
// 结果相当于把p和p的儿子调换了
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
// 取出p的右儿子
Entry<K,V> r = p.right;
// 然后将p的右儿子的左儿子,也就是p的左孙子变成p的右儿子
p.right = r.left;
if (r.left != null)
// p的左孙子的父亲现在是p
r.left.parent = p;

// 然后把p的父亲,设置为p右儿子的父亲
r.parent = p.parent;
// 这说明p原来是root节点
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}

//和左旋类似
private void rotateRight(Entry<K,V> p) {
// ...
}

获取元素

TreeMap中的元素是有序的,当使用中序遍历时就可以得到一个有序的Set集合,所以获取元素可以采用二分法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}

除了获取某个元素外,还可以获取它的前一个元素与后一个元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 获取前一个元素
static <K,V> Entry<K,V> predecessor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.left != null) {
// t有左孩子,所以t的前一个元素是它左孩子所在的子树的最右侧叶子结点
Entry<K,V> p = t.left;
while (p.right != null)
p = p.right;
return p;
} else {
// t没有左孩子,所以t的前一个元素有两种情况
// 1. t是右孩子,那它的前一个元素就是它的父结点
// 2. t是左孩子,它的前一个元素需要向上递归,直到递归到下一个是右孩子的节点,转为情况1
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.left) {
ch = p;
p = p.parent;
}
return p;
}
}

// 获取后一个元素
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
//...
}

删除一个元素

从红黑树中删除一个元素,比增加一个元素还要复杂。我们看看java的实现:

1
2
3
4
5
6
7
8
9
10
public V remove(Object key) {
// 先用二分法获取这个元素,如果为null,不需要继续了
Entry<K,V> p = getEntry(key);
if (p == null)
return null;

V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;

// If strictly internal, copy successor's element to p and then make p
// point to successor.
//如果p有两个儿子,就把p指向它的后继者,也就是它后边的元素
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children

// Start fixup at replacement node, if it exists.
// p有一个儿子,或者没有儿子,获取到之后放在replacement中
Entry<K,V> replacement = (p.left != null ? p.left : p.right);

// p有儿子
if (replacement != null) {
// Link replacement to parent
// 把p的子孙接在p的父级
replacement.parent = p.parent;

//p是根节点
if (p.parent == null)
root = replacement;
//p是左儿子
else if (p == p.parent.left)
p.parent.left = replacement;
// p是右儿子
else
p.parent.right = replacement;

//把p的链接都删掉
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;

// Fix replacement
if (p.color == BLACK)
//修正
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else {
//p没有儿子
// No children. Use self as phantom replacement and unlink.
if (p.color == BLACK)
fixAfterDeletion(p);
// 把其父节点链接到p的都去掉
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}

修正的方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
// x是左儿子
if (x == leftOf(parentOf(x))) {
// sib是x的兄弟
Entry<K,V> sib = rightOf(parentOf(x));

// 兄弟是红色的
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}

// 兄弟没有孩子或者孩子是黑色的
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
// 兄弟的右孩子是黑色的
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
Entry<K,V> sib = leftOf(parentOf(x));

if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}

if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}

setColor(x, BLACK);
}

删除元素的过程相对简单些,在分析红黑树的文章里已经做了示例,这里就不再画图展示了。

遗留问题

在前面分析构造函数时,有两个函数putAllbuildFromSorted当时忽略了,现在我们来看看它们的实现。

1
2
3
4
5
6
7
8
9
10
11
public void putAll(Map<? extends K, ? extends V> map) {
int mapSize = map.size();
if (size==0 && mapSize!=0 && map instanceof SortedMap) {
//...
buildFromSorted(mapSize, map.entrySet().iterator(),
null, null);
//...
return;
}
super.putAll(map);
}

putAllMap是一个SortedMap实例时,依赖于buildFromSorted,其他情况则是由AbstractMap实现的。所以这里重点看下buildFromSorted的实现。

buildFromSorted有两个,一个是供putAll等调用的,另外一个则是具体的实现。

1
2
3
4
5
6
7
8
9
// 这个方法主要是被调用,关注它只为了看下computeRedLevel这个方法
private void buildFromSorted(int size, Iterator<?> it,
java.io.ObjectInputStream str,
V defaultVal)
throws java.io.IOException, ClassNotFoundException {
this.size = size;
root = buildFromSorted(0, 0, size - 1, computeRedLevel(size),
it, str, defaultVal);
}

这里调用了一个computeRedLevel的方法,是这里的关键。

1
2
3
4
5
6
7
private static int computeRedLevel(int sz) {
int level = 0;
for (int m = sz - 1; m >= 0; m = m / 2 - 1)
level++;

return level;
}

这个方法和染色为红色有关,其实现和二分法看似有一定联系,其文档说明它是:

Find the level down to which to assign all nodes BLACK. This is the last ‘full’ level of the complete binary tree produced by buildTree. The remaining nodes are colored RED. (This makes a `nice’ set of color assignments wrt future insertions.) This level number is computed by finding the number of splits needed to reach the zeroeth node. (The answer is ~lg(N), but in any case must be computed by same quick O(lg(N)) loop.)

大概意思是讲通过这种方式可以构建一个优秀的红黑树,能够为以后插入更多数据提供便利。

最后我们看下buildFromSorted的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private final Entry<K,V> buildFromSorted(int level, int lo, int hi,
int redLevel,
Iterator<?> it,
java.io.ObjectInputStream str,
V defaultVal)
throws java.io.IOException, ClassNotFoundException {

if (hi < lo) return null;

// 获取中间位置
int mid = (lo + hi) >>> 1;

Entry<K,V> left = null;
if (lo < mid)
// 递归左子树,和压栈类似,直到lo>=mid才能返回结果
left = buildFromSorted(level+1, lo, mid - 1, redLevel,
it, str, defaultVal);

// extract key and/or value from iterator or stream
K key;
V value;
if (it != null) {
// 给key和value赋值
if (defaultVal==null) {
Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
key = (K)entry.getKey();
value = (V)entry.getValue();
} else {
key = (K)it.next();
value = defaultVal;
}
} else { // use stream
// 从序列化中恢复
key = (K) str.readObject();
value = (defaultVal != null ? defaultVal : (V) str.readObject());
}

Entry<K,V> middle = new Entry<>(key, value, null);

// color nodes in non-full bottommost level red
//
if (level == redLevel)
middle.color = RED;

if (left != null) {
middle.left = left;
left.parent = middle;
}

if (mid < hi) {
Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
it, str, defaultVal);
middle.right = right;
right.parent = middle;
}

return middle;
}

HashMap

HashMap可能是我们使用最多的键值对型的集合类了,它的底层基于哈希表,采用数组存储数据,使用链表来解决哈希碰撞。在JDK1.8中还引入了红黑树来解决链表长度过长导致的查询速度下降问题。以下是文档对它的介绍中我们重点关注的部分:

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits *null* values and the *null* key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

An instance of HashMap has two parameters that affect its performance: *initial capacity* and *load factor*. The *capacity* is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The *load factor* is a measure of how full the hash table is allowed to get before its capacity is automatically increased.

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs.

Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use. And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used.

HashMap的结构如下所示:

image-20200728180057435

构造函数与成员变量

在看构造函数和成员变量前,我们要先看下其数据单元,因为HashMap有普通的元素,还有红黑树的元素,所以其数据单元定义有两个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 普通节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// ...
}

// 树节点,继承自LinkedHashMap.Entry
// 这是因为LinkedHashMap是HashMap的子类,也需要支持树化
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
// ...
}

// LinkedHashMap.Entry的实现
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}

TreeNode定义了一些相关操作的方法,我们会在使用时进行分析。

成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// capacity初始值,为16,必须为2的次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// capacity的最大值,为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;

// load factor,是指当容量被占满0.75时就需要rehash扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表长度到8,就转为红黑树
static final int TREEIFY_THRESHOLD = 8;

// 树大小为6,就转回链表
static final int UNTREEIFY_THRESHOLD = 6;

// 至少容量到64后,才可以转为树
static final int MIN_TREEIFY_CAPACITY = 64;

// 保存所有元素的table表
transient Node<K,V>[] table;

// 通过entrySet变量,提供遍历的功能
transient Set<Map.Entry<K,V>> entrySet;

// 下一次扩容值
int threshold;

// load factor
final float loadFactor;

构造函数

HashMap有多个构造函数,主要支持配置容量capacity和load factor,以及从其他Map集合获取初始化数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 public HashMap(int initialCapacity, float loadFactor) {
// ... 参数校验
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}

public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}

这些构造函数都很简单,putMapEntries也是依次插入元素的,我们后续分析put方法时就能理解其操作了,这里我们还要看下tableSizeFor这个方法:

1
2
3
4
5
6
7
8
9
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这里的方法和Deque中的一样

重要方法

无论是List还是Map,最重要的操作都是增删改查部分,我们还从增加一个元素开始分析。

增加一个元素

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

这里我们先关注下hash函数,在HashMap中其实现如下:

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里用到的方法很简单,就是把key与其高16位异或。文档中有如下说明:

There is a tradeoff between speed, utility, and quality of bit-spreading.

因为没有完美的哈希算法可以彻底避免碰撞,所以只能尽可能减少碰撞,在各方面权衡之后得到一个折中方案,这里我们就不再追究了。

put方法的具体实现在putVal中,我们看下其实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 参数onlyIfAbsent表示是否替换原值
// 参数evict我们可以忽略它,它主要用来区别通过put添加还是创建时初始化数据的
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)
// resize()不仅用来调整大小,还用来进行初始化配置
n = (tab = resize()).length;
// (n - 1) & hash这种方式也熟悉了吧?都在分析ArrayDeque中有体现
//这里就是看下在hash位置有没有元素,实际位置是hash % (length-1)
if ((p = tab[i = (n - 1) & hash]) == null)
// 将元素直接插进去
tab[i] = newNode(hash, key, value, null);
else {
//这时就需要链表或红黑树了
// e是用来查看是不是待插入的元素已经有了,有就替换
Node<K,V> e; K k;
// p是存储在当前位置的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //要插入的元素就是p,这说明目的是修改值
// 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);
// 链表比较长,需要树化,
// 由于初始即为p.next,所以当插入第9个元素才会树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到了对应元素,就可以停止了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 继续向后
p = e;
}
}
// e就是被替换出来的元素,这时候就是修改元素值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 默认为空实现,允许我们修改完成后做一些操作
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// size太大,达到了capacity的0.75,需要扩容
if (++size > threshold)
resize();
// 默认也是空实现,允许我们插入完成后做一些操作
afterNodeInsertion(evict);
return null;
}

以上方法和我们开头看到的文档描述一致,在插入时可能会从链表变成红黑树。里面用到了TreeNode.putTreeVal方法向红黑树中插入元素,关于TreeNode的方法我们最后分析。除此之外,还有一个树化的方法是treeifyBin,我们现在看下其原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果表是空表,或者长度还不到树化的最小值,就需要重新调整表了
// 这样做是为了防止最初就进行树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
// while循环的目的是把链表的每个节点转为TreeNode
do {
// 根据当前元素,生成一个对应的TreeNode节点
TreeNode<K,V> p = replacementTreeNode(e, null);
//挂在红黑树的尾部,顺序和链表一致
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
// 这里也用到了TreeNode的方法,我们在最后一起分析
// 通过头节点调节TreeNode
// 链表数据的顺序是不符合红黑树的,所以需要调整
hd.treeify(tab);
}
}

无论是在put还是treeify时,都依赖于resize,它的重要性不言而喻。它不仅可以调整大小,还能调整树化和反树化(从树变为链表)所带来的影响。我们看看它具体做了哪些工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 大小超过了2^30
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩容,扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 原来的threshold设置了
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 全部设为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 扩容完成,现在需要进行数据拷贝,从原表复制到新表
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 这是只有一个值的情况
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 重新规划树,如果树的size很小,默认为6,就退化为链表
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 处理链表的数据
// loXXX指的是在原表中出现的位置
Node<K,V> loHead = null, loTail = null;
// hiXXX指的是在原表中不包含的位置
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//这里把hash值与oldCap按位与。
//oldCap是2的次幂,所以除了最高位为1以外其他位都是0
// 和它按位与的结果为0,说明hash比它小,原表有这个位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 挂在原表相应位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 挂在后边
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

删除一个元素

1
2
3
4
5
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}

和插入一样,其实际的操作在removeNode方法中完成,我们看下其实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// matchValue是说只有value值相等时候才可以删除,我们是按照key删除的,所以可以忽略它。
// movable是指是否允许移动其他元素,这里是和TreeNode相关的
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 不同情况下获取待删除的node节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
// TreeNode删除
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
// 在队首,直接删除
tab[index] = node.next;
else
// 链表中删除
p.next = node.next;
++modCount;
--size;
// 默认空实现,允许我们删除节点后做些处理
afterNodeRemoval(node);
return node;
}
}
return null;
}

获取一个元素

除了增删之外,重要的就是查询操作了。查询的get方法也是通过调用getNode方法完成的,我们看下其实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

这里逻辑和我们分析的增删很类似,再读起来就很简单了。

TreeNode方法介绍

在前面分析增删时,可以发现与红黑树相关的操作都是通过TreeNode来实现的,下面我们就来看看TreeNode的具体实现:

TreeNode算上其继承来的成员变量,共有11个:

1
2
3
4
5
6
7
8
9
10
final int hash;
final K key;
V value;
Node<K,V> next;
Entry<K,V> before, after;
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;

这么多的变量,说明其功能十分强大。这主要是因为它需要在树和链表之间来回转换。下面按照本文中出现的方法顺序对其函数进行分析。

首先是在添加元素时使用到了TreeNode.putTreeVal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 获取到root节点
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
// dir表示查询方向
int dir, ph; K pk;
// 要插入的位置在树的左侧
// 树化会依据key的hash值
if ((ph = p.hash) > h)
dir = -1;
// 要插入的位置在树的右侧
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p; //找到了,替换即可

// comparableClassFor是如果key实现了Comparable就返回具体类型,否则返回null
// compareComparables是比较传入的key和当前遍历元素的key
// 只有当前hash值与传入的hash值一致才会走到这里
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
//左右都查过了
searched = true;
// 通过hash和Comparable都找不到,只能从根节点开始遍历
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
// 元素的hashCode一致,且没有实现Comparable,在树里也没有
// tieBreakOrder则是调用System.identityHashCode(Object o)来进行比较,
//它的意思是说不管有没有覆写hashCode,都强制使用Object类的hashCode
// 这样做,是为了保持一致性的插入
dir = tieBreakOrder(k, pk);
}

// 代码执行到这,说明没有找到元素,也就是需要新建并插入了
TreeNode<K,V> xp = p;
// 经历过上述for循环,p已经到某个叶节点了
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;

// moveRootToFront目的很明确也是必须的。
// 因为这个红黑树需要挂在数组的某个位置,所以其首个元素必须是root
// balanceInsertion是因为插入元素后可能不符合红黑树定义了
// 这部分知识在分析TreeMap中有详细介绍
// 需要了解的话可以查看文末链接
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}

除了putTreeVal之外,我们还调用过treeify以及removeTreeNode等方法。这些方法的过程都和putTreeVal类似,大家感兴趣可以自己去分析,这里就不再介绍了。

HashMap 到底有哪些线程安全问题

并发 put 可能导致元素的丢失

试验代码如下:(仅作为可能会产生这个问题的样例代码,直接运行不一定会产生问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ConcurrentIssueDemo1 {

private static Map<String, String> map = new HashMap<>();

public static void main(String[] args) {
// 线程1 => t1
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 99999999; i++) {
map.put("thread1_key" + i, "thread1_value" + i);
}
}
}).start();
// 线程2 => t2
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 99999999; i++) {
map.put("thread2_key" + i, "thread2_value" + i);
}
}
}).start();
}
}

先回顾一下 put 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, I;
// 初始化hash表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过hash值计算在hash表中的位置,并将这个位置上的元素赋值给p,如果是空的则new一个新的node放在这个位置上
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// hash表的当前index已经存在元素,向这个元素后追加链表
Node<K,V> e; K k;
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) { // #1
p.next = newNode(hash, key, value, null); // #2
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = 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;
}

假设当前HashMap中的table状态如下:

image-20210728210417225

此时 t1 和 t2 同时执行 put,假设 t1 执行 put(“key2”, “value2”),t2 执行 put(“key3”, “value3”),并且 key2 和 key3 的 hash 值与图中的key1相同。

那么正常情况下,put 完成后,table 的状态应该是下图二者其一

image-20210728210552749

下面来看看异常情况

假设线程 1、线程 2 现在都执行到 put 源代码中 #1 的位置,且当前 table 状态如下

image-20210728210845507

然后两个线程都执行了if ((e = p.next) == null)这句代码,来到了 #2 这行代码。此时假设 t1 先执行p.next = newNode(hash, key, value, null);那么 table 会变成如下状态

image-20210728211039574

紧接着 t2 执行p.next = newNode(hash, key, value, null);

此时 table 会变成如下状态

image-20210728211131552

这样一来,key2元素就丢了。

put 和 get 并发时,可能导致 get 为 null

场景:线程 1 执行 put 时,因为元素个数超出 threshold 而导致 rehash,线程 2 此时执行 get,有可能导致这个问题。

分析如下:

先看下 resize 方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// hash表
transient Node<K,V>[] table;

final Node<K,V>[] resize() {
// 计算新hash表容量大小,begin
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 计算新hash表容量大小,end

@SuppressWarnings({"rawtypes","unchecked”})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // #1
table = newTab; // #2
// rehash begin
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// rehash end
return newTab;
}

大致意思是,先计算新的容量和 threshold,在创建一个新 hash 表,最后将旧 hash 表中元素 rehash 到新的 hash 表中。

重点代码在于 #1 和 #2 两句:在代码 #1 位置,用新计算的容量 new 了一个新的 hash 表,#2 将新创建的空 hash 表赋值给实例变量 table。

注意此时实例变量 table 是空的。

那么,如果此时另一个线程执行 get 时,就会 get 出 null。

JDK7 中 HashMap 并发 put 会造成循环链表,导致 get 时出现死循环

此问题是由两个问题组成:循环链表和死循环

  • 循环链表的形成:
    发生在多线程并发 resize 的情况下。相关源码主要是 resize 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    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];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
    * Transfers all entries from current table to newTable.
    */
    // 关键在于这个transfer方法,这个方法的作用是将旧hash表中的元素rehash到新的hash表中
    void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) { // table变量即为旧hash表
    while(null != e) {
    // #1
    Entry<K,V> next = e.next;
    if (rehash) {
    e.hash = null == e.key ? 0 : hash(e.key);
    }
    // 用元素的hash值计算出这个元素在新hash表中的位置
    int i = indexFor(e.hash, newCapacity);
    // #2
    e.next = newTable[I];
    // #3
    newTable[i] = e;
    // #4
    e = next;
    }
    }
    }

    假设线程 1(t1) 和线程 2(t2) 同时 resize,两个线程 resize 前,两个线程及 hashmap 的状态如下:

    image-20210728211902638

    堆内存中的 HashMap 对象中的 table 字段指向旧的 hash 表,其中 index 为 7 的位置有两个元素,我们以这两个元素的 rehash 为例,看看循环链表是如何形成的。

    线程 1 和线程2分别 new 了一个 hash 表,用 newTable 字段表示。

    PS:如果将每一步的执行都以图的形式呈现出来,篇幅过大,这里提供一下每次循环结束时的状态,可以根据代码和每一步的解释一步一步推算。

    Step1: t2 执行完 #1 代码后,CPU 切走执行 t1,并且 t1 执行完成

    这里可以根据上图推算一下,此时状态如下

    image-20210728211948224

    用 t2.e 表示线程 2 中的局部变量 e,t2.nex t同理。

    Step2: t2 继续向下执行完本次循环

    image-20210728212021302

    Step3: t2 继续执行下一次循环

    image-20210728212048687

    Step4: t2 继续下一次循环,循环链表出现

    image-20210728212117939

  • 死循环的出现:

HashMap.get 方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);

return null == entry ? null : entry.getValue();
}

final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}

int hash = (key == null) ? 0 : hash(key);
// 遍历链表
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 != null && key.equals(k))))
return e;
}
return null;
}

由上图可知,for 循环中的 e = e.next 永远不会为空,那么,如果 get 一个在这个链表中不存在的 key 时,就会出现死循环了。

JDK1.7 和 JDK 1.8 的区别及常见问题

区别 JDK1.7 JDK1.8 说明
插入方法 头插法 尾插法 头插法逆序,且多线程容易出现循环死锁
扩容后位置计算方式 rehash 原位置|原位置+扩容大小 默认大小16,扩容 >> 1
数据结构 数组+链表 数组+链表+红黑树 树化阈值8,链化阈值6
  • 为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8
    • 泊松分布

当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件

  • 哈希表如何解决Hash冲突?

    • 预防措施:hash算法、扰动计算
    • 解决方案:扩容机制、数据结构
  • 为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键

    • 包装类的特性保证了 key 的不可变性和 hash 计算准确性,内部实现了 equals() 和 hashCode()
  • HashMap 中的 key若 Object类型, 则需实现哪些方法?

    • equals() 和 hashCode() ,用于hash运算和查找比较

总结

HashMap是目前分析的最复杂的集合类了。合理的使用它能够在增删改查等方面都有很好的表现。在使用时我们需要注意以下几点:

  1. 设计的key对象一定要实现hashCode方法,并尽可能保证均匀少重复。
  2. 由于树化过程会依次通过hash值、比较值和对象的hash值进行排序,所以key还可以实现Comparable,以方便树化时进行比较。
  3. 如果可以预先估计数量级,可以指定initial capacity,以减少rehash的过程。
  4. 虽然HashMap引入了红黑树,但它的使用是很少的,如果大量出现红黑树,说明数据本身设计的不合理,我们应该从数据源寻找优化方案。

LinkedHashMap

LinkedHashMapHashMap的子类,所以也具备HashMap的诸多特性。不同的是,LinkedHashMap还维护了一个双向链表,以保证通过Iterator遍历时顺序与插入顺序一致。除此之外,它还支持Access Order,即按照元素被访问的顺序来排序,我们熟知的LRUCache底层就依赖于此。以下是文档中需要我们注意的点:

Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs from *HashMap* in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order). Note that insertion order is not affected if a key is re-inserted into the map.

A special *LinkedHashMap(int,float,boolean)* constructor is provided to create a linked hash map whose order of iteration is the order in which its entries were last accessed, from least-recently accessed to most-recently (access-order). This kind of map is well-suited to building LRU caches.

The *removeEldestEntry(Map.Entry)* method may be overridden to impose a policy for removing stale mappings automatically when new mappings are added to the map.

Note, however, that the penalty for choosing an excessively high value for initial capacity is less severe for this class than for HashMap, as iteration times for this class are unaffected by capacity.

下面我们就从构造函数和成员变量开始分析其具体实现。

构造函数与成员变量

成员变量

在分析成员变量前,我们先看下其存储元素的结构。

1
2
3
4
5
6
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}

这个EntryHashMap中被引用过,主要是为了能让LinkedHashMap也支持树化。在这里则是用来存储元素。

1
2
3
4
5
6
7
8
// 双向链表的头,用作AccessOrder时也是最老的元素
transient LinkedHashMap.Entry<K,V> head;

// 双向链表的尾,用作AccessOrder时也是最新的元素
transient LinkedHashMap.Entry<K,V> tail;

// true则为访问顺序,false则为插入顺序
final boolean accessOrder;

构造函数

关于LinkedHashMap的构造函数我们只关注一个,其他的都和HashMap类似,只是把accessOrder设置为了false。在上边的文档说过,initialCapacity并没有在HashMap中那般重要,因为链表不需要像数组那样必须先声明足够的空间。下面这个构造函数是支持访问顺序的。

1
2
3
4
5
6
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

重要方法

LinkedHashMap并没有再实现一整套增删改查的方法,而是通过复写HashMap在此过程中定义的几个方法来实现的。对此不熟悉的可以查看文末关于HashMap分析的文章,或者对照HashMap的源码来看。

插入一个元素

HashMap在插入时,调用了newNode来新建一个节点,或者是通过replacementNode来替换值。在树化时也有两个对应的方法,分别是newTreeNodereplacementTreeNode。完成之后,还调用了afterNodeInsertion方法,这个方法允许我们在插入完成后做些事情,默认是空实现。

为了方便分析,我们会对比HashMap中的实现与LinkedHashMap的实现,来摸清它是如何做的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// HashMap中的实现
Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
return new Node<>(hash, key, value, next);
}

// LinkedHashMap中的实现
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}

// HashMap中的实现
Node<K, V> replacementNode(Node<K, V> p, Node<K, V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}

// LinkedHashMap中的实现
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
LinkedHashMap.Entry<K,V> t =
new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
transferLinks(q, t);
return t;
}

// newTreeNode和replacementTreeNode和此类似

通过以上对比,可以发现,LinkedHashMap在新增时,调用了linkNodeLast,再替换时调用了transferLinks。以下是这两个方法的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 就是将元素挂在链尾
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}

// 用dst替换src
private void transferLinks(LinkedHashMap.Entry<K,V> src,
LinkedHashMap.Entry<K,V> dst) {
LinkedHashMap.Entry<K,V> b = dst.before = src.before;
LinkedHashMap.Entry<K,V> a = dst.after = src.after;
if (b == null)
head = dst;
else
b.after = dst;
if (a == null)
tail = dst;
else
a.before = dst;
}

最后我们看下afterNodeInsertion做了哪些事情吧:

1
2
3
4
5
6
7
8
9
10
// evict在HashMap中说过,为false表示是创建阶段
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
// 不是创建阶段
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
// 自动删除最老的元素,也就是head元素
removeNode(hash(key), key, null, false, true);
}
}

removeEldestEntry是当想要在插入元素时自动删除最老的元素时需要复写的方法。其默认实现如下:

1
2
3
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}

查询

因为要支持访问顺序,所以获取元素的方法和HashMap也有所不同。下面我们看下其实现:

1
2
3
4
5
6
7
8
9
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
// 数据被访问,需要将其移动到末尾
afterNodeAccess(e);
return e.value;
}

getNode方法是在HashMap中实现的,所以这是包装了一下HashMap的方法,并添加了一个afterNodeAccess,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
// e元素不在末尾
if (accessOrder && (last = tail) != e) {
// p是e,b是前一个元素,a是后一个元素
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// e要放在末尾,所以没有after
p.after = null;

// 把e去掉,把b和a接起来
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;

//把e接在末尾
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}

关于LinkedHashMap的分析就到这里了,其他关于Iterator的内容都和Collection是大同小异的,感兴趣的可以去查看相关源码。

Set概述

因为Set的结构及实现都和Map保持高度一致,这里将不再对其进行分析了,感兴趣的朋友可以自行查看源码。但我们还是需要知道什么是SetSet是一个包含不可重元素的集合,也就是所有的元素都是唯一的。还是看下文档说明吧:

A collection that contains no duplicate elements. More formally, sets contain no pair of elements e1 and e2 such that *e1.equals(e2)*, and at most one null element. As implied by its name, this interface models the mathematical set abstraction.

此外Set系列也有SortedSetNavigableSet这种基于排序的接口,它们的作用在分析Map时都已经详细介绍过了。

总结

在Java的集合类中,大量的依赖于对象的equalshashCodeclone方法,有些还需要我们实现Comparable接口。如果对数据结构有所理解,又清楚集合类用了哪些个数据结构,我想需要实现哪些方法是可以推测出来的。如果我们能把握这些细节,就能写出更优秀的代码。如果我们能掌握这些思想,就能超脱语言的束缚,理解软件设计的精髓。

该文图片及笔记整理自 简书 作者:大大纸飞机 链接:https://www.jianshu.com/p/d68eea1a3a8c

部分图片及文字整理自 掘金 作者:Mr羽墨青衫 链接:https://juejin.cn/post/6844903796225605640