17.1 完整的集合分类

第17章 集合深入研究 - 图1

ArrayList源码分析

  • ArrayList是一种集合类,其底层基于数组实现,所以查找操作可在O(1)的时间范围内实现
  • 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。
  • JDK7 new无参构造的ArrayList对象时,直接创建了长度是10的Object[]数组elementData 。jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式。

**

ArrayList扩容机制

  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 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)
**

插入元素

  1. /** 在元素序列尾部插入 */
  2. public boolean add(E e) {
  3. // 1. 检测是否需要扩容
  4. ensureCapacityInternal(size + 1); // Increments modCount!!
  5. // 2. 将新元素插入序列尾部
  6. elementData[size++] = e;
  7. return true;
  8. }
  9. /** 在元素序列 index 位置处插入 */
  10. public void add(int index, E element) {
  11. rangeCheckForAdd(index);
  12. // 1. 检测是否需要扩容
  13. ensureCapacityInternal(size + 1); // Increments modCount!!
  14. // 2. 将 index 及其之后的所有元素都向后移一位
  15. System.arraycopy(elementData, index, elementData, index + 1,
  16. size - index);
  17. // 3. 将新元素插入至 index 处
  18. elementData[index] = element;
  19. size++;
  20. }

插入元素总结:

对于在元素序列尾部插入,这种情况比较简单,只需两个步骤即可:

  • 检测数组是否有足够的空间插入
  • 将新元素插入至序列尾部

如果是在元素序列指定位置(假设该位置合理)插入,则情况稍微复杂一点,需要三个步骤:

  • 检测数组是否有足够的空间
  • 将 index 及其之后的所有元素向后移一位
  • 将新元素插入至 index 处

删除元素

  1. /** 删除指定位置的元素 */
  2. public E remove(int index) {
  3. rangeCheck(index);
  4. modCount++;
  5. // 返回被删除的元素值
  6. E oldValue = elementData(index);
  7. int numMoved = size - index - 1;
  8. if (numMoved > 0)
  9. // 将 index + 1 及之后的元素向前移动一位,覆盖被删除值
  10. System.arraycopy(elementData, index+1, elementData, index,
  11. numMoved);
  12. // 将最后一个元素置空,并将 size 值减1
  13. elementData[--size] = null; // clear to let GC do its work
  14. return oldValue;
  15. }
  16. E elementData(int index) {
  17. return (E) elementData[index];
  18. }
  19. /** 删除指定元素,若元素重复,则只删除下标最小的元素 */
  20. public boolean remove(Object o) {
  21. if (o == null) {
  22. for (int index = 0; index < size; index++)
  23. if (elementData[index] == null) {
  24. fastRemove(index);
  25. return true;
  26. }
  27. } else {
  28. // 遍历数组,查找要删除元素的位置
  29. for (int index = 0; index < size; index++)
  30. if (o.equals(elementData[index])) {
  31. fastRemove(index);
  32. return true;
  33. }
  34. }
  35. return false;
  36. }
  37. /** 快速删除,不做边界检查,也不返回删除的元素值 */
  38. private void fastRemove(int index) {
  39. modCount++;
  40. int numMoved = size - index - 1;
  41. if (numMoved > 0)
  42. System.arraycopy(elementData, index+1, elementData, index,
  43. numMoved);
  44. elementData[--size] = null; // clear to let GC do its work
  45. }

删除元素总结:

以第一个删除方法为例,删除一个元素步骤如下:

  • 获取指定位置 index 处的元素值
  • 将 index + 1 及之后的元素向前移动一位
  • 将最后一个元素置空,并将 size 值减 1
  • 返回被删除值,完成删除操作

LinkedList源码分析

  • LinkedList是一个实现了List接口和Deque接口的双端链表
  • LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; LinkedList不是线程安全的。和 ArrayList 一样,LinkedList 也支持空值和重复值。
  • 由于 LinkedList 基于链表实现,存储元素过程中,无需像 ArrayList 那样进行扩容。但有得必有失,LinkedList 存储元素的节点需要额外的空间存储前驱和后继的引用。

查找元素:

  1. public E get(int index) {
  2. checkElementIndex(index);
  3. return node(index).item;
  4. }
  5. Node<E> node(int index) {
  6. /*
  7. * 则从头节点开始查找,否则从尾节点查找
  8. * 查找位置 index 如果小于节点数量的一半,
  9. */
  10. if (index < (size >> 1)) {
  11. Node<E> x = first;
  12. // 循环向后查找,直至 i == index
  13. for (int i = 0; i < index; i++)
  14. x = x.next;
  15. return x;
  16. } else {
  17. Node<E> x = last;
  18. for (int i = size - 1; i > index; i--)
  19. x = x.prev;
  20. return x;
  21. }
  22. }

查找元素总结:

  • 如果查找位置小于节点数的一半,则从头节点开始找
  • 如果查找位置大于等于节点数一半,则从尾节点向前找

插入元素:

  1. /** 在链表尾部插入元素 */
  2. public boolean add(E e) {
  3. linkLast(e);
  4. return true;
  5. }
  6. /** 在链表指定位置插入元素 */
  7. public void add(int index, E element) {
  8. checkPositionIndex(index);
  9. // 判断 index 是不是链表尾部位置,如果是,直接将元素节点插入链表尾部即可
  10. if (index == size)
  11. linkLast(element);
  12. else
  13. linkBefore(element, node(index));
  14. }
  15. /** 将元素节点插入到链表尾部 */
  16. void linkLast(E e) {
  17. final Node<E> l = last;
  18. // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
  19. final Node<E> newNode = new Node<>(l, e, null);
  20. // 将 last 引用指向新节点
  21. last = newNode;
  22. // 判断尾节点是否为空,为空表示当前链表还没有节点
  23. if (l == null)
  24. first = newNode;
  25. else
  26. l.next = newNode; // 让原尾节点后继引用 next 指向新的尾节点
  27. size++;
  28. modCount++;
  29. }
  30. /** 将元素节点插入到 succ 之前的位置 */
  31. void linkBefore(E e, Node<E> succ) {
  32. // assert succ != null;
  33. final Node<E> pred = succ.prev;
  34. // 1. 初始化节点,并指明前驱和后继节点
  35. final Node<E> newNode = new Node<>(pred, e, succ);
  36. // 2. 将 succ 节点前驱引用 prev 指向新节点
  37. succ.prev = newNode;
  38. // 判断尾节点是否为空,为空表示当前链表还没有节点
  39. if (pred == null)
  40. first = newNode;
  41. else
  42. pred.next = newNode; // 3. succ 节点前驱的后继引用指向新节点
  43. size++;
  44. modCount++;
  45. }

插入元素总结:

  • LinkedList分别有两个指针指向两端,**两端均可插入元素**
  • 一开始创建链表时前后指针指向都是空的。只有插入第一个元素后,前后指针才有指向

删除元素:

  1. public boolean remove(Object o) {
  2. if (o == null) {
  3. for (Node<E> x = first; x != null; x = x.next) {
  4. if (x.item == null) {
  5. unlink(x);
  6. return true;
  7. }
  8. }
  9. } else {
  10. // 遍历链表,找到要删除的节点
  11. for (Node<E> x = first; x != null; x = x.next) {
  12. if (o.equals(x.item)) {
  13. unlink(x); // 将节点从链表中移除
  14. return true;
  15. }
  16. }
  17. }
  18. return false;
  19. }
  20. public E remove(int index) {
  21. checkElementIndex(index);
  22. // 通过 node 方法定位节点,并调用 unlink 将节点从链表中移除
  23. return unlink(node(index));
  24. }
  25. /** 将某个节点从链表中移除 */
  26. E unlink(Node<E> x) {
  27. // assert x != null;
  28. final E element = x.item;
  29. final Node<E> next = x.next;
  30. final Node<E> prev = x.prev;
  31. // prev 为空,表明删除的是头节点
  32. if (prev == null) {
  33. first = next;
  34. } else {
  35. // 将 x 的前驱的后继指向 x 的后继
  36. prev.next = next;
  37. // 将 x 的前驱引用置空,断开与前驱的链接
  38. x.prev = null;
  39. }
  40. // next 为空,表明删除的是尾节点
  41. if (next == null) {
  42. last = prev;
  43. } else {
  44. // 将 x 的后继的前驱指向 x 的前驱
  45. next.prev = prev;
  46. // 将 x 的后继引用置空,断开与后继的链接
  47. x.next = null;
  48. }
  49. // 将 item 置空,方便 GC 回收
  50. x.item = null;
  51. size--;
  52. modCount++;
  53. return element;
  54. }

删除元素总结:

删除元素步骤如下:

  1. 将待删除节点 x 的前驱的后继指向 x 的后继
  2. 将待删除节点 x 的前驱引用置空,断开与前驱的链接
  3. 将待删除节点 x 的后继的前驱指向 x 的前驱
  4. 将待删除节点 x 的后继引用置空,断开与后继的链接

HashMap

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 JDK1.8 之后 HashMap 的组成多了红黑树,在满足下面两个条件之后,会执行链表转红黑树操作,用来加快搜索速度

  • 链表长度大于阈值(默认为 8)
  • HashMap 数组长度超过 64
  • 源码分析还需后期深入研究…