类图

集合类存放于 Java.util 包中,主要有 3 种:set(集)、list(列表包含 Queue)和 map(映射)。

  1. Collection:Collection 是集合 List、Set、Queue 的最基本的接口。

  2. Iterator:迭代器,可以通过迭代器遍历集合中的数据

  3. Map:是映射表的基础接口

image.png


ArrayList

ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。
它继承于 AbstractList,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。
在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为O(n),求表长以及增加元素,取第 i 元素的时间复杂度为O(1)ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。

补充内容:RandomAccess接口

  1. public interface RandomAccess {
  2. }

查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。

在 binarySearch()方法中,它要判断传入的list 是否 RamdomAccess 的实例,如果是,调用indexedBinarySearch()方法,如果不是,那么调用iteratorBinarySearch()方法

  1. public static <T>
  2. int binarySearch(List<? extends Comparable<? super T>> list, T key) {
  3. if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
  4. return Collections.indexedBinarySearch(list, key);
  5. else
  6. return Collections.iteratorBinarySearch(list, key);
  7. }

ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有 关!ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!

下面再总结一下 list 的遍历方式选择:

实现了 RandomAccess 接口的list,优先选择普通 for 循环 ,其次 foreach, 未实现 RandomAccess接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的,),大size的数据,千万不要使用普通for循环   ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。   ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化能通过序列化去传输。   和 Vector 不同,ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。

  • 先从 ArrayList 的构造函数说起

ArrayList有三种方式来初始化,构造方法源码如下:

  1. /**
  2. * 默认初始容量大小
  3. */
  4. private static final int DEFAULT_CAPACITY = 10;
  5. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  6. /**
  7. *默认构造函数,使用初始容量10构造一个空列表(无参数构造)
  8. */
  9. public ArrayList() {
  10. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  11. }
  12. /**
  13. * 带初始容量参数的构造函数。(用户自己指定容量)
  14. */
  15. public ArrayList(int initialCapacity) {
  16. if (initialCapacity > 0) {//初始容量大于0
  17. //创建initialCapacity大小的数组
  18. this.elementData = new Object[initialCapacity];
  19. } else if (initialCapacity == 0) {//初始容量等于0
  20. //创建空数组
  21. this.elementData = EMPTY_ELEMENTDATA;
  22. } else {//初始容量小于0,抛出异常
  23. throw new IllegalArgumentException("Illegal Capacity: "+
  24. initialCapacity);
  25. }
  26. }
  27. /**
  28. *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
  29. *如果指定的集合为null,throws NullPointerException。
  30. */
  31. public ArrayList(Collection<? extends E> c) {
  32. elementData = c.toArray();
  33. if ((size = elementData.length) != 0) {
  34. // c.toArray might (incorrectly) not return Object[] (see 6260652)
  35. if (elementData.getClass() != Object[].class)
  36. elementData = Arrays.copyOf(elementData, size, Object[].class);
  37. } else {
  38. // replace with empty array.
  39. this.elementData = EMPTY_ELEMENTDATA;
  40. }
  41. }

细心的同学一定会发现 :以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。 下面在我们分析 ArrayList 扩容时会讲到这一点内容!

  • 一步一步分析 ArrayList 扩容机制

这里以无参构造函数创建的 ArrayList 为例分析
1. 先来看 add 方法

  1. /**
  2. * 将指定的元素追加到此列表的末尾。
  3. */
  4. public boolean add(E e) {
  5. //添加元素之前,先调用ensureCapacityInternal方法
  6. ensureCapacityInternal(size + 1); // Increments modCount!!
  7. //这里看到ArrayList添加元素的实质就相当于为数组赋值
  8. elementData[size++] = e;
  9. return true;
  10. }

2. 再来看看 ensureCapacityInternal() 方法
可以看到 add 方法 首先调用了ensureCapacityInternal(size + 1)

  1. //得到最小扩容量
  2. private void ensureCapacityInternal(int minCapacity) {
  3. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  4. // 获取默认的容量和传入参数的较大值
  5. minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  6. }
  7. ensureExplicitCapacity(minCapacity);
  8. }

当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10。
3. ensureExplicitCapacity() 方法
如果调用 ensureCapacityInternal() 方法就一定会进过(执行)这个方法,下面我们来研究一下这个方法的源码!

  1. //判断是否需要扩容
  2. private void ensureExplicitCapacity(int minCapacity) {
  3. modCount++;
  4. // overflow-conscious code
  5. if (minCapacity - elementData.length > 0)
  6. //调用grow方法进行扩容,调用此方法代表已经开始扩容了
  7. grow(minCapacity);
  8. }

我们来仔细分析一下:

  • 当我们要 add 进第1个元素到 ArrayList 时,elementData.length 为0 (因为还是一个空的 list),因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法。
  • 当add第2个元素时,minCapacity 为2,此时e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0不成立,所以不会进入 (执行)grow(minCapacity) 方法。
  • 添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。

直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容。
4. grow() 方法

  1. /**
  2. * 要分配的最大数组大小
  3. */
  4. private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
  5. /**
  6. * ArrayList扩容的核心方法。
  7. */
  8. private void grow(int minCapacity) {
  9. // oldCapacity为旧容量,newCapacity为新容量
  10. int oldCapacity = elementData.length;
  11. //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
  12. //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
  13. int newCapacity = oldCapacity + (oldCapacity >> 1);
  14. //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
  15. if (newCapacity - minCapacity < 0)
  16. newCapacity = minCapacity;
  17. // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
  18. //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
  19. if (newCapacity - MAX_ARRAY_SIZE > 0)
  20. newCapacity = hugeCapacity(minCapacity);
  21. // minCapacity is usually close to size, so this is a win:
  22. elementData = Arrays.copyOf(elementData, newCapacity);
  23. }

int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍!(JDK1.6版本以后) JDk1.6版本时,扩容之后容量为 1.5 倍+1!详情请参考源码

“>>”(移位运算符):>>1 右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源  

我们再来通过例子探究一下grow() 方法 :

  • 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 hugeCapacity 方法。数组容量为10,add方法中 return true,size增为1。
  • 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。
  • 以此类推······

这里补充一点比较重要,但是容易被忽视掉的知识点:

  • java 中的 length属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.
  • java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法.
  • java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!

5. hugeCapacity() 方法。
从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8

  1. private static int hugeCapacity(int minCapacity) {
  2. if (minCapacity < 0) // overflow
  3. throw new OutOfMemoryError();
  4. //对minCapacity和MAX_ARRAY_SIZE进行比较
  5. //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
  6. //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
  7. //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
  8. return (minCapacity > MAX_ARRAY_SIZE) ?
  9. Integer.MAX_VALUE :
  10. MAX_ARRAY_SIZE;
  11. }
  • System.arraycopy()Arrays.copyOf()方法

阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)toArray() 等方法中都用到了该方法!

System.arraycopy() 方法

  1. /**
  2. * 在此列表中的指定位置插入指定的元素。
  3. *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
  4. *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
  5. */
  6. public void add(int index, E element) {
  7. rangeCheckForAdd(index);
  8. ensureCapacityInternal(size + 1); // Increments modCount!!
  9. //arraycopy()方法实现数组自己复制自己
  10. //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
  11. System.arraycopy(elementData, index, elementData, index + 1, size - index);
  12. elementData[index] = element;
  13. size++;
  14. }

我们写一个简单的方法测试以下:

  1. public class ArraycopyTest {
  2. public static void main(String[] args) {
  3. // TODO Auto-generated method stub
  4. int[] a = new int[10];
  5. a[0] = 0;
  6. a[1] = 1;
  7. a[2] = 2;
  8. a[3] = 3;
  9. System.arraycopy(a, 2, a, 3, 3);
  10. a[2]=99;
  11. for (int i = 0; i < a.length; i++) {
  12. System.out.println(a[i]);
  13. }
  14. }
  15. }

结果:

  1. 0 1 99 2 3 0 0 0 0 0

Arrays.copyOf()方法

  1. /**
  2. 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。
  3. */
  4. public Object[] toArray() {
  5. //elementData:要复制的数组;size:要复制的长度
  6. return Arrays.copyOf(elementData, size);
  7. }

个人觉得使用 Arrays.copyOf()方法主要是为了给原有数组扩容,测试代码如下:

  1. public class ArrayscopyOfTest {
  2. public static void main(String[] args) {
  3. int[] a = new int[3];
  4. a[0] = 0;
  5. a[1] = 1;
  6. a[2] = 2;
  7. int[] b = Arrays.copyOf(a, 10);
  8. System.out.println("b.length"+b.length);
  9. }
  10. }

结果:

  1. 10

两者联系和区别

联系:

看两者源代码可以发现 copyOf() 内部实际调用了 System.arraycopy() 方法

区别:

arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组。

ensureCapacity方法

ArrayList 源码中有一个 ensureCapacity 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢?

  1. /**
  2. 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
  3. *
  4. * @param minCapacity 所需的最小容量
  5. */
  6. public void ensureCapacity(int minCapacity) {
  7. int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
  8. // any size if not default element table
  9. ? 0
  10. // larger than default for default empty table. It's already
  11. // supposed to be at default size.
  12. : DEFAULT_CAPACITY;
  13. if (minCapacity > minExpand) {
  14. ensureExplicitCapacity(minCapacity);
  15. }
  16. }

最好在 add 大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数

我们通过下面的代码实际测试以下这个方法的效果:

  1. public class EnsureCapacityTest {
  2. public static void main(String[] args) {
  3. ArrayList<Object> list = new ArrayList<Object>();
  4. final int N = 10000000;
  5. long startTime = System.currentTimeMillis();
  6. for (int i = 0; i < N; i++) {
  7. list.add(i);
  8. }
  9. long endTime = System.currentTimeMillis();
  10. System.out.println("使用ensureCapacity方法前:"+(endTime - startTime));
  11. }
  12. }

运行结果:

  1. 使用ensureCapacity方法前:2158
  1. public class EnsureCapacityTest {
  2. public static void main(String[] args) {
  3. ArrayList<Object> list = new ArrayList<Object>();
  4. final int N = 10000000;
  5. list = new ArrayList<Object>();
  6. long startTime1 = System.currentTimeMillis();
  7. list.ensureCapacity(N);
  8. for (int i = 0; i < N; i++) {
  9. list.add(i);
  10. }
  11. long endTime1 = System.currentTimeMillis();
  12. System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1));
  13. }
  14. }

运行结果:

  1. 使用ensureCapacity方法前:1773

通过运行结果,我们可以看出向 ArrayList 添加大量元素之前最好先使用ensureCapacity 方法,以减少增量重新分配的次数。


LinkedList

LinkedList是一个实现了List接口和Deque接口的双端链表。
LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性;
LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:

  1. List list=Collections.synchronizedList(new LinkedList(...));

内部结构分析

如下图所示:
Java 集合 - 图2
看完了图之后,我们再看LinkedList类中的一个内部私有类Node就很好理解了:

  1. private static class Node<E> {
  2. E item;//节点值
  3. Node<E> next;//后继节点
  4. Node<E> prev;//前驱节点
  5. Node(Node<E> prev, E element, Node<E> next) {
  6. this.item = element;
  7. this.next = next;
  8. this.prev = prev;
  9. }
  10. }

这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。

LinkedList源码分析

构造方法

空构造方法:

  1. public LinkedList() {
  2. }

用已有的集合创建链表的构造方法:

  1. public LinkedList(Collection<? extends E> c) {
  2. this();
  3. addAll(c);
  4. }

add方法

add(E e) 方法:将元素添加到链表尾部

  1. public boolean add(E e) {
  2. linkLast(e);//这里就只调用了这一个方法
  3. return true;
  4. }
  1. /**
  2. * 链接使e作为最后一个元素。
  3. */
  4. void linkLast(E e) {
  5. final Node<E> l = last;
  6. final Node<E> newNode = new Node<>(l, e, null);
  7. last = newNode;//新建节点
  8. if (l == null)
  9. first = newNode;
  10. else
  11. l.next = newNode;//指向后继元素也就是指向下一个元素
  12. size++;
  13. modCount++;
  14. }

add(int index,E e):在指定位置添加元素

  1. public void add(int index, E element) {
  2. checkPositionIndex(index); //检查索引是否处于[0-size]之间
  3. if (index == size)//添加在链表尾部
  4. linkLast(element);
  5. else//添加在链表中间
  6. linkBefore(element, node(index));
  7. }

linkBefore方法需要给定两个参数,一个插入节点的值,一个指定的node,所以我们又调用了Node(index)去找到index对应的node

addAll(Collection c ):将集合插入到链表尾部

  1. public boolean addAll(Collection<? extends E> c) {
  2. return addAll(size, c);
  3. }

addAll(int index, Collection c): 将集合从指定位置开始插入

  1. public boolean addAll(int index, Collection<? extends E> c) {
  2. //1:检查index范围是否在size之内
  3. checkPositionIndex(index);
  4. //2:toArray()方法把集合的数据存到对象数组中
  5. Object[] a = c.toArray();
  6. int numNew = a.length;
  7. if (numNew == 0)
  8. return false;
  9. //3:得到插入位置的前驱节点和后继节点
  10. Node<E> pred, succ;
  11. //如果插入位置为尾部,前驱节点为last,后继节点为null
  12. if (index == size) {
  13. succ = null;
  14. pred = last;
  15. }
  16. //否则,调用node()方法得到后继节点,再得到前驱节点
  17. else {
  18. succ = node(index);
  19. pred = succ.prev;
  20. }
  21. // 4:遍历数据将数据插入
  22. for (Object o : a) {
  23. @SuppressWarnings("unchecked") E e = (E) o;
  24. //创建新节点
  25. Node<E> newNode = new Node<>(pred, e, null);
  26. //如果插入位置在链表头部
  27. if (pred == null)
  28. first = newNode;
  29. else
  30. pred.next = newNode;
  31. pred = newNode;
  32. }
  33. //如果插入位置在尾部,重置last节点
  34. if (succ == null) {
  35. last = pred;
  36. }
  37. //否则,将插入的链表与先前链表连接起来
  38. else {
  39. pred.next = succ;
  40. succ.prev = pred;
  41. }
  42. size += numNew;
  43. modCount++;
  44. return true;
  45. }

上面可以看出addAll方法通常包括下面四个步骤:

  1. 检查index范围是否在size之内
  2. toArray()方法把集合的数据存到对象数组中
  3. 得到插入位置的前驱和后继节点
  4. 遍历数据,将数据插入到指定位置

addFirst(E e): 将元素添加到链表头部

  1. public void addFirst(E e) {
  2. linkFirst(e);
  3. }
  1. private void linkFirst(E e) {
  2. final Node<E> f = first;
  3. final Node<E> newNode = new Node<>(null, e, f);//新建节点,以头节点为后继节点
  4. first = newNode;
  5. //如果链表为空,last节点也指向该节点
  6. if (f == null)
  7. last = newNode;
  8. //否则,将头节点的前驱指针指向新节点,也就是指向前一个元素
  9. else
  10. f.prev = newNode;
  11. size++;
  12. modCount++;
  13. }

addLast(E e): 将元素添加到链表尾部,与 add(E e) 方法一样

  1. public void addLast(E e) {
  2. linkLast(e);
  3. }

根据位置取数据的方法

get(int index): 根据指定索引返回数据

  1. public E get(int index) {
  2. //检查index范围是否在size之内
  3. checkElementIndex(index);
  4. //调用Node(index)去找到index对应的node然后返回它的值
  5. return node(index).item;
  6. }

获取头节点(index=0)数据方法:

  1. public E getFirst() {
  2. final Node<E> f = first;
  3. if (f == null)
  4. throw new NoSuchElementException();
  5. return f.item;
  6. }
  7. public E element() {
  8. return getFirst();
  9. }
  10. public E peek() {
  11. final Node<E> f = first;
  12. return (f == null) ? null : f.item;
  13. }
  14. public E peekFirst() {
  15. final Node<E> f = first;
  16. return (f == null) ? null : f.item;
  17. }

区别:
getFirst(),element(),peek(),peekFirst()
这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中getFirst()element() 方法将会在链表为空时,抛出异常

element()方法的内部就是使用getFirst()实现的。它们会在链表为空时,抛出NoSuchElementException
获取尾节点(index=-1)数据方法:

  1. public E getLast() {
  2. final Node<E> l = last;
  3. if (l == null)
  4. throw new NoSuchElementException();
  5. return l.item;
  6. }
  7. public E peekLast() {
  8. final Node<E> l = last;
  9. return (l == null) ? null : l.item;
  10. }

两者区别:
getLast() 方法在链表为空时,会抛出NoSuchElementException,而peekLast() 则不会,只是会返回 null

根据对象得到索引的方法

int indexOf(Object o): 从头遍历找

  1. public int indexOf(Object o) {
  2. int index = 0;
  3. if (o == null) {
  4. //从头遍历
  5. for (Node<E> x = first; x != null; x = x.next) {
  6. if (x.item == null)
  7. return index;
  8. index++;
  9. }
  10. } else {
  11. //从头遍历
  12. for (Node<E> x = first; x != null; x = x.next) {
  13. if (o.equals(x.item))
  14. return index;
  15. index++;
  16. }
  17. }
  18. return -1;
  19. }

int lastIndexOf(Object o): 从尾遍历找

  1. public int lastIndexOf(Object o) {
  2. int index = size;
  3. if (o == null) {
  4. //从尾遍历
  5. for (Node<E> x = last; x != null; x = x.prev) {
  6. index--;
  7. if (x.item == null)
  8. return index;
  9. }
  10. } else {
  11. //从尾遍历
  12. for (Node<E> x = last; x != null; x = x.prev) {
  13. index--;
  14. if (o.equals(x.item))
  15. return index;
  16. }
  17. }
  18. return -1;
  19. }

检查链表是否包含某对象的方法:

contains(Object o): 检查对象o是否存在于链表中

  1. public boolean contains(Object o) {
  2. return indexOf(o) != -1;
  3. }

删除方法

remove() ,removeFirst(),pop(): 删除头节点

  1. public E pop() {
  2. return removeFirst();
  3. }
  4. public E remove() {
  5. return removeFirst();
  6. }
  7. public E removeFirst() {
  8. final Node<E> f = first;
  9. if (f == null)
  10. throw new NoSuchElementException();
  11. return unlinkFirst(f);
  12. }

removeLast(),pollLast(): 删除尾节点

  1. public E removeLast() {
  2. final Node<E> l = last;
  3. if (l == null)
  4. throw new NoSuchElementException();
  5. return unlinkLast(l);
  6. }
  7. public E pollLast() {
  8. final Node<E> l = last;
  9. return (l == null) ? null : unlinkLast(l);
  10. }

区别: removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()方法返回null。

remove(Object o): 删除指定元素

  1. public boolean remove(Object o) {
  2. //如果删除对象为null
  3. if (o == null) {
  4. //从头开始遍历
  5. for (Node<E> x = first; x != null; x = x.next) {
  6. //找到元素
  7. if (x.item == null) {
  8. //从链表中移除找到的元素
  9. unlink(x);
  10. return true;
  11. }
  12. }
  13. } else {
  14. //从头开始遍历
  15. for (Node<E> x = first; x != null; x = x.next) {
  16. //找到元素
  17. if (o.equals(x.item)) {
  18. //从链表中移除找到的元素
  19. unlink(x);
  20. return true;
  21. }
  22. }
  23. }
  24. return false;
  25. }

当删除指定对象时,只需调用remove(Object o)即可,不过该方法一次只会删除一个匹配的对象,如果删除了匹配对象,返回true,否则false。

unlink(Node x) 方法:

  1. E unlink(Node<E> x) {
  2. // assert x != null;
  3. final E element = x.item;
  4. final Node<E> next = x.next;//得到后继节点
  5. final Node<E> prev = x.prev;//得到前驱节点
  6. //删除前驱指针
  7. if (prev == null) {
  8. first = next;//如果删除的节点是头节点,令头节点指向该节点的后继节点
  9. } else {
  10. prev.next = next;//将前驱节点的后继节点指向后继节点
  11. x.prev = null;
  12. }
  13. //删除后继指针
  14. if (next == null) {
  15. last = prev;//如果删除的节点是尾节点,令尾节点指向该节点的前驱节点
  16. } else {
  17. next.prev = prev;
  18. x.next = null;
  19. }
  20. x.item = null;
  21. size--;
  22. modCount++;
  23. return element;
  24. }

remove(int index):删除指定位置的元素

  1. public E remove(int index) {
  2. //检查index范围
  3. checkElementIndex(index);
  4. //将节点删除
  5. return unlink(node(index));
  6. }

LinkedList类常用方法测试

  1. package list;
  2. import java.util.Iterator;
  3. import java.util.LinkedList;
  4. public class LinkedListDemo {
  5. public static void main(String[] srgs) {
  6. //创建存放int类型的linkedList
  7. LinkedList<Integer> linkedList = new LinkedList<>();
  8. /************************** linkedList的基本操作 ************************/
  9. linkedList.addFirst(0); // 添加元素到列表开头
  10. linkedList.add(1); // 在列表结尾添加元素
  11. linkedList.add(2, 2); // 在指定位置添加元素
  12. linkedList.addLast(3); // 添加元素到列表结尾
  13. System.out.println("LinkedList(直接输出的): " + linkedList);
  14. System.out.println("getFirst()获得第一个元素: " + linkedList.getFirst()); // 返回此列表的第一个元素
  15. System.out.println("getLast()获得第最后一个元素: " + linkedList.getLast()); // 返回此列表的最后一个元素
  16. System.out.println("removeFirst()删除第一个元素并返回: " + linkedList.removeFirst()); // 移除并返回此列表的第一个元素
  17. System.out.println("removeLast()删除最后一个元素并返回: " + linkedList.removeLast()); // 移除并返回此列表的最后一个元素
  18. System.out.println("After remove:" + linkedList);
  19. System.out.println("contains()方法判断列表是否包含1这个元素:" + linkedList.contains(1)); // 判断此列表包含指定元素,如果是,则返回true
  20. System.out.println("该linkedList的大小 : " + linkedList.size()); // 返回此列表的元素个数
  21. /************************** 位置访问操作 ************************/
  22. System.out.println("-----------------------------------------");
  23. linkedList.set(1, 3); // 将此列表中指定位置的元素替换为指定的元素
  24. System.out.println("After set(1, 3):" + linkedList);
  25. System.out.println("get(1)获得指定位置(这里为1)的元素: " + linkedList.get(1)); // 返回此列表中指定位置处的元素
  26. /************************** Search操作 ************************/
  27. System.out.println("-----------------------------------------");
  28. linkedList.add(3);
  29. System.out.println("indexOf(3): " + linkedList.indexOf(3)); // 返回此列表中首次出现的指定元素的索引
  30. System.out.println("lastIndexOf(3): " + linkedList.lastIndexOf(3));// 返回此列表中最后出现的指定元素的索引
  31. /************************** Queue操作 ************************/
  32. System.out.println("-----------------------------------------");
  33. System.out.println("peek(): " + linkedList.peek()); // 获取但不移除此列表的头
  34. System.out.println("element(): " + linkedList.element()); // 获取但不移除此列表的头
  35. linkedList.poll(); // 获取并移除此列表的头
  36. System.out.println("After poll():" + linkedList);
  37. linkedList.remove();
  38. System.out.println("After remove():" + linkedList); // 获取并移除此列表的头
  39. linkedList.offer(4);
  40. System.out.println("After offer(4):" + linkedList); // 将指定元素添加到此列表的末尾
  41. /************************** Deque操作 ************************/
  42. System.out.println("-----------------------------------------");
  43. linkedList.offerFirst(2); // 在此列表的开头插入指定的元素
  44. System.out.println("After offerFirst(2):" + linkedList);
  45. linkedList.offerLast(5); // 在此列表末尾插入指定的元素
  46. System.out.println("After offerLast(5):" + linkedList);
  47. System.out.println("peekFirst(): " + linkedList.peekFirst()); // 获取但不移除此列表的第一个元素
  48. System.out.println("peekLast(): " + linkedList.peekLast()); // 获取但不移除此列表的第一个元素
  49. linkedList.pollFirst(); // 获取并移除此列表的第一个元素
  50. System.out.println("After pollFirst():" + linkedList);
  51. linkedList.pollLast(); // 获取并移除此列表的最后一个元素
  52. System.out.println("After pollLast():" + linkedList);
  53. linkedList.push(2); // 将元素推入此列表所表示的堆栈(插入到列表的头)
  54. System.out.println("After push(2):" + linkedList);
  55. linkedList.pop(); // 从此列表所表示的堆栈处弹出一个元素(获取并移除列表第一个元素)
  56. System.out.println("After pop():" + linkedList);
  57. linkedList.add(3);
  58. linkedList.removeFirstOccurrence(3); // 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表)
  59. System.out.println("After removeFirstOccurrence(3):" + linkedList);
  60. linkedList.removeLastOccurrence(3); // 从此列表中移除最后一次出现的指定元素(从尾部到头部遍历列表)
  61. System.out.println("After removeFirstOccurrence(3):" + linkedList);
  62. /************************** 遍历操作 ************************/
  63. System.out.println("-----------------------------------------");
  64. linkedList.clear();
  65. for (int i = 0; i < 100000; i++) {
  66. linkedList.add(i);
  67. }
  68. // 迭代器遍历
  69. long start = System.currentTimeMillis();
  70. Iterator<Integer> iterator = linkedList.iterator();
  71. while (iterator.hasNext()) {
  72. iterator.next();
  73. }
  74. long end = System.currentTimeMillis();
  75. System.out.println("Iterator:" + (end - start) + " ms");
  76. // 顺序遍历(随机遍历)
  77. start = System.currentTimeMillis();
  78. for (int i = 0; i < linkedList.size(); i++) {
  79. linkedList.get(i);
  80. }
  81. end = System.currentTimeMillis();
  82. System.out.println("for:" + (end - start) + " ms");
  83. // 另一种for循环遍历
  84. start = System.currentTimeMillis();
  85. for (Integer i : linkedList)
  86. ;
  87. end = System.currentTimeMillis();
  88. System.out.println("for2:" + (end - start) + " ms");
  89. // 通过pollFirst()或pollLast()来遍历LinkedList
  90. LinkedList<Integer> temp1 = new LinkedList<>();
  91. temp1.addAll(linkedList);
  92. start = System.currentTimeMillis();
  93. while (temp1.size() != 0) {
  94. temp1.pollFirst();
  95. }
  96. end = System.currentTimeMillis();
  97. System.out.println("pollFirst()或pollLast():" + (end - start) + " ms");
  98. // 通过removeFirst()或removeLast()来遍历LinkedList
  99. LinkedList<Integer> temp2 = new LinkedList<>();
  100. temp2.addAll(linkedList);
  101. start = System.currentTimeMillis();
  102. while (temp2.size() != 0) {
  103. temp2.removeFirst();
  104. }
  105. end = System.currentTimeMillis();
  106. System.out.println("removeFirst()或removeLast():" + (end - start) + " ms");
  107. }
  108. }

Vector

ArrayList与Vector的区别

  • Vector是线程安全的, 也就是线程同步的, 而ArrayList是线程序不安全的. 对于Vector&ArrayList, Hashtable&HashMap, 要记住线程安全的问题, 记住Vector与Hashtable是旧的, 是java一诞生就提供了的, 它们是线程安全的, ArrayList与HashMap是java2时才提供的, 它们是线程不安全的.
  • ArrayList与Vector都有一个初始的容量大小, 当存储进它们里面的元素的个数超过了容量时, 就需要增加ArrayList与Vector的存储空间, Vector默认增长为原来两倍,而ArrayList的增长策略在文档中没有明确规定(从源代码看到的是增长为原来的1.5倍).ArrayList与Vector都可以设置初始的空间大小, Vector还可以设置增长的空间大小, 而ArrayList没有提供设置增长空间的方法.

总结:即Vector增长原来的一倍,ArrayList增加原来的0.5倍. Vector 线程安全, ArrayList 不是.
**

同步容器(如Vector)的所有操作一定是线程安全的吗?

Vector这样的同步容器的所有公有方法全都是synchronized的,也就是说,我们可以在多线程场景中放心的使用单独这些方法,因为这些方法本身的确是线程安全的。
但是,请注意上面这句话中,有一个比较关键的词:单独
因为,虽然同步容器的所有方法都加了锁,但是对这些容器的复合操作无法保证其线程安全性。需要客户端通过主动加锁来保证。
简单举一个例子,我们定义如下删除Vector中最后一个元素方法:

  1. public Object deleteLast(Vector v){
  2. int lastIndex = v.size()-1;
  3. v.remove(lastIndex);
  4. }
  5. 复制代码

上面这个方法是一个复合方法,包括size()和remove(),乍一看上去好像并没有什么问题,无论是size()方法还是remove()方法都是线程安全的,那么整个deleteLast方法应该也是线程安全的。
但是时,如果多线程调用该方法的过程中,remove方法有可能抛出ArrayIndexOutOfBoundsException。

  1. Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 879
  2. at java.util.Vector.remove(Vector.java:834)
  3. at com.hollis.Test.deleteLast(EncodeTest.java:40)
  4. at com.hollis.Test$2.run(EncodeTest.java:28)
  5. at java.lang.Thread.run(Thread.java:748)
  6. 复制代码

我们上面贴了remove的源码,我们可以分析得出:当index >= elementCount时,会抛出ArrayIndexOutOfBoundsException ,也就是说,当当前索引值不再有效的时候,将会抛出这个异常。
因为removeLast方法,有可能被多个线程同时执行,当线程2通过index()获得索引值为10,在尝试通过remove()删除该索引位置的元素之前,线程1把该索引位置的值删除掉了,这时线程一在执行时便会抛出异常。
Java 集合 - 图3
为了避免出现类似问题,可以尝试加锁:

  1. public void deleteLast() {
  2. synchronized (v) {
  3. int index = v.size() - 1;
  4. v.remove(index);
  5. }
  6. }
  7. 复制代码

如上,我们在deleteLast中,对v进行加锁,即可保证同一时刻,不会有其他线程删除掉v中的元素。
另外,如果以下代码会被多线程执行时,也要特别注意:

  1. for (int i = 0; i < v.size(); i++) {
  2. v.remove(i);
  3. }
  4. 复制代码

由于,不同线程在同一时间操作同一个Vector,其中包括删除操作,那么就同样有可能发生线程安全问题。所以,在使用同步容器的时候,如果涉及到多个线程同时执行删除操作,就要考虑下是否需要加锁。

同步容器的问题

前面说过了,同步容器直接保证耽搁操作的线程安全性,但是无法保证复合操作的线程安全,遇到这种情况时,必须要通过主动加锁的方式来实现。
而且,除此之外,同步容易由于对其所有方法都加了锁,这就导致多个线程访问同一个容器的时候,只能进行顺序访问,即使是不同的操作,也要排队,如get和add要排队执行。这就大大的降低了容器的并发能力。


HashMap

HashMap 简介

HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间,具体可以参考 treeifyBin方法。

底层数据结构分析

JDK1.8之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

JDK 1.8 HashMap 的 hash 方法源码:

JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。

  1. static final int hash(Object key) {
  2. int h;
  3. // key.hashCode():返回散列值也就是hashcode
  4. // ^ :按位异或
  5. // >>>:无符号右移,忽略符号位,空位都以0补齐
  6. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  7. }

对比一下 JDK1.7的 HashMap 的 hash 方法源码.

  1. static int hash(int h) {
  2. // This function ensures that hashCodes that differ only by
  3. // constant multiples at each bit position have a bounded
  4. // number of collisions (approximately 8 at default load factor).
  5. h ^= (h >>> 20) ^ (h >>> 12);
  6. return h ^ (h >>> 7) ^ (h >>> 4);
  7. }

相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

Java 集合 - 图4

JDK1.8之后

相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

Java 集合 - 图5

类的属性:

  1. public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
  2. // 序列号
  3. private static final long serialVersionUID = 362498820763181265L;
  4. // 默认的初始容量是16
  5. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
  6. // 最大容量
  7. static final int MAXIMUM_CAPACITY = 1 << 30;
  8. // 默认的填充因子
  9. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  10. // 当桶(bucket)上的结点数大于这个值时会转成红黑树
  11. static final int TREEIFY_THRESHOLD = 8;
  12. // 当桶(bucket)上的结点数小于这个值时树转链表
  13. static final int UNTREEIFY_THRESHOLD = 6;
  14. // 桶中结构转化为红黑树对应的table的最小大小
  15. static final int MIN_TREEIFY_CAPACITY = 64;
  16. // 存储元素的数组,总是2的幂次倍
  17. transient Node<k,v>[] table;
  18. // 存放具体元素的集
  19. transient Set<map.entry<k,v>> entrySet;
  20. // 存放元素的个数,注意这个不等于数组的长度。
  21. transient int size;
  22. // 每次扩容和更改map结构的计数器
  23. transient int modCount;
  24. // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
  25. int threshold;
  26. // 加载因子
  27. final float loadFactor;
  28. }
  • loadFactor加载因子
    loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。
    loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值
    给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
  • threshold
    threshold = capacity loadFactor当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准**。

Node节点类源码:

  1. // 继承自 Map.Entry<K,V>
  2. static class Node<K,V> implements Map.Entry<K,V> {
  3. final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
  4. final K key;//键
  5. V value;//值
  6. // 指向下一个节点
  7. Node<K,V> next;
  8. Node(int hash, K key, V value, Node<K,V> next) {
  9. this.hash = hash;
  10. this.key = key;
  11. this.value = value;
  12. this.next = next;
  13. }
  14. public final K getKey() { return key; }
  15. public final V getValue() { return value; }
  16. public final String toString() { return key + "=" + value; }
  17. // 重写hashCode()方法
  18. public final int hashCode() {
  19. return Objects.hashCode(key) ^ Objects.hashCode(value);
  20. }
  21. public final V setValue(V newValue) {
  22. V oldValue = value;
  23. value = newValue;
  24. return oldValue;
  25. }
  26. // 重写 equals() 方法
  27. public final boolean equals(Object o) {
  28. if (o == this)
  29. return true;
  30. if (o instanceof Map.Entry) {
  31. Map.Entry<?,?> e = (Map.Entry<?,?>)o;
  32. if (Objects.equals(key, e.getKey()) &&
  33. Objects.equals(value, e.getValue()))
  34. return true;
  35. }
  36. return false;
  37. }
  38. }

树节点类源码:

  1. static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
  2. TreeNode<K,V> parent; // 父
  3. TreeNode<K,V> left; // 左
  4. TreeNode<K,V> right; // 右
  5. TreeNode<K,V> prev; // needed to unlink next upon deletion
  6. boolean red; // 判断颜色
  7. TreeNode(int hash, K key, V val, Node<K,V> next) {
  8. super(hash, key, val, next);
  9. }
  10. // 返回根节点
  11. final TreeNode<K,V> root() {
  12. for (TreeNode<K,V> r = this, p;;) {
  13. if ((p = r.parent) == null)
  14. return r;
  15. r = p;
  16. }

HashMap源码分析

构造方法

HashMap 中有四个构造方法,它们分别如下:

  1. // 默认构造函数。
  2. public HashMap() {
  3. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  4. }
  5. // 包含另一个“Map”的构造函数
  6. public HashMap(Map<? extends K, ? extends V> m) {
  7. this.loadFactor = DEFAULT_LOAD_FACTOR;
  8. putMapEntries(m, false);//下面会分析到这个方法
  9. }
  10. // 指定“容量大小”的构造函数
  11. public HashMap(int initialCapacity) {
  12. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  13. }
  14. // 指定“容量大小”和“加载因子”的构造函数
  15. public HashMap(int initialCapacity, float loadFactor) {
  16. if (initialCapacity < 0)
  17. throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
  18. if (initialCapacity > MAXIMUM_CAPACITY)
  19. initialCapacity = MAXIMUM_CAPACITY;
  20. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  21. throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
  22. this.loadFactor = loadFactor;
  23. this.threshold = tableSizeFor(initialCapacity);
  24. }

putMapEntries方法:

  1. final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
  2. int s = m.size();
  3. if (s > 0) {
  4. // 判断table是否已经初始化
  5. if (table == null) { // pre-size
  6. // 未初始化,s为m的实际元素个数
  7. float ft = ((float)s / loadFactor) + 1.0F;
  8. int t = ((ft < (float)MAXIMUM_CAPACITY) ?
  9. (int)ft : MAXIMUM_CAPACITY);
  10. // 计算得到的t大于阈值,则初始化阈值
  11. if (t > threshold)
  12. threshold = tableSizeFor(t);
  13. }
  14. // 已初始化,并且m元素个数大于阈值,进行扩容处理
  15. else if (s > threshold)
  16. resize();
  17. // 将m中的所有元素添加至HashMap中
  18. for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
  19. K key = e.getKey();
  20. V value = e.getValue();
  21. putVal(hash(key), key, value, false, evict);
  22. }
  23. }
  24. }

put方法

HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。

对putVal方法添加元素的分析如下:

  • ①如果定位到的数组位置没有元素 就直接插入。
  • ②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。

ps:下图有一个小问题,来自 issue#608指出:直接覆盖之后应该就会 return,不会有后续操作。参考 JDK8 HashMap.java 658 行。

Java 集合 - 图6

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  5. boolean evict) {
  6. Node<K,V>[] tab; Node<K,V> p; int n, i;
  7. // table未初始化或者长度为0,进行扩容
  8. if ((tab = table) == null || (n = tab.length) == 0)
  9. n = (tab = resize()).length;
  10. // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
  11. if ((p = tab[i = (n - 1) & hash]) == null)
  12. tab[i] = newNode(hash, key, value, null);
  13. // 桶中已经存在元素
  14. else {
  15. Node<K,V> e; K k;
  16. // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
  17. if (p.hash == hash &&
  18. ((k = p.key) == key || (key != null && key.equals(k))))
  19. // 将第一个元素赋值给e,用e来记录
  20. e = p;
  21. // hash值不相等,即key不相等;为红黑树结点
  22. else if (p instanceof TreeNode)
  23. // 放入树中
  24. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  25. // 为链表结点
  26. else {
  27. // 在链表最末插入结点
  28. for (int binCount = 0; ; ++binCount) {
  29. // 到达链表的尾部
  30. if ((e = p.next) == null) {
  31. // 在尾部插入新结点
  32. p.next = newNode(hash, key, value, null);
  33. // 结点数量达到阈值,转化为红黑树
  34. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  35. treeifyBin(tab, hash);
  36. // 跳出循环
  37. break;
  38. }
  39. // 判断链表中结点的key值与插入的元素的key值是否相等
  40. if (e.hash == hash &&
  41. ((k = e.key) == key || (key != null && key.equals(k))))
  42. // 相等,跳出循环
  43. break;
  44. // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
  45. p = e;
  46. }
  47. }
  48. // 表示在桶中找到key值、hash值与插入元素相等的结点
  49. if (e != null) {
  50. // 记录e的value
  51. V oldValue = e.value;
  52. // onlyIfAbsent为false或者旧值为null
  53. if (!onlyIfAbsent || oldValue == null)
  54. //用新值替换旧值
  55. e.value = value;
  56. // 访问后回调
  57. afterNodeAccess(e);
  58. // 返回旧值
  59. return oldValue;
  60. }
  61. }
  62. // 结构性修改
  63. ++modCount;
  64. // 实际大小大于阈值则扩容
  65. if (++size > threshold)
  66. resize();
  67. // 插入后回调
  68. afterNodeInsertion(evict);
  69. return null;
  70. }

我们再来对比一下 JDK1.7 put方法的代码

对于put方法的分析如下:

  • ①如果定位到的数组位置没有元素 就直接插入。
  • ②如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素。
  1. public V put(K key, V value)
  2. if (table == EMPTY_TABLE) {
  3. inflateTable(threshold);
  4. }
  5. if (key == null)
  6. return putForNullKey(value);
  7. int hash = hash(key);
  8. int i = indexFor(hash, table.length);
  9. for (Entry<K,V> e = table[i]; e != null; e = e.next) { // 先遍历
  10. Object k;
  11. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  12. V oldValue = e.value;
  13. e.value = value;
  14. e.recordAccess(this);
  15. return oldValue;
  16. }
  17. }
  18. modCount++;
  19. addEntry(hash, key, value, i); // 再插入
  20. return null;
  21. }

get方法

  1. public V get(Object key) {
  2. Node<K,V> e;
  3. return (e = getNode(hash(key), key)) == null ? null : e.value;
  4. }
  5. final Node<K,V> getNode(int hash, Object key) {
  6. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  7. if ((tab = table) != null && (n = tab.length) > 0 &&
  8. (first = tab[(n - 1) & hash]) != null) {
  9. // 数组元素相等
  10. if (first.hash == hash && // always check first node
  11. ((k = first.key) == key || (key != null && key.equals(k))))
  12. return first;
  13. // 桶中不止一个节点
  14. if ((e = first.next) != null) {
  15. // 在树中get
  16. if (first instanceof TreeNode)
  17. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  18. // 在链表中get
  19. do {
  20. if (e.hash == hash &&
  21. ((k = e.key) == key || (key != null && key.equals(k))))
  22. return e;
  23. } while ((e = e.next) != null);
  24. }
  25. }
  26. return null;
  27. }

resize方法

进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。

  1. final Node<K,V>[] resize() {
  2. Node<K,V>[] oldTab = table;
  3. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  4. int oldThr = threshold;
  5. int newCap, newThr = 0;
  6. if (oldCap > 0) {
  7. // 超过最大值就不再扩充了,就只好随你碰撞去吧
  8. if (oldCap >= MAXIMUM_CAPACITY) {
  9. threshold = Integer.MAX_VALUE;
  10. return oldTab;
  11. }
  12. // 没超过最大值,就扩充为原来的2倍
  13. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
  14. newThr = oldThr << 1; // double threshold
  15. }
  16. else if (oldThr > 0) // initial capacity was placed in threshold
  17. newCap = oldThr;
  18. else {
  19. // signifies using defaults
  20. newCap = DEFAULT_INITIAL_CAPACITY;
  21. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  22. }
  23. // 计算新的resize上限
  24. if (newThr == 0) {
  25. float ft = (float)newCap * loadFactor;
  26. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
  27. }
  28. threshold = newThr;
  29. @SuppressWarnings({"rawtypes","unchecked"})
  30. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  31. table = newTab;
  32. if (oldTab != null) {
  33. // 把每个bucket都移动到新的buckets中
  34. for (int j = 0; j < oldCap; ++j) {
  35. Node<K,V> e;
  36. if ((e = oldTab[j]) != null) {
  37. oldTab[j] = null;
  38. if (e.next == null)
  39. newTab[e.hash & (newCap - 1)] = e;
  40. else if (e instanceof TreeNode)
  41. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  42. else {
  43. Node<K,V> loHead = null, loTail = null;
  44. Node<K,V> hiHead = null, hiTail = null;
  45. Node<K,V> next;
  46. do {
  47. next = e.next;
  48. // 原索引
  49. if ((e.hash & oldCap) == 0) {
  50. if (loTail == null)
  51. loHead = e;
  52. else
  53. loTail.next = e;
  54. loTail = e;
  55. }
  56. // 原索引+oldCap
  57. else {
  58. if (hiTail == null)
  59. hiHead = e;
  60. else
  61. hiTail.next = e;
  62. hiTail = e;
  63. }
  64. } while ((e = next) != null);
  65. // 原索引放到bucket里
  66. if (loTail != null) {
  67. loTail.next = null;
  68. newTab[j] = loHead;
  69. }
  70. // 原索引+oldCap放到bucket里
  71. if (hiTail != null) {
  72. hiTail.next = null;
  73. newTab[j + oldCap] = hiHead;
  74. }
  75. }
  76. }
  77. }
  78. }
  79. return newTab;
  80. }

HashMap常用方法测试

  1. package map;
  2. import java.util.Collection;
  3. import java.util.HashMap;
  4. import java.util.Set;
  5. public class HashMapDemo {
  6. public static void main(String[] args) {
  7. HashMap<String, String> map = new HashMap<String, String>();
  8. // 键不能重复,值可以重复
  9. map.put("san", "张三");
  10. map.put("si", "李四");
  11. map.put("wu", "王五");
  12. map.put("wang", "老王");
  13. map.put("wang", "老王2");// 老王被覆盖
  14. map.put("lao", "老王");
  15. System.out.println("-------直接输出hashmap:-------");
  16. System.out.println(map);
  17. /**
  18. * 遍历HashMap
  19. */
  20. // 1.获取Map中的所有键
  21. System.out.println("-------foreach获取Map中所有的键:------");
  22. Set<String> keys = map.keySet();
  23. for (String key : keys) {
  24. System.out.print(key+" ");
  25. }
  26. System.out.println();//换行
  27. // 2.获取Map中所有值
  28. System.out.println("-------foreach获取Map中所有的值:------");
  29. Collection<String> values = map.values();
  30. for (String value : values) {
  31. System.out.print(value+" ");
  32. }
  33. System.out.println();//换行
  34. // 3.得到key的值的同时得到key所对应的值
  35. System.out.println("-------得到key的值的同时得到key所对应的值:-------");
  36. Set<String> keys2 = map.keySet();
  37. for (String key : keys2) {
  38. System.out.print(key + ":" + map.get(key)+" ");
  39. }
  40. /**
  41. * 另外一种不常用的遍历方式
  42. */
  43. // 当我调用put(key,value)方法的时候,首先会把key和value封装到
  44. // Entry这个静态内部类对象中,把Entry对象再添加到数组中,所以我们想获取
  45. // map中的所有键值对,我们只要获取数组中的所有Entry对象,接下来
  46. // 调用Entry对象中的getKey()和getValue()方法就能获取键值对了
  47. Set<java.util.Map.Entry<String, String>> entrys = map.entrySet();
  48. for (java.util.Map.Entry<String, String> entry : entrys) {
  49. System.out.println(entry.getKey() + "--" + entry.getValue());
  50. }
  51. /**
  52. * HashMap其他常用方法
  53. */
  54. System.out.println("after map.size():"+map.size());
  55. System.out.println("after map.isEmpty():"+map.isEmpty());
  56. System.out.println(map.remove("san"));
  57. System.out.println("after map.remove():"+map);
  58. System.out.println("after map.get(si):"+map.get("si"));
  59. System.out.println("after map.containsKey(si):"+map.containsKey("si"));
  60. System.out.println("after containsValue(李四):"+map.containsValue("李四"));
  61. System.out.println(map.replace("si", "李四2"));
  62. System.out.println("after map.replace(si, 李四2):"+map);
  63. }
  64. }

并发情况下HASHMAP的死循环

正常的ReHash的过程

画了个图做了个演示。

  • 我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。
  • 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。
  • 接下来的三个步骤是Hash表 resize成4,然后所有的 重新rehash的过程

Java 集合 - 图7

并发下的Rehash

1)假设我们有两个线程。我用红色和浅蓝色标注了一下。
我们再回头看一下我们的 transfer代码中的这个细节:

do {
``Entry<K,V> next = e.next; ``// <--假设线程一执行到这里就被调度挂起了
``int i = indexFor(e.hash, newCapacity);
``e.next = newTable[i];
``newTable[i] = e;
``e = next;
} ``while (e != ``null``);

而我们的线程二执行完成了。于是我们有下面的这个样子。
Java 集合 - 图8
注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。
2)线程一被调度回来执行。

  • 先是执行 newTalbe[i] = e;
  • 然后是e = next,导致了e指向了key(7),
  • 而下一次循环的next = e.next导致了next指向了key(3)

Java 集合 - 图9
3)一切安好。
线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移
Java 集合 - 图10
4)环形链接出现。
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。
Java 集合 - 图11
于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop。


LinkedHashMap

1. 概述
LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此之外,LinkedHashMap 对访问顺序也提供了相关支持。在一些场景下,该特性很有用,比如缓存。在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。所以,要看懂 LinkedHashMap 的源码,需要先看懂 HashMap 的源码。关于 HashMap 的源码分析,本文并不打算展开讲了。大家可以参考我之前的一篇文章“HashMap 源码详细分析(JDK1.8)”。在那篇文章中,我配了十多张图帮助大家学习 HashMap 源码。
本篇文章的结构与我之前两篇关于 Java 集合类(集合框架)的源码分析文章不同,本文将不再分析集合类的基本操作(查找、遍历、插入、删除),而是把重点放在双向链表的维护上。包括链表的建立过程,删除节点的过程,以及访问顺序维护的过程等。好了,接下里开始分析吧。
2. 原理
上一章说了 LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构。该结构由数组和链表或红黑树组成,结构示意图大致如下:
Java 集合 - 图12
LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。其结构可能如下图:
Java 集合 - 图13
上图中,淡蓝色的箭头表示前驱引用,红色箭头表示后继引用。每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了。
上面的结构并不是很难理解,虽然引入了红黑树,导致结构看起来略为复杂了一些。但大家完全可以忽略红黑树,而只关注链表结构本身。好了,接下来进入细节分析吧。
3. 源码分析

3.1 Entry 的继承体系

在对核心内容展开分析之前,这里先插队分析一下键值对节点的继承体系。先来看看继承体系结构图:
Java 集合 - 图14
上面的继承体系乍一看还是有点复杂的,同时也有点让人迷惑。HashMap 的内部类 TreeNode 不继承它的了一个内部类 Node,却继承自 Node 的子类 LinkedHashMap 内部类 Entry。这里这样做是有一定原因的,这里先不说。先来简单说明一下上面的继承体系。LinkedHashMap 内部类 Entry 继承自 HashMap 内部类 Node,并新增了两个引用,分别是 before 和 after。这两个引用的用途不难理解,也就是用于维护双向链表。同时,TreeNode 继承 LinkedHashMap 的内部类 Entry 后,就具备了和其他 Entry 一起组成链表的能力。但是这里需要大家考虑一个问题。当我们使用 HashMap 时,TreeNode 并不需要具备组成链表能力。如果继承 LinkedHashMap 内部类 Entry ,TreeNode 就多了两个用不到的引用,这样做不是会浪费空间吗?简单说明一下这个问题(水平有限,不保证完全正确),这里这么做确实会浪费空间,但与 TreeNode 通过继承获取的组成链表的能力相比,这点浪费是值得的。在 HashMap 的设计思路注释中,有这样一段话:

Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). 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.

大致的意思是 TreeNode 对象的大小约是普通 Node 对象的2倍,我们仅在桶(bin)中包含足够多的节点时再使用。当桶中的节点数量变少时(取决于删除和扩容),TreeNode 会被转成 Node。当用户实现的 hashCode 方法具有良好分布性时,树类型的桶将会很少被使用。
通过上面的注释,我们可以了解到。一般情况下,只要 hashCode 的实现不糟糕,Node 组成的链表很少会被转成由 TreeNode 组成的红黑树。也就是说 TreeNode 使用的并不多,浪费那点空间是可接受的。假如 TreeNode 机制继承自 Node 类,那么它要想具备组成链表的能力,就需要 Node 去继承 LinkedHashMap 的内部类 Entry。这个时候就得不偿失了,浪费很多空间去获取不一定用得到的能力。
说到这里,大家应该能明白节点类型的继承体系了。这里单独拿出来说一下,为下面的分析做铺垫。叙述略为啰嗦,见谅。

3.1 链表的建立过程

链表的建立过程是在插入键值对节点时开始的,初始情况下,让 LinkedHashMap 的 head 和 tail 引用同时指向新节点,链表就算建立起来了。随后不断有新节点插入,通过将新节点接在 tail 引用指向节点的后面,即可实现链表的更新。
Map 类型的集合类是通过 put(K,V) 方法插入键值对,LinkedHashMap 本身并没有覆写父类的 put 方法,而是直接使用了父类的实现。但在 HashMap 中,put 方法插入的是 HashMap 内部类 Node 类型的节点,该类型的节点并不具备与 LinkedHashMap 内部类 Entry 及其子类型节点组成链表的能力。那么,LinkedHashMap 是怎样建立链表的呢?在展开说明之前,我们先看一下 LinkedHashMap 插入操作相关的代码:

  1. // HashMap 中实现
  2. public V put(K key, V value) {
  3. return putVal(hash(key), key, value, false, true);
  4. }
  5. // HashMap 中实现
  6. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  7. boolean evict) {
  8. Node<K,V>[] tab; Node<K,V> p; int n, i;
  9. if ((tab = table) == null || (n = tab.length) == 0) {...}
  10. // 通过节点 hash 定位节点所在的桶位置,并检测桶中是否包含节点引用
  11. if ((p = tab[i = (n - 1) & hash]) == null) {...}
  12. else {
  13. Node<K,V> e; K k;
  14. if (p.hash == hash &&
  15. ((k = p.key) == key || (key != null && key.equals(k))))
  16. e = p;
  17. else if (p instanceof TreeNode) {...}
  18. else {
  19. // 遍历链表,并统计链表长度
  20. for (int binCount = 0; ; ++binCount) {
  21. // 未在单链表中找到要插入的节点,将新节点接在单链表的后面
  22. if ((e = p.next) == null) {
  23. p.next = newNode(hash, key, value, null);
  24. if (binCount >= TREEIFY_THRESHOLD - 1) {...}
  25. break;
  26. }
  27. // 插入的节点已经存在于单链表中
  28. if (e.hash == hash &&
  29. ((k = e.key) == key || (key != null && key.equals(k))))
  30. break;
  31. p = e;
  32. }
  33. }
  34. if (e != null) { // existing mapping for key
  35. V oldValue = e.value;
  36. if (!onlyIfAbsent || oldValue == null) {...}
  37. afterNodeAccess(e); // 回调方法,后续说明
  38. return oldValue;
  39. }
  40. }
  41. ++modCount;
  42. if (++size > threshold) {...}
  43. afterNodeInsertion(evict); // 回调方法,后续说明
  44. return null;
  45. }
  46. // HashMap 中实现
  47. Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
  48. return new Node<>(hash, key, value, next);
  49. }
  50. // LinkedHashMap 中覆写
  51. Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
  52. LinkedHashMap.Entry<K,V> p =
  53. new LinkedHashMap.Entry<K,V>(hash, key, value, e);
  54. // 将 Entry 接在双向链表的尾部
  55. linkNodeLast(p);
  56. return p;
  57. }
  58. // LinkedHashMap 中实现
  59. private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
  60. LinkedHashMap.Entry<K,V> last = tail;
  61. tail = p;
  62. // last 为 null,表明链表还未建立
  63. if (last == null)
  64. head = p;
  65. else {
  66. // 将新节点 p 接在链表尾部
  67. p.before = last;
  68. last.after = p;
  69. }
  70. }

上面就是 LinkedHashMap 插入相关的源码,这里省略了部分非关键的代码。我根据上面的代码,可以知道 LinkedHashMap 插入操作的调用过程。如下:
Java 集合 - 图15
我把 newNode 方法红色背景标注了出来,这一步比较关键。LinkedHashMap 覆写了该方法。在这个方法中,LinkedHashMap 创建了 Entry,并通过 linkNodeLast 方法将 Entry 接在双向链表的尾部,实现了双向链表的建立。双向链表建立之后,我们就可以按照插入顺序去遍历 LinkedHashMap,大家可以自己写点测试代码验证一下插入顺序。
以上就是 LinkedHashMap 维护插入顺序的相关分析。本节的最后,再额外补充一些东西。大家如果仔细看上面的代码的话,会发现有两个以after开头方法,在上文中没有被提及。在 JDK 1.8 HashMap 的源码中,相关的方法有3个:

  1. // Callbacks to allow LinkedHashMap post-actions
  2. void afterNodeAccess(Node<K,V> p) { }
  3. void afterNodeInsertion(boolean evict) { }
  4. void afterNodeRemoval(Node<K,V> p) { }

根据这三个方法的注释可以看出,这些方法的用途是在增删查等操作后,通过回调的方式,让 LinkedHashMap 有机会做一些后置操作。上述三个方法的具体实现在 LinkedHashMap 中,本节先不分析这些实现,相关分析会在后续章节中进行。

3.2 链表节点的删除过程

与插入操作一样,LinkedHashMap 删除操作相关的代码也是直接用父类的实现。在删除节点时,父类的删除逻辑并不会修复 LinkedHashMap 所维护的双向链表,这不是它的职责。那么删除及节点后,被删除的节点该如何从双链表中移除呢?当然,办法还算是有的。上一节最后提到 HashMap 中三个回调方法运行 LinkedHashMap 对一些操作做出响应。所以,在删除及节点后,回调方法 afterNodeRemoval 会被调用。LinkedHashMap 覆写该方法,并在该方法中完成了移除被删除节点的操作。相关源码如下:

  1. // HashMap 中实现
  2. public V remove(Object key) {
  3. Node<K,V> e;
  4. return (e = removeNode(hash(key), key, null, false, true)) == null ?
  5. null : e.value;
  6. }
  7. // HashMap 中实现
  8. final Node<K,V> removeNode(int hash, Object key, Object value,
  9. boolean matchValue, boolean movable) {
  10. Node<K,V>[] tab; Node<K,V> p; int n, index;
  11. if ((tab = table) != null && (n = tab.length) > 0 &&
  12. (p = tab[index = (n - 1) & hash]) != null) {
  13. Node<K,V> node = null, e; K k; V v;
  14. if (p.hash == hash &&
  15. ((k = p.key) == key || (key != null && key.equals(k))))
  16. node = p;
  17. else if ((e = p.next) != null) {
  18. if (p instanceof TreeNode) {...}
  19. else {
  20. // 遍历单链表,寻找要删除的节点,并赋值给 node 变量
  21. do {
  22. if (e.hash == hash &&
  23. ((k = e.key) == key ||
  24. (key != null && key.equals(k)))) {
  25. node = e;
  26. break;
  27. }
  28. p = e;
  29. } while ((e = e.next) != null);
  30. }
  31. }
  32. if (node != null && (!matchValue || (v = node.value) == value ||
  33. (value != null && value.equals(v)))) {
  34. if (node instanceof TreeNode) {...}
  35. // 将要删除的节点从单链表中移除
  36. else if (node == p)
  37. tab[index] = node.next;
  38. else
  39. p.next = node.next;
  40. ++modCount;
  41. --size;
  42. afterNodeRemoval(node); // 调用删除回调方法进行后续操作
  43. return node;
  44. }
  45. }
  46. return null;
  47. }
  48. // LinkedHashMap 中覆写
  49. void afterNodeRemoval(Node<K,V> e) { // unlink
  50. LinkedHashMap.Entry<K,V> p =
  51. (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
  52. // 将 p 节点的前驱后后继引用置空
  53. p.before = p.after = null;
  54. // b 为 null,表明 p 是头节点
  55. if (b == null)
  56. head = a;
  57. else
  58. b.after = a;
  59. // a 为 null,表明 p 是尾节点
  60. if (a == null)
  61. tail = b;
  62. else
  63. a.before = b;
  64. }

删除的过程并不复杂,上面这么多代码其实就做了三件事:

  1. 根据 hash 定位到桶位置
  2. 遍历链表或调用红黑树相关的删除方法
  3. 从 LinkedHashMap 维护的双链表中移除要删除的节点

举个例子说明一下,假如我们要删除下图键值为 3 的节点。
Java 集合 - 图16
根据 hash 定位到该节点属于3号桶,然后在对3号桶保存的单链表进行遍历。找到要删除的节点后,先从单链表中移除该节点。如下:
Java 集合 - 图17
然后再双向链表中移除该节点:
Java 集合 - 图18
删除及相关修复过程并不复杂,结合上面的图片,大家应该很容易就能理解,这里就不多说了。

3.3 访问顺序的维护过程

前面说了插入顺序的实现,本节来讲讲访问顺序。默认情况下,LinkedHashMap 是按插入顺序维护链表。不过我们可以在初始化 LinkedHashMap,指定 accessOrder 参数为 true,即可让它按访问顺序维护链表。访问顺序的原理上并不复杂,当我们调用get/getOrDefault/replace等方法时,只需要将这些方法访问的节点移动到链表的尾部即可。相应的源码如下:

  1. // LinkedHashMap 中覆写
  2. public V get(Object key) {
  3. Node<K,V> e;
  4. if ((e = getNode(hash(key), key)) == null)
  5. return null;
  6. // 如果 accessOrder 为 true,则调用 afterNodeAccess 将被访问节点移动到链表最后
  7. if (accessOrder)
  8. afterNodeAccess(e);
  9. return e.value;
  10. }
  11. // LinkedHashMap 中覆写
  12. void afterNodeAccess(Node<K,V> e) { // move node to last
  13. LinkedHashMap.Entry<K,V> last;
  14. if (accessOrder && (last = tail) != e) {
  15. LinkedHashMap.Entry<K,V> p =
  16. (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
  17. p.after = null;
  18. // 如果 b 为 null,表明 p 为头节点
  19. if (b == null)
  20. head = a;
  21. else
  22. b.after = a;
  23. if (a != null)
  24. a.before = b;
  25. /*
  26. * 这里存疑,父条件分支已经确保节点 e 不会是尾节点,
  27. * 那么 e.after 必然不会为 null,不知道 else 分支有什么作用
  28. */
  29. else
  30. last = b;
  31. if (last == null)
  32. head = p;
  33. else {
  34. // 将 p 接在链表的最后
  35. p.before = last;
  36. last.after = p;
  37. }
  38. tail = p;
  39. ++modCount;
  40. }
  41. }

上面就是访问顺序的实现代码,并不复杂。下面举例演示一下,帮助大家理解。假设我们访问下图键值为3的节点,访问前结构为:
Java 集合 - 图19
访问后,键值为3的节点将会被移动到双向链表的最后位置,其前驱和后继也会跟着更新。访问后的结构如下:
Java 集合 - 图20

3.4 基于 LinkedHashMap 实现缓存

前面介绍了 LinkedHashMap 是如何维护插入和访问顺序的,大家对 LinkedHashMap 的原理应该有了一定的认识。本节我们来写一些代码实践一下,这里通过继承 LinkedHashMap 实现了一个简单的 LRU 策略的缓存。在写代码之前,先介绍一下前置知识。
在3.1节分析链表建立过程时,我故意忽略了部分源码分析。本节就把忽略的部分补上,先看源码吧:

  1. void afterNodeInsertion(boolean evict) { // possibly remove eldest
  2. LinkedHashMap.Entry<K,V> first;
  3. // 根据条件判断是否移除最近最少被访问的节点
  4. if (evict && (first = head) != null && removeEldestEntry(first)) {
  5. K key = first.key;
  6. removeNode(hash(key), key, null, false, true);
  7. }
  8. }
  9. // 移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存
  10. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  11. return false;
  12. }

上面的源码的核心逻辑在一般情况下都不会被执行,所以之前并没有进行分析。上面的代码做的事情比较简单,就是通过一些条件,判断是否移除最近最少被访问的节点。看到这里,大家应该知道上面两个方法的用途了。当我们基于 LinkedHashMap 实现缓存时,通过覆写removeEldestEntry方法可以实现自定义策略的 LRU 缓存。比如我们可以根据节点数量判断是否移除最近最少被访问的节点,或者根据节点的存活时间判断是否移除该节点等。本节所实现的缓存是基于判断节点数量是否超限的策略。在构造缓存对象时,传入最大节点数。当插入的节点数超过最大节点数时,移除最近最少被访问的节点。实现代码如下:

  1. public class SimpleCache<K, V> extends LinkedHashMap<K, V> {
  2. private static final int MAX_NODE_NUM = 100;
  3. private int limit;
  4. public SimpleCache() {
  5. this(MAX_NODE_NUM);
  6. }
  7. public SimpleCache(int limit) {
  8. super(limit, 0.75f, true);
  9. this.limit = limit;
  10. }
  11. public V save(K key, V val) {
  12. return put(key, val);
  13. }
  14. public V getOne(K key) {
  15. return get(key);
  16. }
  17. public boolean exists(K key) {
  18. return containsKey(key);
  19. }
  20. /**
  21. * 判断节点数是否超限
  22. * @param eldest
  23. * @return 超限返回 true,否则返回 false
  24. */
  25. @Override
  26. protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
  27. return size() > limit;
  28. }
  29. }

测试代码如下:

  1. public class SimpleCacheTest {
  2. @Test
  3. public void test() throws Exception {
  4. SimpleCache<Integer, Integer> cache = new SimpleCache<>(3);
  5. for (int i = 0; i < 10; i++) {
  6. cache.save(i, i * i);
  7. }
  8. System.out.println("插入10个键值对后,缓存内容:");
  9. System.out.println(cache + "\n");
  10. System.out.println("访问键值为7的节点后,缓存内容:");
  11. cache.getOne(7);
  12. System.out.println(cache + "\n");
  13. System.out.println("插入键值为1的键值对后,缓存内容:");
  14. cache.save(1, 1);
  15. System.out.println(cache);
  16. }
  17. }

测试结果如下:
Java 集合 - 图21
在测试代码中,设定缓存大小为3。在向缓存中插入10个键值对后,只有最后3个被保存下来了,其他的都被移除了。然后通过访问键值为7的节点,使得该节点被移到双向链表的最后位置。当我们再次插入一个键值对时,键值为7的节点就不会被移除。
本节作为对前面内的补充,简单介绍了 LinkedHashMap 在其他方面的应用。本节内容及相关代码并不难理解,这里就不在赘述了。
4. 总结
本文从 LinkedHashMap 维护双向链表的角度对 LinkedHashMap 的源码进行了分析,并在文章的结尾基于 LinkedHashMap 实现了一个简单的 Cache。在日常开发中,LinkedHashMap 的使用频率虽不及 HashMap,但它也个重要的实现。在 Java 集合框架中,HashMap、LinkedHashMap 和 TreeMap 三个映射类基于不同的数据结构,并实现了不同的功能。HashMap 底层基于拉链式的散列结构,并在 JDK 1.8 中引入红黑树优化过长链表的问题。基于这样结构,HashMap 可提供高效的增删改查操作。LinkedHashMap 在其之上,通过维护一条双向链表,实现了散列数据结构的有序遍历。TreeMap 底层基于红黑树实现,利用红黑树的性质,实现了键值对排序功能。我在前面几篇文章中,对 HashMap 和 TreeMap 以及他们均使用到的红黑树进行了详细的分析,有兴趣的朋友可以去看看。


TreeMap

按照惯例,我简单翻译了一下顶部的注释(我英文水平渣,如果有错的地方请多多包涵~欢迎在评论区下指正)
Java 集合 - 图22
接着我们来看看类继承图:
Java 集合 - 图23
在注释中提到的要点,我来总结一下:

  • TreeMap实现了NavigableMap接口,而NavigableMap接口继承着SortedMap接口,致使我们的TreeMap是有序的
  • TreeMap底层是红黑树,它方法的时间复杂度都不会太高:log(n)~
  • 非同步
  • 使用Comparator或者Comparable来比较key是否相等与排序的问题~

对我而言,Comparator和Comparable我都忘得差不多了~~~下面就开始看TreeMap的源码来看看它是怎么实现的,并且回顾一下Comparator和Comparable的用法吧!

1.1TreeMap的域

Java 集合 - 图24

1.2TreeMap构造方法

TreeMap的构造方法有4个:
Java 集合 - 图25
可以发现,TreeMap的构造方法大多数与comparator有关:
Java 集合 - 图26
也就是顶部注释说的:TreeMap有序是通过Comparator来进行比较的,如果comparator为null,那么就使用自然顺序~
打个比方:

  • 如果value是整数,自然顺序指的就是我们平常排序的顺序(1,2,3,4,5..)~

    1. TreeMap<Integer, Integer> treeMap = new TreeMap<>();
    2. treeMap.put(1, 5);
    3. treeMap.put(2, 4);
    4. treeMap.put(3, 3);
    5. treeMap.put(4, 2);
    6. treeMap.put(5, 1);
    7. for (Entry<Integer, Integer> entry : treeMap.entrySet()) {
    8. String s = entry.getKey() +"关注公众号:Java3y---->" + entry.getValue();
    9. System.out.println(s);
    10. }

    Java 集合 - 图27

    1.3put方法

    我们来看看TreeMap的核心put方法,阅读它就可以获取不少关于TreeMap特性的东西了~
    Java 集合 - 图28
    下面是compare(Object k1, Object k2)方法

    1. /**
    2. * Compares two keys using the correct comparison method for this TreeMap.
    3. */
    4. @SuppressWarnings("unchecked")
    5. final int compare(Object k1, Object k2) {
    6. return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
    7. : comparator.compare((K)k1, (K)k2);
    8. }

    如果我们设置key为null,会抛出异常的,就不执行下面的代码了。
    Java 集合 - 图29

    1.4get方法

    接下来我们来看看get方法的实现:
    Java 集合 - 图30
    点进去getEntry()看看实现:
    Java 集合 - 图31
    如果Comparator不为null,接下来我们进去看看getEntryUsingComparator(Object key),是怎么实现的
    Java 集合 - 图32

    1.5remove方法

    Java 集合 - 图33
    删除节点的时候调用的是deleteEntry(Entry<K,V> p)方法,这个方法主要是删除节点并且平衡红黑树
    平衡红黑树的代码是比较复杂的,我就不说了,你们去看吧(反正我看不懂)….

    1.6遍历方法

    在看源码的时候可能不知道哪个是核心的遍历方法,因为Iterator有非常非常多~
    Java 集合 - 图34
    此时,我们只需要debug一下看看,跟下去就好!
    Java 集合 - 图35
    Java 集合 - 图36
    于是乎,我们可以找到:TreeMap遍历是使用EntryIterator这个内部类的
    首先来看看EntryIterator的类结构图吧:
    Java 集合 - 图37
    可以发现,EntryIterator大多的实现都是在父类中:
    Java 集合 - 图38
    那接下来我们去看看PrivateEntryIterator比较重要的方法:
    Java 集合 - 图39
    我们进去successor(e)方法看看实现: successor 其实就是一个结点的 下一个结点,所谓 下一个,是按次序排序后的下一个结点。从代码中可以看出,如果右子树不为空,就返回右子树中最小结点。如果右子树为空,就要向上回溯了。在这种情况下,t 是以其为根的树的最后一个结点。如果它是其父结点的左孩子,那么父结点就是它的下一个结点,否则,t 就是以其父结点为根的树的最后一个结点,需要再次向上回溯。一直到 ch 是 p 的左孩子为止。 来源:https://blog.csdn.net/on_1y/article/details/27231855
    Java 集合 - 图40

    总结

    TreeMap底层是红黑树,能够实现该Map集合有序~
    如果在构造方法中传递了Comparator对象,那么就会以Comparator对象的方法进行比较。否则,则使用Comparable的compareTo(T o)方法来比较。

  • 值得说明的是:如果使用的是compareTo(T o)方法来比较,key一定是不能为null,并且得实现了Comparable接口的。

  • 即使是传入了Comparator对象,不用compareTo(T o)方法来比较,key也是不能为null的
    1. public static void main(String[] args) {
    2. TreeMap<Student, String> map = new TreeMap<Student, String>((o1, o2) -> {
    3. //主要条件
    4. int num = o1.getAge() - o2.getAge();
    5. //次要条件
    6. int num2 = num == 0 ? o1.getName().compareTo(o2.getName()) : num;
    7. return num2;
    8. });
    9. //创建学生对象
    10. Student s1 = new Student("潘安", 30);
    11. Student s2 = new Student("柳下惠", 35);
    12. //添加元素进集合
    13. map.put(s1, "宋朝");
    14. map.put(s2, "元朝");
    15. map.put(null, "汉朝");
    16. //获取key集合
    17. Set<Student> set = map.keySet();
    18. //遍历key集合
    19. for (Student student : set) {
    20. String value = map.get(student);
    21. System.out.println(student + "---------" + value);
    22. }
    23. }
    Java 集合 - 图41
    我们从源码中的很多地方中发现:Comparator和Comparable出现的频率是很高的,因为TreeMap实现有序要么就是外界传递进来Comparator对象,要么就使用默认key的Comparable接口(实现自然排序)
    最后我就来总结一下TreeMap要点吧:
  1. 由于底层是红黑树,那么时间复杂度可以保证为log(n)
  2. key不能为null,为null为抛出NullPointException的
  3. 想要自定义比较,在构造方法中传入Comparator对象,否则使用key的自然排序来进行比较
  4. TreeMap非同步的,想要同步可以使用Collections来进行封装

    自己实现一个一致性 Hash 算法(基于TreeMap)

    前言

    在前文分布式理论(八)—— Consistent Hash(一致性哈希算法)中,我们讨论了一致性 hash 算法的原理,并说了,我们会自己写一个简单的算法。今天就来写一个。

    普通 hash 的结果

    先看看普通 hash 怎么做。
    首先,需要缓存节点对象,缓存中的存储对象,还有一个缓存节点集合,用于保存有效的缓存节点。

  5. 实际存储对象,很简单的一个类,只需要获取他的 hash 值就好:

    1. static class Obj {
    2. String key;
    3. Obj(String key) {
    4. this.key = key;
    5. }
    6. @Override
    7. public int hashCode() {
    8. return key.hashCode();
    9. }
    10. @Override
    11. public String toString() {
    12. return "Obj{" +
    13. "key='" + key + '\'' +
    14. '}';
    15. }
    16. }
    17. 复制代码
  6. 缓存节点对象,用于存储实际对象:

    1. static class Node {
    2. Map<Integer, Obj> node = new HashMap<>();
    3. String name;
    4. Node(String name) {
    5. this.name = name;
    6. }
    7. public void putObj(Obj obj) {
    8. node.put(obj.hashCode(), obj);
    9. }
    10. Obj getObj(Obj obj) {
    11. return node.get(obj.hashCode());
    12. }
    13. @Override
    14. public int hashCode() {
    15. return name.hashCode();
    16. }
    17. }
    18. 复制代码

    也很简单,内部使用了一个 map 保存节点。

  7. 缓存节点集合,用于保存有效的缓存节点:

    1. static class NodeArray {
    2. Node[] nodes = new Node[1024];
    3. int size = 0;
    4. public void addNode(Node node) {
    5. nodes[size++] = node;
    6. }
    7. Obj get(Obj obj) {
    8. int index = obj.hashCode() % size;
    9. return nodes[index].getObj(obj);
    10. }
    11. void put(Obj obj) {
    12. int index = obj.hashCode() % size;
    13. nodes[index].putObj(obj);
    14. }
    15. }
    16. 复制代码

    内部一个数组,取数据时,通过取余机器数量获取缓存节点,再从节点中取出数据。

  8. 测试:当增减节点时,还能不能找到原有数据:

    1. /**
    2. * 验证普通 hash 对于增减节点,原有会不会出现移动。
    3. */
    4. public static void main(String[] args) {
    5. NodeArray nodeArray = new NodeArray();
    6. Node[] nodes = {
    7. new Node("Node--> 1"),
    8. new Node("Node--> 2"),
    9. new Node("Node--> 3")
    10. };
    11. for (Node node : nodes) {
    12. nodeArray.addNode(node);
    13. }
    14. Obj[] objs = {
    15. new Obj("1"),
    16. new Obj("2"),
    17. new Obj("3"),
    18. new Obj("4"),
    19. new Obj("5")
    20. };
    21. for (Obj obj : objs) {
    22. nodeArray.put(obj);
    23. }
    24. validate(nodeArray, objs);
    25. }
    26. 复制代码
    1. private static void validate(NodeArray nodeArray, Obj[] objs) {
    2. for (Obj obj : objs) {
    3. System.out.println(nodeArray.get(obj));
    4. }
    5. nodeArray.addNode(new Node("anything1"));
    6. nodeArray.addNode(new Node("anything2"));
    7. System.out.println("========== after =============");
    8. for (Obj obj : objs) {
    9. System.out.println(nodeArray.get(obj));
    10. }
    11. }
    12. 复制代码

    测试步骤如下:

  9. 向集合中添加 3 个节点。

  10. 集群 中添加 5 个对象,这 5 个对象会根据 hash 值散列到不同的节点中。
  11. 打印 未增减前 的数据。
  12. 打印 增加 2 个节点 后数据,看看还能不能访问到数据。

结果:
Java 集合 - 图42
一个都访问不到了。这就是普通的取余的缺点,在增减机器的情况下,这种结果无法接收。
再看看一致性 hash 如何解决。

一致性 Hash 的结果

关键的地方来了。
缓存节点对象和实际保存对象不用更改,改的是什么?
改的是保存对象的方式和取出对象的方式,也就是不使用对机器进行取余的算法。
新的 NodeArray 对象如下:

  1. static class NodeArray {
  2. /** 按照 键 排序*/
  3. TreeMap<Integer, Node> nodes = new TreeMap<>();
  4. void addNode(Node node) {
  5. nodes.put(node.hashCode(), node);
  6. }
  7. void put(Obj obj) {
  8. int objHashcode = obj.hashCode();
  9. Node node = nodes.get(objHashcode);
  10. if (node != null) {
  11. node.putObj(obj);
  12. return;
  13. }
  14. // 找到比给定 key 大的集合
  15. SortedMap<Integer, Node> tailMap = nodes.tailMap(objHashcode);
  16. // 找到最小的节点
  17. int nodeHashcode = tailMap.isEmpty() ? nodes.firstKey() : tailMap.firstKey();
  18. nodes.get(nodeHashcode).putObj(obj);
  19. }
  20. Obj get(Obj obj) {
  21. Node node = nodes.get(obj.hashCode());
  22. if (node != null) {
  23. return node.getObj(obj);
  24. }
  25. // 找到比给定 key 大的集合
  26. SortedMap<Integer, Node> tailMap = nodes.tailMap(obj.hashCode());
  27. // 找到最小的节点
  28. int nodeHashcode = tailMap.isEmpty() ? nodes.firstKey() : tailMap.firstKey();
  29. return nodes.get(nodeHashcode).getObj(obj);
  30. }
  31. }
  32. 复制代码

该类和之前的类的不同之处在于:

  1. 内部没有使用数组,而是使用了有序 Map。
  2. put 方法中,对象如果没有落到缓存节点上,就找比他小的节点且离他最近的。这里我们使用了 TreeMap 的 tailMap 方法,具体 API 可以看文档。
  3. get 方法中,和 put 步骤相同,否则是取不到对象的。

具体寻找节点的方式如图:
Java 集合 - 图43
相同的测试用例,执行结果如下:
Java 集合 - 图44
找到了之前所有的节点。解决了普通 hash 的问题。

总结

代码比较简单,主要是通过 JDK 自带的 TreeMap 实现的寻找临近节点。当然,我们这里也只是测试了添加,关于修改还没有测试,但思路是一样的。这里只是做一个抛砖引玉。
同时,我们也没有实现虚拟节点,感兴趣的朋友可以尝试一下。
good luck!!!!


HashSet

HashSet 源码只有短短的 300 行,上文也阐述了实现依赖于 HashMap,这一点充分体现在其构造方法和成员变量上。我们来看下 HashSet 的构造方法和成员变量:

  1. // HashSet 真实的存储元素结构
  2. private transient HashMap<E,Object> map;
  3. // 作为各个存储在 HashMap 元素的键值对中的 Value
  4. private static final Object PRESENT = new Object();
  5. //空参数构造方法 调用 HashMap 的空构造参数
  6. //初始化了 HashMap 中的加载因子 loadFactor = 0.75f
  7. public HashSet() {
  8. map = new HashMap<>();
  9. }
  10. //指定期望容量的构造方法
  11. public HashSet(int initialCapacity) {
  12. map = new HashMap<>(initialCapacity);
  13. }
  14. //指定期望容量和加载因子
  15. public HashSet(int initialCapacity, float loadFactor) {
  16. map = new HashMap<>(initialCapacity, loadFactor);
  17. }
  18. //使用指定的集合填充Set
  19. public HashSet(Collection<? extends E> c) {
  20. //调用 new HashMap<>(initialCapacity) 其中初始期望容量为 16 和 c 容量 / 默认 load factor 后 + 1的较大值
  21. map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
  22. addAll(c);
  23. }
  24. // 该方法为 default 访问权限,不允许使用者直接调用,目的是为了初始化 LinkedHashSet 时使用
  25. HashSet(int initialCapacity, float loadFactor, boolean dummy) {
  26. map = new LinkedHashMap<>(initialCapacity, loadFactor);
  27. }

通过 HashSet 的构造参数我们可以看出每个构造方法,都调用了对应的 HashMap 的构造方法用来初始化成员变量 map ,因此我们可以知道,HashSet 的初始容量也为 1<<4 即16,加载因子默认也是 0.75f。

我们都知道 Set 不允许存储重复元素,又由构造参数得出结论底层存储结构为 HashMap,那么这个不可重复的属性必然是有 HashMap 中存储键值对的 Key 来实现了。在分析 HashMap 的时候,提到过 HashMap 通过存储键值对的 Key 的 hash 值(经过扰动函数hash()处理后)来决定键值对在哈希表中的位置,当 Key 的 hash 值相同时,再通过 equals 方法判读是否是替换原来对应 key 的 Value 还是存储新的键值对。那么我们在使用 Set 方法的时候也必须保证,存储元素的 HashCode 方法以及 equals 方法被正确覆写。

HashSet 中的添加元素的方法也很简单,我们来看下实现:

  1. public boolean add(E e) {
  2. return map.put(e, PRESENT)==null;
  3. }

可以看出 add 方法调用了 HashMap 的 put 方法,构造的键值对的 key 为待添加的元素,而 Value 这时有全局变量 PRESENT 来充当,这个PRESENT只是一个 Object 对象。

除了 add 方法外 HashSet 实现了 Set 接口中的其他方法这些方法有:

  1. public int size() {
  2. return map.size();
  3. }
  4. public boolean isEmpty() {
  5. return map.isEmpty();
  6. }
  7. public boolean contains(Object o) {
  8. return map.containsKey(o);
  9. }
  10. //调用 remove(Object key) 方法去移除对应的键值对
  11. public boolean remove(Object o) {
  12. return map.remove(o)==PRESENT;
  13. }
  14. public void clear() {
  15. map.clear();
  16. }
  17. // 返回一个 map.keySet 的 HashIterator 来作为 Set 的迭代器
  18. public Iterator<E> iterator() {
  19. return map.keySet().iterator();
  20. }

关于 HashSet 中的源码分析就这些,其实除了一些序列化和克隆的方法以外,我们已经列举了所有的 HashSet 的源码,有没有感觉巨简单,其实下面的 LinkedHashSet 由于继承自 HashSet 使得其代码更加简单只有短短100多行不信继续往下看。

LinkedHashSet

在上述分析 HashSet 构造方法的时候,有一个 default 权限的构造方法没有讲,只说了其跟 LinkedHashSet 构造有关系,该构造方法内部调用的是 LinkedHashMap 的构造方法。

LinkedHashMap 较之 HashMap 内部多维护了一个双向链表用来维护元素的添加顺序:

  1. // dummy 参数没有作用这里可以忽略
  2. HashSet(int initialCapacity, float loadFactor, boolean dummy) {
  3. map = new LinkedHashMap<>(initialCapacity, loadFactor);
  4. }
  5. //调用 LinkedHashMap 的构造方法,该方法初始化了初始起始容量,以及加载因子,
  6. //accessOrder = false 即迭代顺序不等于访问顺序
  7. public LinkedHashMap(int initialCapacity, float loadFactor) {
  8. super(initialCapacity, loadFactor);
  9. accessOrder = false;
  10. }

LinkedHashSet的构造方法一共有四个,统一调用了父类的 HashSet(int initialCapacity, float loadFactor, boolean dummy)构造方法。

  1. //初始化 LinkedHashMap 的初始容量为诶 16 加载因子为 0.75f
  2. public LinkedHashSet() {
  3. super(16, .75f, true);
  4. }
  5. //初始化 LinkedHashMap 的初始容量为 Math.max(2*c.size(), 11) 加载因子为 0.75f
  6. public LinkedHashSet(Collection<? extends E> c) {
  7. super(Math.max(2*c.size(), 11), .75f, true);
  8. addAll(c);
  9. }
  10. //初始化 LinkedHashMap 的初始容量为参数指定值 加载因子为 0.75f
  11. public LinkedHashSet(int initialCapacity) {
  12. super(initialCapacity, .75f, true);
  13. }
  14. //初始化 LinkedHashMap 的初始容量,加载因子为参数指定值
  15. public LinkedHashSet(int initialCapacity, float loadFactor) {
  16. super(initialCapacity, loadFactor, true);
  17. }

完了..没错,LinkedHashSet 源码就这几行,所以可以看出其实现依赖于 LinkedHashMap 内部的数据存储结构。
原文地址:https://www.yuque.com/lobotomy/java/tep84p